diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index a070515b3..b17ad3292 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -8,11 +8,14 @@ import Tune from './tunejs.js'; import { register } from '@strudel/core'; /** + * Deprecated. + * * 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 + * @deprecated * @example * i("0 1 2 3 4 5").tune("hexany15").mul("220").freq() * @example diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 3f940c89a..0dcc4c9d6 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core'; +import { register, _mod, parseNumeral, removeUndefineds, Pattern, parray, State } from '@strudel/core'; import Tune from './tunejs.js'; // returns a list of frequency ratios for given edo scale @@ -61,14 +61,8 @@ function xenOffset(xenScale, offset, index = 0) { const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); -// accepts a scale name such as 31edo, and a pattern -// pattern expected to follow format such that a value can be mapped -// to an edostep within the scale. Returns the pattern with -// values mapped to the frequencies associated with the given edosteps -// scaleNameOrRatios: string || number[], steps?: number - /** - * Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. + * Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. See first 4 examples for the different ways you can input a scale into `xen` * * @name xen * @returns Pattern @@ -87,6 +81,15 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); * Math.pow(2, 18/31), * ]).piano() * @example + * // However, with the above you won't get highlighting of the scale indices, + * // instead you could do something like: + * i("[<2 ->, 0 1 <- 2> 3]*2").xen([ + * "1:1", // tonic + * "5:4", // Major 3rd + * "[3:2 4:3]", // Perfect 5th, 4th + * "5:3" // Major 6th + * ]).s("tri") + * @example * // xen also supports all scale names that * // tune does: * i("0 1 2 3 4 5").xen("hexany15") @@ -96,27 +99,100 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); * i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") */ -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 (!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.i)); - // 10 is somewhat arbitrary - freq = trimFreq(freq); - hap.value = { ...otherValues, freq }; - return isEdo(scaleNameOrRatios) - ? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] }) - : hap; - }); - return removeUndefineds(haps); - }); -}); +export const xen = register( + 'xen', + function (scaleNameOrRatios, pat) { + const JIsc2 = (input) => { + let k = parray(input); + + return new Pattern((state) => { + let res = k.withValue((x) => x.map((y) => eval(y.join('/')))).query(state); + return res; + }); + }; + + const query = (state) => { + let haps = pat.query(state); + let resHaps = []; + + haps.forEach((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + 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; + let edo = false; + let st = new State(hap.wholeOrPart().intersection(state.span), state.controls); + + // Format: An array of values + // Each value is a pattern, representing an index on the scale + let sc = scaleNameOrRatios.query(st); + // Check to see if input is attempting to be a named scale or a raw scale + let isStr = typeof sc[0].value == 'string'; + let isRawScale = Array.isArray(sc[0].value) && sc[0].value.every((x) => typeof x == 'number'); + if (!isStr && !isRawScale) { + sc = sc[0].value; + } else if (isRawScale) { + sc = getXenScale(sc[0].value); + } + + if (!isStr && !isRawScale) { + let scaleByIndices = []; + for (let j = 0; j < sc.length; j++) { + scaleByIndices[j] = sc[j].query(st); + } + /* + Format: An array of 1 or more Haps, each Hap has an array of values of + one or more scale index definitions (contains whole scale) + Assumption: inner value array should be same length between each + */ + let sc2 = JIsc2(sc).query(st); + + let index = _mod(i, sc.length); + for (let j = 0; j < scaleByIndices[index].length; j++) { + let scale = getXenScale(sc2[j].value); + let freq = xenOffset(scale, parseNumeral(i)); + let newHap = hap; + freq = trimFreq(freq); + newHap.value = { ...otherValues, freq }; + newHap = newHap.setContext(newHap.combineContext(scaleByIndices[index][j])); + resHaps.push(newHap); + } + } else if (isRawScale) { + let freq = xenOffset(sc, parseNumeral(i)); + let newHap = hap; + freq = trimFreq(freq); + newHap.value = { ...otherValues, freq }; + resHaps.push(newHap); + } else if (isStr) { + // scale is named scale(s) + for (let j = 0; j < sc.length; j++) { + let scaleName = sc[j].value; + let scale = getXenScale(scaleName); + let freq = xenOffset(scale, parseNumeral(i)); + let newHap = hap; + freq = trimFreq(freq); + newHap.value = { ...otherValues, freq }; + newHap = newHap.setContext(newHap.combineContext(sc[j])); + if (isEdo(scaleName)) { + edo = scaleName.match(/^([1-9]+[0-9]*)edo$/)[1]; + newHap = newHap.setContext({ ...newHap.context, edoSize: edo }); + } + resHaps.push(newHap); + } + } else { + throw new Error('failed to parse scale'); + } + }); + return resHaps; + }; + return new Pattern(query); + }, + false, +); /** * Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen` diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index a0a81ada8..325d4cab4 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -14916,6 +14916,43 @@ exports[`runs examples > example "xen" example index 1 1`] = ` `; exports[`runs examples > example "xen" example index 2 1`] = ` +[ + "[ 0/1 → 1/8 | freq:220 s:tri ]", + "[ 0/1 → 1/2 | freq:330 s:tri ]", + "[ 1/8 → 1/4 | freq:275 s:tri ]", + "[ 3/8 → 1/2 | freq:366.6666667 s:tri ]", + "[ 1/2 → 5/8 | freq:220 s:tri ]", + "[ 5/8 → 3/4 | freq:275 s:tri ]", + "[ 3/4 → 7/8 | freq:293.3333333 s:tri ]", + "[ 7/8 → 1/1 | freq:366.6666667 s:tri ]", + "[ 1/1 → 9/8 | freq:220 s:tri ]", + "[ 1/1 → 3/2 | freq:330 s:tri ]", + "[ 9/8 → 5/4 | freq:275 s:tri ]", + "[ 11/8 → 3/2 | freq:366.6666667 s:tri ]", + "[ 3/2 → 13/8 | freq:220 s:tri ]", + "[ 13/8 → 7/4 | freq:275 s:tri ]", + "[ 7/4 → 15/8 | freq:293.3333333 s:tri ]", + "[ 15/8 → 2/1 | freq:366.6666667 s:tri ]", + "[ 2/1 → 17/8 | freq:220 s:tri ]", + "[ 2/1 → 5/2 | freq:330 s:tri ]", + "[ 17/8 → 9/4 | freq:275 s:tri ]", + "[ 19/8 → 5/2 | freq:366.6666667 s:tri ]", + "[ 5/2 → 21/8 | freq:220 s:tri ]", + "[ 21/8 → 11/4 | freq:275 s:tri ]", + "[ 11/4 → 23/8 | freq:293.3333333 s:tri ]", + "[ 23/8 → 3/1 | freq:366.6666667 s:tri ]", + "[ 3/1 → 25/8 | freq:220 s:tri ]", + "[ 3/1 → 7/2 | freq:330 s:tri ]", + "[ 25/8 → 13/4 | freq:275 s:tri ]", + "[ 27/8 → 7/2 | freq:366.6666667 s:tri ]", + "[ 7/2 → 29/8 | freq:220 s:tri ]", + "[ 29/8 → 15/4 | freq:275 s:tri ]", + "[ 15/4 → 31/8 | freq:293.3333333 s:tri ]", + "[ 31/8 → 4/1 | freq:366.6666667 s:tri ]", +] +`; + +exports[`runs examples > example "xen" example index 3 1`] = ` [ "[ 0/1 → 1/6 | freq:220 ]", "[ 1/6 → 1/3 | freq:275 ]", @@ -14944,7 +14981,7 @@ exports[`runs examples > example "xen" example index 2 1`] = ` ] `; -exports[`runs examples > example "xen" example index 3 1`] = ` +exports[`runs examples > example "xen" example index 4 1`] = ` [ "[ 0/1 → 1/8 | freq:220 ]", "[ 1/8 → 1/4 | freq:252.7136381 ]", diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 144a40f1a..3e6a5b12f 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -12,6 +12,12 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. +### xen(scaleOrRatios) + +{/* TODO add explanation of EDO to documentation */} + + + ### tune(scale) {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} @@ -110,9 +116,3 @@ You can also give tune a list of frequencies to use as the scale: 523.2511306012 ]).mul(220).freq();`} /> - -### xen(scaleOrRatios) - -{/* TODO add explanation of EDO to documentation */} - -