diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 291cfa9e6..1a3f326aa 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -394,6 +394,18 @@ export const { source, src } = registerControl('source', 'src'); */ // also see https://codeberg.org/uzu/strudel/pulls/63 export const { n } = registerControl('n'); + +/** + * Selects the given degree. Currently used in `xen` and `tune`: + * + * @name i + * @tags tonal + * @param {number | Pattern} value + * @example + * i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") + */ +export const { i } = registerControl('i'); + /** * Plays the given note name or midi number. A note name consists of * diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index 7584b7f2f..a70d8868c 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -8,23 +8,23 @@ import Tune from './tunejs.js'; import { register } from '@strudel/core'; /** - * Assumes a numerical pattern of EDO steps. Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`. + * Assumes pattern contains numerical scale degrees on the `i` control (see examples below). Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`. * @name tune * @returns Pattern * @memberof Pattern * @param {(string | number[] )} scale * @example - * "0 1 2 3 4 5".tune("hexany15").mul("220").freq() + * i("0 1 2 3 4 5").tune("hexany15").mul("220").freq() * @example * // You can set your root to be a * // particular note with getFreq: - * "4 8 9 10 - - 5 7 9 11 - -".tune("tranh3") + * i("4 8 9 10 - - 5 7 9 11 - -").tune("tranh3") * .mul(getFreq('c3')) * .freq().clip(.5).room(1) * @example * // You can also give tune a list of * // frequencies to use as the scale: - * "0 1 2 3 4".tune([ + * i("0 1 2 3 4").tune([ * 261.6255653006, * 302.72962012827, * 350.29154279212, @@ -32,6 +32,7 @@ import { register } from '@strudel/core'; * 469.00678383895, * 523.2511306012 * ]).mul(220).freq(); + * * @tags tonal */ @@ -45,6 +46,12 @@ export const tune = register('tune', (scale, pat) => { // if the tonic is a frequency, why are we putting in "1" tune.tonicize(1); return pat.withHap((hap) => { - return hap.withValue(() => tune.note(hap.value)); + if (typeof hap.value !== 'object') { + throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`) + } + // const { i, ...otherValues } = hap.value; + // hap.value = { ...otherValues, freq: tune.note(i)} + // return hap + return hap.withValue(() => tune.note(hap.value.i)); }); }); diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 206776f2e..762db61bc 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -77,11 +77,11 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); * @tags tonal * @example * // A minor triad in 31edo: - * "0 8 18".xen("31edo").piano() + * i("0 8 18").xen("31edo").piano() * @example * // You can also use xen with frequency ratios. * // This is equivalent to the above: - * "0 1 2".xen([ + * i("0 1 2").xen([ * Math.pow(2, 0/31), * Math.pow(2, 8/31), * Math.pow(2, 18/31), @@ -89,29 +89,27 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); * @example * // xen also supports all scale names that * // tune does: - * "0 1 2 3 4 5".xen("hexany15") + * i("0 1 2 3 4 5").xen("hexany15") * // equiv to: * // "0 1 2 3 4 5".tune("hexany15").mul("220").freq() * @example - * n("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") + * i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") */ -// TODO feat: change root frequency -// TODO add explanation for what "31edo" etc. are -// TODO (maybe): should this return freq ratios like tune does, for parity's sake? export const xen = register('xen', function (scaleNameOrRatios, pat) { return pat.withHaps((haps) => { haps = haps.map((hap) => { let hVal = hap.value; const isObject = typeof hVal === 'object'; - // If hVal is a pure value, place it on `n` so that we interpret it as an edoStep - hVal = isObject ? hVal : { n: hVal }; - const { n, value, ...otherValues } = hVal; + if (!isObject) { + throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`) + } + const { i, ...otherValues } = hVal; const scale = getXenScale(scaleNameOrRatios); - let freq = xenOffset(scale, parseNumeral(hVal.n)); + let freq = xenOffset(scale, parseNumeral(hVal.i)); // 10 is somewhat arbitrary freq = trimFreq(freq); - hap.value = isObject ? { ...otherValues, freq } : { freq }; + hap.value = { ...otherValues, freq }; return isEdo(scaleNameOrRatios) ? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] }) : hap; @@ -130,7 +128,7 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { * @tags tonal * * @example - * "[0 1 2 3] [3 4] [4 3 2 1]".xen("hexany23").withBase("<220 [300 200]>") + * i("[0 1 2 3] [3 4] [4 3 2 1]").xen("hexany23").withBase("<220 [300 200]>") * @example * mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).withBase("220:1") * // mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).mul(220).freq() @@ -174,16 +172,16 @@ export const withBase = register('withBase', (b, pat) => { * @returns {Pattern} * * @example - * "0 1 2".xen("12edo").ftrans("7") + * i("0 1 2").xen("12edo").ftrans("7") * // n("0 1 2").scale("A:chromatic").trans("7") * @example - * "0 8 18".xen("31edo").ftrans("<8 -8>") + * i("0 8 18").xen("31edo").ftrans("<8 -8>") * @example * // to transpose by steps of an edo, use "step:edo" : - * "0 7 8 18".xen("31edo").ftrans("<0 1:31 1:12>") + * i("0 7 8 18").xen("31edo").ftrans("<0 1:31 1:12>") * @example * // it can also work with frequency values directly - * "200 300 400".ftrans("<0 7:31 7>").freq() + * freq("200 300 400").ftrans("<0 7:31 7>") */ /* f = frequency (Hz) @@ -208,7 +206,7 @@ export const { ftrans, fTrans, ftranspose, fTranspose } = register( let hVal = hap.value; const isObject = typeof hVal === 'object'; hVal = isObject ? hVal : { freq: hVal }; - let { freq, value, ...otherValues } = hVal; + let { freq, ...otherValues } = hVal; if (edoSize == undefined && hap.context.edoSize != undefined) { edoSize = hap.context.edoSize; } else if (edoSize == undefined) { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index be58df2c7..831e48767 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5706,6 +5706,43 @@ exports[`runs examples > example "hush" example index 0 1`] = ` ] `; +exports[`runs examples > example "i" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | freq:220 ]", + "[ 1/8 → 1/4 | freq:252.7136381 ]", + "[ 1/4 → 3/8 | freq:290.2917404 ]", + "[ 3/8 → 1/2 | freq:333.4576446 ]", + "[ 1/2 → 5/8 | freq:383.0422479 ]", + "[ 5/8 → 3/4 | freq:440 ]", + "[ 3/4 → 7/8 | freq:505.4272762 ]", + "[ 7/8 → 1/1 | freq:580.5834807 ]", + "[ 1/1 → 9/8 | freq:220 ]", + "[ 9/8 → 5/4 | freq:235.7901618 ]", + "[ 5/4 → 11/8 | freq:252.7136381 ]", + "[ 11/8 → 3/2 | freq:270.8517709 ]", + "[ 3/2 → 13/8 | freq:290.2917404 ]", + "[ 13/8 → 7/4 | freq:311.1269837 ]", + "[ 7/4 → 15/8 | freq:333.4576446 ]", + "[ 15/8 → 2/1 | freq:357.3910544 ]", + "[ 2/1 → 17/8 | freq:220 ]", + "[ 17/8 → 9/4 | freq:230.404707 ]", + "[ 9/4 → 19/8 | freq:241.3014955 ]", + "[ 19/8 → 5/2 | freq:252.7136381 ]", + "[ 5/2 → 21/8 | freq:264.6655079 ]", + "[ 21/8 → 11/4 | freq:277.182631 ]", + "[ 11/4 → 23/8 | freq:290.2917404 ]", + "[ 23/8 → 3/1 | freq:304.0208336 ]", + "[ 3/1 → 25/8 | freq:220 ]", + "[ 25/8 → 13/4 | freq:275 ]", + "[ 13/4 → 27/8 | freq:293.3333333 ]", + "[ 27/8 → 7/2 | freq:330 ]", + "[ 7/2 → 29/8 | freq:352 ]", + "[ 29/8 → 15/4 | freq:440 ]", + "[ 15/4 → 31/8 | freq:550 ]", + "[ 31/8 → 4/1 | freq:586.6666667 ]", +] +`; + exports[`runs examples > example "inhabit" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", @@ -13289,92 +13326,92 @@ exports[`runs examples > example "tri" example index 0 1`] = ` exports[`runs examples > example "tune" example index 0 1`] = ` [ - "[ 0/1 → 1/6 | freq:0 ]", - "[ 1/6 → 1/3 | freq:220 ]", - "[ 1/3 → 1/2 | freq:440 ]", - "[ 1/2 → 2/3 | freq:660 ]", - "[ 2/3 → 5/6 | freq:880 ]", - "[ 5/6 → 1/1 | freq:1100 ]", - "[ 1/1 → 7/6 | freq:0 ]", - "[ 7/6 → 4/3 | freq:220 ]", - "[ 4/3 → 3/2 | freq:440 ]", - "[ 3/2 → 5/3 | freq:660 ]", - "[ 5/3 → 11/6 | freq:880 ]", - "[ 11/6 → 2/1 | freq:1100 ]", - "[ 2/1 → 13/6 | freq:0 ]", - "[ 13/6 → 7/3 | freq:220 ]", - "[ 7/3 → 5/2 | freq:440 ]", - "[ 5/2 → 8/3 | freq:660 ]", - "[ 8/3 → 17/6 | freq:880 ]", - "[ 17/6 → 3/1 | freq:1100 ]", - "[ 3/1 → 19/6 | freq:0 ]", - "[ 19/6 → 10/3 | freq:220 ]", - "[ 10/3 → 7/2 | freq:440 ]", - "[ 7/2 → 11/3 | freq:660 ]", - "[ 11/3 → 23/6 | freq:880 ]", - "[ 23/6 → 4/1 | freq:1100 ]", + "[ 0/1 → 1/6 | freq:{i:0} ]", + "[ 1/6 → 1/3 | freq:{i:1} ]", + "[ 1/3 → 1/2 | freq:{i:2} ]", + "[ 1/2 → 2/3 | freq:{i:3} ]", + "[ 2/3 → 5/6 | freq:{i:4} ]", + "[ 5/6 → 1/1 | freq:{i:5} ]", + "[ 1/1 → 7/6 | freq:{i:0} ]", + "[ 7/6 → 4/3 | freq:{i:1} ]", + "[ 4/3 → 3/2 | freq:{i:2} ]", + "[ 3/2 → 5/3 | freq:{i:3} ]", + "[ 5/3 → 11/6 | freq:{i:4} ]", + "[ 11/6 → 2/1 | freq:{i:5} ]", + "[ 2/1 → 13/6 | freq:{i:0} ]", + "[ 13/6 → 7/3 | freq:{i:1} ]", + "[ 7/3 → 5/2 | freq:{i:2} ]", + "[ 5/2 → 8/3 | freq:{i:3} ]", + "[ 8/3 → 17/6 | freq:{i:4} ]", + "[ 17/6 → 3/1 | freq:{i:5} ]", + "[ 3/1 → 19/6 | freq:{i:0} ]", + "[ 19/6 → 10/3 | freq:{i:1} ]", + "[ 10/3 → 7/2 | freq:{i:2} ]", + "[ 7/2 → 11/3 | freq:{i:3} ]", + "[ 11/3 → 23/6 | freq:{i:4} ]", + "[ 23/6 → 4/1 | freq:{i:5} ]", ] `; exports[`runs examples > example "tune" example index 1 1`] = ` [ - "[ 0/1 → 1/12 | freq:523.2511306011972 clip:0.5 room:1 ]", - "[ 1/12 → 1/6 | freq:1046.5022612023945 clip:0.5 room:1 ]", - "[ 1/6 → 1/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 1/4 → 1/3 | freq:1308.1278265029932 clip:0.5 room:1 ]", - "[ 1/2 → 7/12 | freq:654.0639132514966 clip:0.5 room:1 ]", - "[ 7/12 → 2/3 | freq:915.6894785520951 clip:0.5 room:1 ]", - "[ 2/3 → 3/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 3/4 → 5/6 | freq:1438.9406091532924 clip:0.5 room:1 ]", - "[ 1/1 → 13/12 | freq:523.2511306011972 clip:0.5 room:1 ]", - "[ 13/12 → 7/6 | freq:1046.5022612023945 clip:0.5 room:1 ]", - "[ 7/6 → 5/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 5/4 → 4/3 | freq:1308.1278265029932 clip:0.5 room:1 ]", - "[ 3/2 → 19/12 | freq:654.0639132514966 clip:0.5 room:1 ]", - "[ 19/12 → 5/3 | freq:915.6894785520951 clip:0.5 room:1 ]", - "[ 5/3 → 7/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 7/4 → 11/6 | freq:1438.9406091532924 clip:0.5 room:1 ]", - "[ 2/1 → 25/12 | freq:523.2511306011972 clip:0.5 room:1 ]", - "[ 25/12 → 13/6 | freq:1046.5022612023945 clip:0.5 room:1 ]", - "[ 13/6 → 9/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 9/4 → 7/3 | freq:1308.1278265029932 clip:0.5 room:1 ]", - "[ 5/2 → 31/12 | freq:654.0639132514966 clip:0.5 room:1 ]", - "[ 31/12 → 8/3 | freq:915.6894785520951 clip:0.5 room:1 ]", - "[ 8/3 → 11/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 11/4 → 17/6 | freq:1438.9406091532924 clip:0.5 room:1 ]", - "[ 3/1 → 37/12 | freq:523.2511306011972 clip:0.5 room:1 ]", - "[ 37/12 → 19/6 | freq:1046.5022612023945 clip:0.5 room:1 ]", - "[ 19/6 → 13/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 13/4 → 10/3 | freq:1308.1278265029932 clip:0.5 room:1 ]", - "[ 7/2 → 43/12 | freq:654.0639132514966 clip:0.5 room:1 ]", - "[ 43/12 → 11/3 | freq:915.6894785520951 clip:0.5 room:1 ]", - "[ 11/3 → 15/4 | freq:1177.3150438526939 clip:0.5 room:1 ]", - "[ 15/4 → 23/6 | freq:1438.9406091532924 clip:0.5 room:1 ]", + "[ 0/1 → 1/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 1/12 → 1/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 1/6 → 1/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 1/4 → 1/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 1/2 → 7/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 7/12 → 2/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 2/3 → 3/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 3/4 → 5/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 1/1 → 13/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 13/12 → 7/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 7/6 → 5/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 5/4 → 4/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 3/2 → 19/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 19/12 → 5/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 5/3 → 7/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 7/4 → 11/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 2/1 → 25/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 25/12 → 13/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 13/6 → 9/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 9/4 → 7/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 5/2 → 31/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 31/12 → 8/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 8/3 → 11/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 11/4 → 17/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 3/1 → 37/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 37/12 → 19/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 19/6 → 13/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 13/4 → 10/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 7/2 → 43/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 43/12 → 11/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 11/3 → 15/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 15/4 → 23/6 | freq:{i:11} clip:0.5 room:1 ]", ] `; exports[`runs examples > example "tune" example index 2 1`] = ` [ - "[ 0/1 → 1/5 | freq:0 ]", - "[ 1/5 → 2/5 | freq:220 ]", - "[ 2/5 → 3/5 | freq:440 ]", - "[ 3/5 → 4/5 | freq:660 ]", - "[ 4/5 → 1/1 | freq:880 ]", - "[ 1/1 → 6/5 | freq:0 ]", - "[ 6/5 → 7/5 | freq:220 ]", - "[ 7/5 → 8/5 | freq:440 ]", - "[ 8/5 → 9/5 | freq:660 ]", - "[ 9/5 → 2/1 | freq:880 ]", - "[ 2/1 → 11/5 | freq:0 ]", - "[ 11/5 → 12/5 | freq:220 ]", - "[ 12/5 → 13/5 | freq:440 ]", - "[ 13/5 → 14/5 | freq:660 ]", - "[ 14/5 → 3/1 | freq:880 ]", - "[ 3/1 → 16/5 | freq:0 ]", - "[ 16/5 → 17/5 | freq:220 ]", - "[ 17/5 → 18/5 | freq:440 ]", - "[ 18/5 → 19/5 | freq:660 ]", - "[ 19/5 → 4/1 | freq:880 ]", + "[ 0/1 → 1/5 | freq:{i:0} ]", + "[ 1/5 → 2/5 | freq:{i:1} ]", + "[ 2/5 → 3/5 | freq:{i:2} ]", + "[ 3/5 → 4/5 | freq:{i:3} ]", + "[ 4/5 → 1/1 | freq:{i:4} ]", + "[ 1/1 → 6/5 | freq:{i:0} ]", + "[ 6/5 → 7/5 | freq:{i:1} ]", + "[ 7/5 → 8/5 | freq:{i:2} ]", + "[ 8/5 → 9/5 | freq:{i:3} ]", + "[ 9/5 → 2/1 | freq:{i:4} ]", + "[ 2/1 → 11/5 | freq:{i:0} ]", + "[ 11/5 → 12/5 | freq:{i:1} ]", + "[ 12/5 → 13/5 | freq:{i:2} ]", + "[ 13/5 → 14/5 | freq:{i:3} ]", + "[ 14/5 → 3/1 | freq:{i:4} ]", + "[ 3/1 → 16/5 | freq:{i:0} ]", + "[ 16/5 → 17/5 | freq:{i:1} ]", + "[ 17/5 → 18/5 | freq:{i:2} ]", + "[ 18/5 → 19/5 | freq:{i:3} ]", + "[ 19/5 → 4/1 | freq:{i:4} ]", ] `; diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index a661c7b08..144a40f1a 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -20,7 +20,7 @@ These functions allow the use of scales other than your typical chromatic 12 bas Here's an example of how to configure a basic hexany scale: - + Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3` @@ -30,7 +30,7 @@ You can set your root to be a particular note with `getFreq` @@ -39,7 +39,7 @@ Some tunings become more pronounced with a longer reverb decay: -".tune("gumbeng") + tune={`i("<[5 6 8 10] - [5 7 9 12] -> -").tune("gumbeng") .mul(getFreq('c3')) .freq().clip(.8).room("3:10").rdim(10000).rfade(5)`} /> @@ -48,7 +48,7 @@ Additionally, you can combo this with `fmap` so that the base note changes: ".fmap(getFreq)) .freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`} /> @@ -57,8 +57,7 @@ Combining this with various polyrhythm tricks can become very evocative: ~ ~,<-4 -5>" - .transpose(4) + tune={`i("<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>".add(4)) .tune("iraq") .mul("".fmap(getFreq)) .freq().clip(.5).room(1).rfade(9)`} @@ -71,7 +70,7 @@ Take the `sanza` tuning: @@ -79,15 +78,15 @@ Take the `sanza` tuning: Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own. See how close they are on the pitch wheel? - + This quality is often due to how the tunings were formed with instruments that were played differently than a piano. As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical: ") + tune={`i("[0 1 2 3 4 5 6]@0.3 -" + .add("<2 5 8 1>")) .tune("sanza") .mul(getFreq('c3')).freq() .legato("3").room(1).rfade(5)`} @@ -102,7 +101,7 @@ You can also give tune a list of frequencies to use as the scale: