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 */}
-
-