From 7fc8f1c5c94713f3da4eded782ff16c0936cb109 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 30 Jan 2026 14:12:31 -0500 Subject: [PATCH 01/29] xen/scale parity improvements --- packages/xen/xen.mjs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index cc96f411..6ede4261 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 } from '@strudel/core'; +import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core'; export function edo(name) { if (!/^[1-9]+[0-9]*edo$/.test(name)) { @@ -49,11 +49,20 @@ function xenOffset(xenScale, offset, index = 0) { // scaleNameOrRatios: string || number[], steps?: number export const xen = register('xen', function (scaleNameOrRatios, pat) { - return pat.withHap((hap) => { - const scale = getXenScale(scaleNameOrRatios); - const frequency = xenOffset(scale, parseNumeral(hap.value)); - return hap.withValue(() => frequency); - }); + 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 a scale degree + hVal = isObject ? hVal : { n: hVal }; + const { n, value, ...otherValues } = hVal; + const scale = getXenScale(scaleNameOrRatios); + const frequency = xenOffset(scale, parseNumeral(hVal.n)); + hap.value = isObject ? {...otherValues, freq: frequency } : frequency + return hap; + }); + return removeUndefineds(haps) + }) }); export const tuning = register('tuning', function (ratios, pat) { From 3abcc5987493bceff092b78f95434a85387a1482 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 30 Jan 2026 14:13:36 -0500 Subject: [PATCH 02/29] adjust wording --- packages/xen/xen.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 6ede4261..908d7b12 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -53,7 +53,7 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { 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 a scale degree + // 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; const scale = getXenScale(scaleNameOrRatios); From 1e771d61503e51e675a1cee359e0b5ea0ff81814 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 23 Jan 2026 13:30:36 -0500 Subject: [PATCH 03/29] prelim work on uzu/strudel#1944 --- packages/xen/xen.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 908d7b12..2c4bcf65 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -6,6 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core'; + +// returns a list of frequency ratios for given edo scale export function edo(name) { if (!/^[1-9]+[0-9]*edo$/.test(name)) { throw new Error('not an edo scale: "' + name + '"'); @@ -18,12 +20,15 @@ const presets = { '12ji': [1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4, 4 / 3, 45 / 32, 3 / 2, 8 / 5, 5 / 3, 16 / 9, 15 / 8], }; +// Given a base frequency such as 220 and an edo scale, returns +// an array of frequencies representing the given edo scale in that base function withBase(freq, scale) { return scale.map((r) => r * freq); } const defaultBase = 220; +// Assumes a base of 220. Returns a filtered scale based on 'indices' function getXenScale(scale, indices) { if (typeof scale === 'string') { if (/^[1-9]+[0-9]*edo$/.test(scale)) { @@ -41,12 +46,17 @@ function getXenScale(scale, indices) { return scale.filter((_, i) => indices.includes(i)); } + function xenOffset(xenScale, offset, index = 0) { const i = _mod(index + offset, xenScale.length); const oct = Math.floor(offset / xenScale.length); return xenScale[i] * Math.pow(2, oct); } +// 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 export const xen = register('xen', function (scaleNameOrRatios, pat) { return pat.withHaps((haps) => { @@ -65,6 +75,7 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { }) }); +// not sure there's a point to having this and the above, seems like a proto version of the above. export const tuning = register('tuning', function (ratios, pat) { return pat.withHap((hap) => { const frequency = xenOffset(ratios, parseNumeral(hap.value)); From e3ad6b1f0affa9fef5a84ad4d84e45e13b9b678e Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 23 Jan 2026 16:32:41 -0500 Subject: [PATCH 04/29] added documentation for xen() re: uzu/strudel#1944 --- packages/xen/xen.mjs | 23 +++++++++++++++++++++++ website/src/pages/learn/xen.mdx | 13 ++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 2c4bcf65..266bc3fc 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -58,6 +58,29 @@ function xenOffset(xenScale, offset, index = 0) { // 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 EDO steps. Returns a new pattern with all values + * mapped to their associated frequency + * + * @name xen + * @returns Pattern + * @memberof Pattern + * @param {(string | number[] )} scaleNameOrRatios + * @tags music_theory + * @example + * "0 8 18".xen("31edo").freq().piano() + * @example + * // You can also use xen with frequency ratios. + * // This is equivalent to the above: + * "0 1 2".xen([ + * Math.pow(2, 0/31), + * Math.pow(2, 8/31), + * Math.pow(2, 18/31), + * ]).freq().piano() + */ + +// TODO support tunings defined in './tunejs.js' export const xen = register('xen', function (scaleNameOrRatios, pat) { return pat.withHaps((haps) => { haps = haps.map(hap=>{ diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index b8b27ac7..0dd27270 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -6,7 +6,7 @@ layout: ../../layouts/MainLayout.astro import { MiniRepl } from '../../docs/MiniRepl'; import { JsDoc } from '../../docs/JsDoc'; -# Xen Harmonic Functions +# Xen Harmonic Functions (experimental) These functions allow the use of scales other than your typical chromatic 12 based ones. @@ -93,3 +93,14 @@ Note the legato and reverb effects make sure the sound of the strumming gets to tones sound even more alive, too. The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern. + +### xen(scaleOrRatios) + + + +{/* */} From 47ad4bb1718b5897e0ad097ac71d496fe6a00234 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 23 Jan 2026 17:18:38 -0500 Subject: [PATCH 05/29] minor: remove space between Xen and Harmonic re uzu/strudel#1944 --- website/src/config.ts | 2 +- website/src/pages/learn/xen.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/config.ts b/website/src/config.ts index eb39d295..eb8bd1af 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -104,7 +104,7 @@ export const SIDEBAR: Sidebar = { Understand: [ { text: 'Coding syntax', link: 'learn/code' }, { text: 'Pitch', link: 'understand/pitch' }, - { text: 'Xen Harmonic Functions', link: 'learn/xen' }, + { text: 'Xenharmonic Functions', link: 'learn/xen' }, { text: 'Cycles', link: 'understand/cycles' }, { text: 'Voicings', link: 'understand/voicings' }, { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 0dd27270..0dd0ea5f 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -1,12 +1,12 @@ --- -title: Xen Harmonic Functions +title: Xenharmonic Functions layout: ../../layouts/MainLayout.astro --- import { MiniRepl } from '../../docs/MiniRepl'; import { JsDoc } from '../../docs/JsDoc'; -# Xen Harmonic Functions (experimental) +# Xenharmonic Functions (experimental) These functions allow the use of scales other than your typical chromatic 12 based ones. From 14cc5d989bebb1061cf494b2e1fb4d9d098ea1c4 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 23 Jan 2026 18:08:34 -0500 Subject: [PATCH 06/29] feat: added Tune scales to Xen, improved documentation re uzu/strudel#1944 --- packages/xen/tune.mjs | 4 ++++ packages/xen/tunejs.js | 18 ++++++++++++++---- packages/xen/xen.mjs | 18 ++++++++++++++++-- website/src/pages/learn/xen.mdx | 11 +++-------- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index 01303bf5..22b34569 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -7,12 +7,16 @@ This program is free software: you can redistribute it and/or modify it under th import Tune from './tunejs.js'; import { register } from '@strudel/core'; +// Tune.scale seems to be in ratio format +// + export const tune = register('tune', (scale, pat) => { const tune = new Tune(); if (!tune.isValidScale(scale)) { throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html'); } tune.loadScale(scale); + // 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)); diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 9f425370..e4e27251 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -78,14 +78,21 @@ Tune.prototype.frequency = function(stepIn, octaveIn) { } // which scale degree (0 - scale length) is our input + // 60 % 12 = 0 var scaleDegree = stepIn % this.scale.length + // what's this doing + // 0 scaleDegree = 12 + // seems to simply be for a negative result from above, maybe another way to do it, but ok for now while (scaleDegree < 0) { scaleDegree += this.scale.length } + // tonic is currently always 1 + // so this is 1*scale[scaledegree] var freq = this.tonic*this.scale[scaleDegree] + // map it to octave freq = freq*(Math.pow(2,octave)) // truncate irrational numbers @@ -137,10 +144,13 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) { } /* Load a new scale */ +// sets .scale to ratios Tune.prototype.loadScale = function(scale){ /* load the scale */ + let name + if (typeof scale === 'string') name = scale; var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies this.scale = [] for (var i=0;i { diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 0dd0ea5f..584d91b1 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -8,6 +8,8 @@ import { JsDoc } from '../../docs/JsDoc'; # Xenharmonic Functions (experimental) +{/* TODO expand explanation of xenharmony */} + These functions allow the use of scales other than your typical chromatic 12 based ones. ### tune(scale) @@ -96,11 +98,4 @@ The `tranh3` tuning has a similar set of notes, with two clashing. You might try ### xen(scaleOrRatios) - - -{/* */} + \ No newline at end of file From 77ced19144a775bba1955f463ff5b6822a0fb9b1 Mon Sep 17 00:00:00 2001 From: tyow Date: Sat, 24 Jan 2026 14:18:51 -0500 Subject: [PATCH 07/29] added jsdoc documentation for tune re uzu/strudel#1944 --- packages/xen/tune.mjs | 31 +++++++++++++++++++++++++++++-- packages/xen/xen.mjs | 11 ++++++----- website/src/pages/learn/xen.mdx | 17 +++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index 22b34569..1f7076b9 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -7,9 +7,36 @@ This program is free software: you can redistribute it and/or modify it under th import Tune from './tunejs.js'; import { register } from '@strudel/core'; -// Tune.scale seems to be in ratio format -// +/** + * 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`. + * @name tune + * @returns Pattern + * @memberof Pattern + * @param {(string | number[] )} scale + * @example + * "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") + * .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([ + * 261.6255653006, + * 302.72962012827, + * 350.29154279212, + * 405.32593044476, + * 469.00678383895, + * 523.2511306012 + * ]).mul(220).freq(); + * @tags tonal +*/ + +// Tune.scale seems to be in ratio format export const tune = register('tune', (scale, pat) => { const tune = new Tune(); if (!tune.isValidScale(scale)) { diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 33ea7ea1..15e5291c 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -29,6 +29,7 @@ function withBase(freq, scale) { const defaultBase = 220; // Assumes a base of 220. Returns a filtered scale based on 'indices' +// NOTE: indices functionality is unused function getXenScale(scale, indices) { let tune = new Tune() if (typeof scale === 'string') { @@ -66,14 +67,13 @@ function xenOffset(xenScale, offset, index = 0) { // scaleNameOrRatios: string || number[], steps?: number /** - * Assumes a numerical pattern of EDO steps. Returns a new pattern with all values - * mapped to their associated frequency + * Assumes a numerical pattern of EDO steps. Accepts all scale names of `tune` as well as any arbitrary edo scale. Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. * * @name xen * @returns Pattern * @memberof Pattern * @param {(string | number[] )} scaleNameOrRatios - * @tags music_theory + * @tags tonal * @example * // A major tried in 31edo: * "0 8 18".xen("31edo").freq().piano() @@ -93,8 +93,9 @@ function xenOffset(xenScale, offset, index = 0) { * // "0 1 2 3 4 5".tune("hexany15").mul("220").freq() */ -// TODO how do you reference another function in jsdoc (tune, above) -// TODO support tunings defined in './tunejs.js' +// 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=>{ diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 584d91b1..b32c93c8 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -13,6 +13,7 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. ### tune(scale) +{/* TODO (maybe): combine jsdoc things in tune.mjs with here */} @@ -96,6 +97,22 @@ tones sound even more alive, too. The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern. +You can also give tune a list of frequencies to use as the scale: + + + ### xen(scaleOrRatios) +{/* TODO add explanation of EDO to documentation */} + \ No newline at end of file From 6b525b481957d15d81bd157f3e96226cf4a71e56 Mon Sep 17 00:00:00 2001 From: tyow Date: Sat, 24 Jan 2026 14:30:15 -0500 Subject: [PATCH 08/29] formatting uzu/strudel#1944 --- packages/xen/tune.mjs | 21 ++- packages/xen/xen.mjs | 31 ++--- test/__snapshots__/examples.test.mjs.snap | 154 ++++++++++++++++++++++ website/src/pages/learn/xen.mdx | 3 +- 4 files changed, 180 insertions(+), 29 deletions(-) diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index 1f7076b9..7584b7f2 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -7,7 +7,6 @@ This program is free software: you can redistribute it and/or modify it under th 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`. * @name tune @@ -17,24 +16,24 @@ import { register } from '@strudel/core'; * @example * "0 1 2 3 4 5".tune("hexany15").mul("220").freq() * @example - * // You can set your root to be a + * // You can set your root to be a * // particular note with getFreq: - * `"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3") + * "4 8 9 10 - - 5 7 9 11 - -".tune("tranh3") * .mul(getFreq('c3')) - * .freq().clip(.5).room(1)` + * .freq().clip(.5).room(1) * @example - * // You can also give tune a list of + * // You can also give tune a list of * // frequencies to use as the scale: * "0 1 2 3 4".tune([ - * 261.6255653006, - * 302.72962012827, - * 350.29154279212, - * 405.32593044476, - * 469.00678383895, + * 261.6255653006, + * 302.72962012827, + * 350.29154279212, + * 405.32593044476, + * 469.00678383895, * 523.2511306012 * ]).mul(220).freq(); * @tags tonal -*/ + */ // Tune.scale seems to be in ratio format export const tune = register('tune', (scale, pat) => { diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 15e5291c..35fc3415 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core'; -import Tune from './tunejs.js' +import Tune from './tunejs.js'; // returns a list of frequency ratios for given edo scale export function edo(name) { @@ -31,18 +31,16 @@ const defaultBase = 220; // Assumes a base of 220. Returns a filtered scale based on 'indices' // NOTE: indices functionality is unused function getXenScale(scale, indices) { - let tune = new Tune() + let tune = new Tune(); if (typeof scale === 'string') { if (/^[1-9]+[0-9]*edo$/.test(scale)) { scale = edo(scale); } else if (presets[scale]) { scale = presets[scale]; - } - else if (tune.isValidScale(scale)) { - tune.loadScale(scale) - scale = tune.scale - } - else { + } else if (tune.isValidScale(scale)) { + tune.loadScale(scale); + scale = tune.scale; + } else { throw new Error('unknown scale name: "' + scale + '"'); } } @@ -53,7 +51,6 @@ function getXenScale(scale, indices) { return scale.filter((_, i) => indices.includes(i)); } - function xenOffset(xenScale, offset, index = 0) { const i = _mod(index + offset, xenScale.length); const oct = Math.floor(offset / xenScale.length); @@ -68,7 +65,7 @@ function xenOffset(xenScale, offset, index = 0) { /** * Assumes a numerical pattern of EDO steps. Accepts all scale names of `tune` as well as any arbitrary edo scale. Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. - * + * * @name xen * @returns Pattern * @memberof Pattern @@ -78,18 +75,18 @@ function xenOffset(xenScale, offset, index = 0) { * // A major tried in 31edo: * "0 8 18".xen("31edo").freq().piano() * @example - * // You can also use xen with frequency ratios. + * // You can also use xen with frequency ratios. * // This is equivalent to the above: * "0 1 2".xen([ - * Math.pow(2, 0/31), - * Math.pow(2, 8/31), - * Math.pow(2, 18/31), + * Math.pow(2, 0/31), + * Math.pow(2, 8/31), + * Math.pow(2, 18/31), * ]).freq().piano() - * @example - * // xen also supports all scale names that + * @example + * // xen also supports all scale names that * // tune does: * "0 1 2 3 4 5".xen("hexany15").freq() - * // equiv to: + * // equiv to: * // "0 1 2 3 4 5".tune("hexany15").mul("220").freq() */ diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index b205e4e5..724c3fc1 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -13215,6 +13215,97 @@ 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 ]", +] +`; + +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 ]", +] +`; + +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 ]", +] +`; + exports[`runs examples > example "undegrade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", @@ -14127,6 +14218,69 @@ exports[`runs examples > example "wtphaserand" example index 0 1`] = ` ] `; +exports[`runs examples > example "xen" example index 0 1`] = ` +[ + "[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 1/3 → 2/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 2/3 → 1/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 4/3 → 5/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 5/3 → 2/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 7/3 → 8/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 8/3 → 3/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 10/3 → 11/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 11/3 → 4/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", +] +`; + +exports[`runs examples > example "xen" example index 1 1`] = ` +[ + "[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 1/3 → 2/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 2/3 → 1/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 4/3 → 5/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 5/3 → 2/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 7/3 → 8/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 8/3 → 3/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 10/3 → 11/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 11/3 → 4/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", +] +`; + +exports[`runs examples > example "xen" example index 2 1`] = ` +[ + "[ 0/1 → 1/6 | freq:220 ]", + "[ 1/6 → 1/3 | freq:275 ]", + "[ 1/3 → 1/2 | freq:293.33333333333616 ]", + "[ 1/2 → 2/3 | freq:330 ]", + "[ 2/3 → 5/6 | freq:352 ]", + "[ 5/6 → 1/1 | freq:440 ]", + "[ 1/1 → 7/6 | freq:220 ]", + "[ 7/6 → 4/3 | freq:275 ]", + "[ 4/3 → 3/2 | freq:293.33333333333616 ]", + "[ 3/2 → 5/3 | freq:330 ]", + "[ 5/3 → 11/6 | freq:352 ]", + "[ 11/6 → 2/1 | freq:440 ]", + "[ 2/1 → 13/6 | freq:220 ]", + "[ 13/6 → 7/3 | freq:275 ]", + "[ 7/3 → 5/2 | freq:293.33333333333616 ]", + "[ 5/2 → 8/3 | freq:330 ]", + "[ 8/3 → 17/6 | freq:352 ]", + "[ 17/6 → 3/1 | freq:440 ]", + "[ 3/1 → 19/6 | freq:220 ]", + "[ 19/6 → 10/3 | freq:275 ]", + "[ 10/3 → 7/2 | freq:293.33333333333616 ]", + "[ 7/2 → 11/3 | freq:330 ]", + "[ 11/3 → 23/6 | freq:352 ]", + "[ 23/6 → 4/1 | freq:440 ]", +] +`; + exports[`runs examples > example "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index b32c93c8..a661c7b0 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -13,6 +13,7 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. ### tune(scale) + {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} @@ -115,4 +116,4 @@ You can also give tune a list of frequencies to use as the scale: {/* TODO add explanation of EDO to documentation */} - \ No newline at end of file + From aa3698c4f42488c0c846017a9da9d4ceddc9e893 Mon Sep 17 00:00:00 2001 From: tyow Date: Mon, 26 Jan 2026 17:50:38 -0500 Subject: [PATCH 09/29] Small wording changes, bring up to date with main, uzu/strudel#1944 --- packages/xen/xen.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 35fc3415..f8527ed7 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -64,7 +64,7 @@ function xenOffset(xenScale, offset, index = 0) { // scaleNameOrRatios: string || number[], steps?: number /** - * Assumes a numerical pattern of EDO steps. Accepts all scale names of `tune` as well as any arbitrary edo scale. 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. Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. * * @name xen * @returns Pattern @@ -72,7 +72,7 @@ function xenOffset(xenScale, offset, index = 0) { * @param {(string | number[] )} scaleNameOrRatios * @tags tonal * @example - * // A major tried in 31edo: + * // A major triad in 31edo: * "0 8 18".xen("31edo").freq().piano() * @example * // You can also use xen with frequency ratios. From 8def3f6af6f4c5f06b3c90b26e74329693ed109e Mon Sep 17 00:00:00 2001 From: tyow Date: Mon, 26 Jan 2026 17:52:25 -0500 Subject: [PATCH 10/29] add note about octave assumption uzu/strudel#1944 --- packages/xen/xen.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index f8527ed7..5d205cf8 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -64,7 +64,7 @@ function xenOffset(xenScale, offset, index = 0) { // 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. 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. * * @name xen * @returns Pattern From 35e32a32e817bd5f97c00cb2d6d9e34ec098f8e7 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 30 Jan 2026 16:59:33 -0500 Subject: [PATCH 11/29] added frequency rounding --- packages/xen/xen.mjs | 4 ++- test/__snapshots__/examples.test.mjs.snap | 40 +++++++++++------------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 5d205cf8..554eb26c 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -102,7 +102,9 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { hVal = isObject ? hVal : { n: hVal }; const { n, value, ...otherValues } = hVal; const scale = getXenScale(scaleNameOrRatios); - const frequency = xenOffset(scale, parseNumeral(hVal.n)); + let frequency = xenOffset(scale, parseNumeral(hVal.n)); + // 10 is somewhat arbitrary + frequency = parseFloat(frequency.toPrecision(10)) hap.value = isObject ? {...otherValues, freq: frequency } : frequency return hap; }); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 724c3fc1..7d7b12e1 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -14221,34 +14221,34 @@ exports[`runs examples > example "wtphaserand" example index 0 1`] = ` exports[`runs examples > example "xen" example index 0 1`] = ` [ "[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 1/3 → 2/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 2/3 → 1/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 1/3 → 2/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 2/3 → 1/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 4/3 → 5/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 5/3 → 2/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 4/3 → 5/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 5/3 → 2/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 7/3 → 8/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 8/3 → 3/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 7/3 → 8/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 8/3 → 3/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 10/3 → 11/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 11/3 → 4/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 10/3 → 11/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 11/3 → 4/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", ] `; exports[`runs examples > example "xen" example index 1 1`] = ` [ "[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 1/3 → 2/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 2/3 → 1/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 1/3 → 2/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 2/3 → 1/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 4/3 → 5/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 5/3 → 2/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 4/3 → 5/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 5/3 → 2/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 7/3 → 8/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 8/3 → 3/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 7/3 → 8/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 8/3 → 3/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ 10/3 → 11/3 | freq:263.09212028971103 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 11/3 → 4/1 | freq:329.01393411660587 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 10/3 → 11/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 11/3 → 4/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", ] `; @@ -14256,25 +14256,25 @@ exports[`runs examples > example "xen" example index 2 1`] = ` [ "[ 0/1 → 1/6 | freq:220 ]", "[ 1/6 → 1/3 | freq:275 ]", - "[ 1/3 → 1/2 | freq:293.33333333333616 ]", + "[ 1/3 → 1/2 | freq:293.3333333 ]", "[ 1/2 → 2/3 | freq:330 ]", "[ 2/3 → 5/6 | freq:352 ]", "[ 5/6 → 1/1 | freq:440 ]", "[ 1/1 → 7/6 | freq:220 ]", "[ 7/6 → 4/3 | freq:275 ]", - "[ 4/3 → 3/2 | freq:293.33333333333616 ]", + "[ 4/3 → 3/2 | freq:293.3333333 ]", "[ 3/2 → 5/3 | freq:330 ]", "[ 5/3 → 11/6 | freq:352 ]", "[ 11/6 → 2/1 | freq:440 ]", "[ 2/1 → 13/6 | freq:220 ]", "[ 13/6 → 7/3 | freq:275 ]", - "[ 7/3 → 5/2 | freq:293.33333333333616 ]", + "[ 7/3 → 5/2 | freq:293.3333333 ]", "[ 5/2 → 8/3 | freq:330 ]", "[ 8/3 → 17/6 | freq:352 ]", "[ 17/6 → 3/1 | freq:440 ]", "[ 3/1 → 19/6 | freq:220 ]", "[ 19/6 → 10/3 | freq:275 ]", - "[ 10/3 → 7/2 | freq:293.33333333333616 ]", + "[ 10/3 → 7/2 | freq:293.3333333 ]", "[ 7/2 → 11/3 | freq:330 ]", "[ 11/3 → 23/6 | freq:352 ]", "[ 23/6 → 4/1 | freq:440 ]", From c2e45eb36c8eac4437e429a62e5449ef1cbb0569 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 30 Jan 2026 17:22:52 -0500 Subject: [PATCH 12/29] code format --- packages/xen/xen.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 554eb26c..0310a00c 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -103,8 +103,8 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { const { n, value, ...otherValues } = hVal; const scale = getXenScale(scaleNameOrRatios); let frequency = xenOffset(scale, parseNumeral(hVal.n)); - // 10 is somewhat arbitrary - frequency = parseFloat(frequency.toPrecision(10)) + // 10 is somewhat arbitrary + frequency = parseFloat(frequency.toPrecision(10)); hap.value = isObject ? {...otherValues, freq: frequency } : frequency return hap; }); From 99295275ec58f73cd72a5d2e542aa00308a8c85d Mon Sep 17 00:00:00 2001 From: tyow Date: Sun, 1 Feb 2026 00:39:22 -0500 Subject: [PATCH 13/29] ftrans draft --- packages/xen/xen.mjs | 75 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 0310a00c..ffb783ef 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -28,12 +28,14 @@ function withBase(freq, scale) { const defaultBase = 220; +const isEdo = (scale) => /^[1-9]+[0-9]*edo$/.test(scale); + // Assumes a base of 220. Returns a filtered scale based on 'indices' // NOTE: indices functionality is unused function getXenScale(scale, indices) { let tune = new Tune(); if (typeof scale === 'string') { - if (/^[1-9]+[0-9]*edo$/.test(scale)) { + if (isEdo(scale)) { scale = edo(scale); } else if (presets[scale]) { scale = presets[scale]; @@ -72,7 +74,7 @@ function xenOffset(xenScale, offset, index = 0) { * @param {(string | number[] )} scaleNameOrRatios * @tags tonal * @example - * // A major triad in 31edo: + * // A minor triad in 31edo: * "0 8 18".xen("31edo").freq().piano() * @example * // You can also use xen with frequency ratios. @@ -95,23 +97,76 @@ function xenOffset(xenScale, offset, index = 0) { // 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 + 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; const scale = getXenScale(scaleNameOrRatios); let frequency = xenOffset(scale, parseNumeral(hVal.n)); - // 10 is somewhat arbitrary - frequency = parseFloat(frequency.toPrecision(10)); - hap.value = isObject ? {...otherValues, freq: frequency } : frequency - return hap; + // 10 is somewhat arbitrary + frequency = parseFloat(frequency.toPrecision(10)); + hap.value = isObject ? { ...otherValues, freq: frequency } : frequency; + return isEdo(scaleNameOrRatios) + ? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] }) + : hap; }); - return removeUndefineds(haps) - }) + return removeUndefineds(haps); + }); }); +/** + * Frequency transpose. Assumes pattern either has `freq` set, or has values that can be interpreted as frequencies + * amt has optional `edoSize` param, defaults to 12. + * If haps have edoSize param set, such as from the output of `xen("edo31")`, + * `ftrans` will fallback to that instead of 12 as the default. + * + * Transposes the frequency by `amt` edoSteps + * @name ftranspose + * @synonyms ftrans, fTrans ftranspose, fTranspose + * @param {number} amt + * @param {number} edoSize (optional) + * @returns {Pattern} + */ + +/* f = frequency (Hz) + n = edo (steps per octave) + x = number of steps + if 0\n = f, then x\n = f * 2^(x/n) + example: 5edo, 0\5 = 220 Hz, then 2\5 = 220*2^(2/5) = 290.29 Hz */ + +export const { ftrans, fTrans, ftranspose, fTranspose } = register( + ['ftrans', 'fTrans', 'ftranspose', 'fTranspose'], + (amt, pat) => { + let edoSize; + let numSteps; + if (Array.isArray(amt)) { + edoSize = amt[1]; + numSteps = amt[0]; + } else { + numSteps = amt; + } + return pat.withHaps((haps) => { + haps = haps.map((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + hVal = isObject ? hVal : { freq: hVal }; + let { freq, value, ...otherValues } = hVal; + if (edoSize == undefined && hap.context.edoSize != undefined) { + edoSize = hap.context.edoSize; + } else if (edoSize == undefined) { + edoSize = 12; + } + freq = freq * Math.pow(2, numSteps / edoSize); + hap.value = isObject ? { ...otherValues, freq } : freq; + return hap.setContext({ ...hap.context, edoSize }); + }); + return removeUndefineds(haps); + }); + }, +); + // not sure there's a point to having this and the above, seems like a proto version of the above. export const tuning = register('tuning', function (ratios, pat) { return pat.withHap((hap) => { From 530f45a7bb3be582b780cb4380f17e98ed65a6a3 Mon Sep 17 00:00:00 2001 From: tyow Date: Sun, 1 Feb 2026 00:39:42 -0500 Subject: [PATCH 14/29] format --- packages/xen/xen.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index ffb783ef..3629e5fe 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -119,9 +119,9 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { /** * Frequency transpose. Assumes pattern either has `freq` set, or has values that can be interpreted as frequencies * amt has optional `edoSize` param, defaults to 12. - * If haps have edoSize param set, such as from the output of `xen("edo31")`, - * `ftrans` will fallback to that instead of 12 as the default. - * + * If haps have edoSize param set, such as from the output of `xen("edo31")`, + * `ftrans` will fallback to that instead of 12 as the default. + * * Transposes the frequency by `amt` edoSteps * @name ftranspose * @synonyms ftrans, fTrans ftranspose, fTranspose From 55095ba4e8306dc07035696a08cff9db9136e132 Mon Sep 17 00:00:00 2001 From: tyow Date: Sun, 1 Feb 2026 13:34:22 -0500 Subject: [PATCH 15/29] added examples to ftrans --- packages/xen/xen.mjs | 27 +++++++-- test/__snapshots__/examples.test.mjs.snap | 68 +++++++++++++++++++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 3629e5fe..bd77f75a 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -59,6 +59,8 @@ function xenOffset(xenScale, offset, index = 0) { return xenScale[i] * Math.pow(2, oct); } +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 @@ -75,7 +77,7 @@ function xenOffset(xenScale, offset, index = 0) { * @tags tonal * @example * // A minor triad in 31edo: - * "0 8 18".xen("31edo").freq().piano() + * "0 8 18".xen("31edo").piano() * @example * // You can also use xen with frequency ratios. * // This is equivalent to the above: @@ -83,11 +85,11 @@ function xenOffset(xenScale, offset, index = 0) { * Math.pow(2, 0/31), * Math.pow(2, 8/31), * Math.pow(2, 18/31), - * ]).freq().piano() + * ]).piano() * @example * // xen also supports all scale names that * // tune does: - * "0 1 2 3 4 5".xen("hexany15").freq() + * "0 1 2 3 4 5".xen("hexany15") * // equiv to: * // "0 1 2 3 4 5".tune("hexany15").mul("220").freq() */ @@ -104,10 +106,10 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { hVal = isObject ? hVal : { n: hVal }; const { n, value, ...otherValues } = hVal; const scale = getXenScale(scaleNameOrRatios); - let frequency = xenOffset(scale, parseNumeral(hVal.n)); + let freq = xenOffset(scale, parseNumeral(hVal.n)); // 10 is somewhat arbitrary - frequency = parseFloat(frequency.toPrecision(10)); - hap.value = isObject ? { ...otherValues, freq: frequency } : frequency; + freq = trimFreq(freq); + hap.value = isObject ? { ...otherValues, freq } : { freq }; return isEdo(scaleNameOrRatios) ? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] }) : hap; @@ -128,6 +130,18 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { * @param {number} amt * @param {number} edoSize (optional) * @returns {Pattern} + * + * @example + * "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>") + * @example + * // if you specify the edoSize, it changes the amount of transposition: + * "0 8 18".xen("31edo").ftrans("8:12") + * @example + * // it can also work with frequency values directly + * "200 300 400".ftrans("<0 7:31 7>").freq() */ /* f = frequency (Hz) @@ -159,6 +173,7 @@ export const { ftrans, fTrans, ftranspose, fTranspose } = register( edoSize = 12; } freq = freq * Math.pow(2, numSteps / edoSize); + freq = trimFreq(freq); hap.value = isObject ? { ...otherValues, freq } : freq; return hap.setContext({ ...hap.context, edoSize }); }); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 7d7b12e1..fef97dea 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4950,6 +4950,74 @@ exports[`runs examples > example "fscope" example index 0 1`] = ` ] `; +exports[`runs examples > example "ftranspose" example index 0 1`] = ` +[ + "[ 0/1 → 1/3 | freq:329.6275569 ]", + "[ 1/3 → 2/3 | freq:349.2282315 ]", + "[ 2/3 → 1/1 | freq:369.9944227 ]", + "[ 1/1 → 4/3 | freq:329.6275569 ]", + "[ 4/3 → 5/3 | freq:349.2282315 ]", + "[ 5/3 → 2/1 | freq:369.9944227 ]", + "[ 2/1 → 7/3 | freq:329.6275569 ]", + "[ 7/3 → 8/3 | freq:349.2282315 ]", + "[ 8/3 → 3/1 | freq:369.9944227 ]", + "[ 3/1 → 10/3 | freq:329.6275569 ]", + "[ 10/3 → 11/3 | freq:349.2282315 ]", + "[ 11/3 → 4/1 | freq:369.9944227 ]", +] +`; + +exports[`runs examples > example "ftranspose" example index 1 1`] = ` +[ + "[ 0/1 → 1/3 | freq:263.0921203 ]", + "[ 1/3 → 2/3 | freq:314.6248353 ]", + "[ 2/3 → 1/1 | freq:393.4589706 ]", + "[ 1/1 → 4/3 | freq:183.965981 ]", + "[ 4/3 → 5/3 | freq:220 ]", + "[ 5/3 → 2/1 | freq:275.1244143 ]", + "[ 2/1 → 7/3 | freq:263.0921203 ]", + "[ 7/3 → 8/3 | freq:314.6248353 ]", + "[ 8/3 → 3/1 | freq:393.4589706 ]", + "[ 3/1 → 10/3 | freq:183.965981 ]", + "[ 10/3 → 11/3 | freq:220 ]", + "[ 11/3 → 4/1 | freq:275.1244143 ]", +] +`; + +exports[`runs examples > example "ftranspose" example index 2 1`] = ` +[ + "[ 0/1 → 1/3 | freq:349.2282314 ]", + "[ 1/3 → 2/3 | freq:417.6327085 ]", + "[ 2/3 → 1/1 | freq:522.2770651 ]", + "[ 1/1 → 4/3 | freq:349.2282314 ]", + "[ 4/3 → 5/3 | freq:417.6327085 ]", + "[ 5/3 → 2/1 | freq:522.2770651 ]", + "[ 2/1 → 7/3 | freq:349.2282314 ]", + "[ 7/3 → 8/3 | freq:417.6327085 ]", + "[ 8/3 → 3/1 | freq:522.2770651 ]", + "[ 3/1 → 10/3 | freq:349.2282314 ]", + "[ 10/3 → 11/3 | freq:417.6327085 ]", + "[ 11/3 → 4/1 | freq:522.2770651 ]", +] +`; + +exports[`runs examples > example "ftranspose" example index 3 1`] = ` +[ + "[ 0/1 → 1/3 | freq:200 ]", + "[ 1/3 → 2/3 | freq:300 ]", + "[ 2/3 → 1/1 | freq:400 ]", + "[ 1/1 → 4/3 | freq:233.8861531 ]", + "[ 4/3 → 5/3 | freq:350.8292297 ]", + "[ 5/3 → 2/1 | freq:467.7723062 ]", + "[ 2/1 → 7/3 | freq:299.6614154 ]", + "[ 7/3 → 8/3 | freq:449.4921231 ]", + "[ 8/3 → 3/1 | freq:599.3228308 ]", + "[ 3/1 → 10/3 | freq:200 ]", + "[ 10/3 → 11/3 | freq:300 ]", + "[ 11/3 → 4/1 | freq:400 ]", +] +`; + exports[`runs examples > example "ftype" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:f s:sawtooth lpenv:4 cutoff:500 ftype:0 resonance:1 ]", From 2e041e8647c254329d0c6380d6f6a51c9de3dbea Mon Sep 17 00:00:00 2001 From: tyow Date: Sun, 1 Feb 2026 13:53:50 -0500 Subject: [PATCH 16/29] added xen example --- packages/xen/xen.mjs | 2 ++ test/__snapshots__/examples.test.mjs.snap | 37 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index bd77f75a..34d219c0 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -92,6 +92,8 @@ const trimFreq = freq => parseFloat(freq.toPrecision(10)); * "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>") */ // TODO feat: change root frequency diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index fef97dea..77d95003 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -14349,6 +14349,43 @@ exports[`runs examples > example "xen" example index 2 1`] = ` ] `; +exports[`runs examples > example "xen" example index 3 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 "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", From 8ff51d44682705cf610d33d9c2bbdc36ce2f4bcb Mon Sep 17 00:00:00 2001 From: tyow Date: Sun, 1 Feb 2026 18:16:19 -0500 Subject: [PATCH 17/29] small fixes --- packages/xen/xen.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 34d219c0..4ae79fec 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -59,7 +59,7 @@ function xenOffset(xenScale, offset, index = 0) { return xenScale[i] * Math.pow(2, oct); } -const trimFreq = freq => parseFloat(freq.toPrecision(10)); +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 @@ -123,12 +123,12 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { /** * Frequency transpose. Assumes pattern either has `freq` set, or has values that can be interpreted as frequencies * amt has optional `edoSize` param, defaults to 12. - * If haps have edoSize param set, such as from the output of `xen("edo31")`, + * If haps have edoSize param set, such as from the output of `xen("31edo")`, * `ftrans` will fallback to that instead of 12 as the default. * * Transposes the frequency by `amt` edoSteps * @name ftranspose - * @synonyms ftrans, fTrans ftranspose, fTranspose + * @synonyms ftrans, fTrans, ftranspose, fTranspose * @param {number} amt * @param {number} edoSize (optional) * @returns {Pattern} @@ -185,7 +185,7 @@ export const { ftrans, fTrans, ftranspose, fTranspose } = register( ); // not sure there's a point to having this and the above, seems like a proto version of the above. -export const tuning = register('tuning', function (ratios, pat) { +const tuning = register('tuning', function (ratios, pat) { return pat.withHap((hap) => { const frequency = xenOffset(ratios, parseNumeral(hap.value)); return hap.withValue(() => frequency); From 00a3c1acc05655757395d8a41f7789f837ad4044 Mon Sep 17 00:00:00 2001 From: tyow Date: Mon, 2 Feb 2026 13:40:48 -0500 Subject: [PATCH 18/29] clarified an example --- packages/xen/xen.mjs | 4 ++-- test/__snapshots__/examples.test.mjs.snap | 28 +++++++++++++---------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 4ae79fec..5261c3a3 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -139,8 +139,8 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { * @example * "0 8 18".xen("31edo").ftrans("<8 -8>") * @example - * // if you specify the edoSize, it changes the amount of transposition: - * "0 8 18".xen("31edo").ftrans("8:12") + * // to transpose by steps of an edo, use "step:edo" : + * "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() diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 77d95003..0bf39ad3 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4986,18 +4986,22 @@ exports[`runs examples > example "ftranspose" example index 1 1`] = ` exports[`runs examples > example "ftranspose" example index 2 1`] = ` [ - "[ 0/1 → 1/3 | freq:349.2282314 ]", - "[ 1/3 → 2/3 | freq:417.6327085 ]", - "[ 2/3 → 1/1 | freq:522.2770651 ]", - "[ 1/1 → 4/3 | freq:349.2282314 ]", - "[ 4/3 → 5/3 | freq:417.6327085 ]", - "[ 5/3 → 2/1 | freq:522.2770651 ]", - "[ 2/1 → 7/3 | freq:349.2282314 ]", - "[ 7/3 → 8/3 | freq:417.6327085 ]", - "[ 8/3 → 3/1 | freq:522.2770651 ]", - "[ 3/1 → 10/3 | freq:349.2282314 ]", - "[ 10/3 → 11/3 | freq:417.6327085 ]", - "[ 11/3 → 4/1 | freq:522.2770651 ]", + "[ 0/1 → 1/4 | freq:220 ]", + "[ 1/4 → 1/2 | freq:257.2747684 ]", + "[ 1/2 → 3/4 | freq:263.0921203 ]", + "[ 3/4 → 1/1 | freq:329.0139341 ]", + "[ 1/1 → 5/4 | freq:224.9745158 ]", + "[ 5/4 → 3/2 | freq:263.0921203 ]", + "[ 3/2 → 7/4 | freq:269.0410108 ]", + "[ 7/4 → 2/1 | freq:336.4534115 ]", + "[ 2/1 → 9/4 | freq:233.0818808 ]", + "[ 9/4 → 5/2 | freq:272.5731222 ]", + "[ 5/2 → 11/4 | freq:278.7363919 ]", + "[ 11/4 → 3/1 | freq:348.5781207 ]", + "[ 3/1 → 13/4 | freq:220 ]", + "[ 13/4 → 7/2 | freq:257.2747684 ]", + "[ 7/2 → 15/4 | freq:263.0921203 ]", + "[ 15/4 → 4/1 | freq:329.0139341 ]", ] `; From a6dc6bb83114c0a246ba22c9d460c97c4cbba6f5 Mon Sep 17 00:00:00 2001 From: tyow Date: Mon, 2 Feb 2026 16:07:40 -0500 Subject: [PATCH 19/29] withBase --- packages/xen/xen.mjs | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 5261c3a3..1e9ea825 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -22,7 +22,7 @@ const presets = { // Given a base frequency such as 220 and an edo scale, returns // an array of frequencies representing the given edo scale in that base -function withBase(freq, scale) { +function _withBase(freq, scale) { return scale.map((r) => r * freq); } @@ -46,7 +46,7 @@ function getXenScale(scale, indices) { throw new Error('unknown scale name: "' + scale + '"'); } } - scale = withBase(defaultBase, scale); + scale = _withBase(defaultBase, scale); if (!indices) { return scale; } @@ -120,6 +120,44 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { }); }); +/** + * Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen` + * Because `xen` defaults to `220Hz`, so will `withBase`. + * but you can specify a different original base with the standard optional array syntax `:` + * @name withBase + * @param {number} base + * @param {number} (optional) originalBase + * + * @example + * "[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() + * + * @returns Pattern + */ +export const withBase = register('withBase', (b, pat) => { + let base; + let originalBase = 220; + if (Array.isArray(b)) { + base = b[0]; + originalBase = b[1]; + } else { + base = b; + } + return pat.withHaps((haps) => { + haps = haps.map((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + let freq = isObject ? hVal.freq : hVal; + freq = (freq * base) / originalBase; + hap.value = isObject ? { ...hap.value, freq } : { freq }; + return hap; + }); + return removeUndefineds(haps); + }); +}); + /** * Frequency transpose. Assumes pattern either has `freq` set, or has values that can be interpreted as frequencies * amt has optional `edoSize` param, defaults to 12. From 81d02cf5aed4e34bb803229c135654cc9fd6bcae Mon Sep 17 00:00:00 2001 From: tyow Date: Mon, 2 Feb 2026 16:08:01 -0500 Subject: [PATCH 20/29] format --- packages/xen/xen.mjs | 6 +- test/__snapshots__/examples.test.mjs.snap | 70 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 1e9ea825..4e6f75d0 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -127,13 +127,13 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { * @name withBase * @param {number} base * @param {number} (optional) originalBase - * + * * @example * "[0 1 2 3] [3 4] [4 3 2 1]".xen("hexany23").withBase("<220 [300 200]>") - * @example + * @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() - * + * * @returns Pattern */ export const withBase = register('withBase', (b, pat) => { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 0bf39ad3..be58df2c 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -14151,6 +14151,76 @@ exports[`runs examples > example "when" example index 0 1`] = ` exports[`runs examples > example "whenKey" example index 0 1`] = `[]`; +exports[`runs examples > example "withBase" example index 0 1`] = ` +[ + "[ 0/1 → 1/12 | freq:220 ]", + "[ 1/12 → 1/6 | freq:293.3333333 ]", + "[ 1/6 → 1/4 | freq:302.5 ]", + "[ 1/4 → 1/3 | freq:320 ]", + "[ 1/3 → 1/2 | freq:320 ]", + "[ 1/2 → 2/3 | freq:330 ]", + "[ 2/3 → 3/4 | freq:330 ]", + "[ 3/4 → 5/6 | freq:320 ]", + "[ 5/6 → 11/12 | freq:302.5 ]", + "[ 11/12 → 1/1 | freq:293.3333333 ]", + "[ 1/1 → 13/12 | freq:300 ]", + "[ 13/12 → 7/6 | freq:399.99999995454544 ]", + "[ 7/6 → 5/4 | freq:412.5 ]", + "[ 5/4 → 4/3 | freq:436.3636363636364 ]", + "[ 4/3 → 3/2 | freq:436.3636363636364 ]", + "[ 3/2 → 5/3 | freq:300 ]", + "[ 5/3 → 7/4 | freq:300 ]", + "[ 7/4 → 11/6 | freq:290.90909090909093 ]", + "[ 11/6 → 23/12 | freq:275 ]", + "[ 23/12 → 2/1 | freq:266.66666663636363 ]", + "[ 2/1 → 25/12 | freq:220 ]", + "[ 25/12 → 13/6 | freq:293.3333333 ]", + "[ 13/6 → 9/4 | freq:302.5 ]", + "[ 9/4 → 7/3 | freq:320 ]", + "[ 7/3 → 5/2 | freq:320 ]", + "[ 5/2 → 8/3 | freq:330 ]", + "[ 8/3 → 11/4 | freq:330 ]", + "[ 11/4 → 17/6 | freq:320 ]", + "[ 17/6 → 35/12 | freq:302.5 ]", + "[ 35/12 → 3/1 | freq:293.3333333 ]", + "[ 3/1 → 37/12 | freq:300 ]", + "[ 37/12 → 19/6 | freq:399.99999995454544 ]", + "[ 19/6 → 13/4 | freq:412.5 ]", + "[ 13/4 → 10/3 | freq:436.3636363636364 ]", + "[ 10/3 → 7/2 | freq:436.3636363636364 ]", + "[ 7/2 → 11/3 | freq:300 ]", + "[ 11/3 → 15/4 | freq:300 ]", + "[ 15/4 → 23/6 | freq:290.90909090909093 ]", + "[ 23/6 → 47/12 | freq:275 ]", + "[ 47/12 → 4/1 | freq:266.66666663636363 ]", +] +`; + +exports[`runs examples > example "withBase" example index 1 1`] = ` +[ + "[ 0/1 → 1/5 | freq:220 ]", + "[ 1/5 → 2/5 | freq:234.66666666666666 ]", + "[ 2/5 → 3/5 | freq:247.5 ]", + "[ 3/5 → 4/5 | freq:264 ]", + "[ 4/5 → 1/1 | freq:275 ]", + "[ 1/1 → 6/5 | freq:220 ]", + "[ 6/5 → 7/5 | freq:234.66666666666666 ]", + "[ 7/5 → 8/5 | freq:247.5 ]", + "[ 8/5 → 9/5 | freq:264 ]", + "[ 9/5 → 2/1 | freq:275 ]", + "[ 2/1 → 11/5 | freq:220 ]", + "[ 11/5 → 12/5 | freq:234.66666666666666 ]", + "[ 12/5 → 13/5 | freq:247.5 ]", + "[ 13/5 → 14/5 | freq:264 ]", + "[ 14/5 → 3/1 | freq:275 ]", + "[ 3/1 → 16/5 | freq:220 ]", + "[ 16/5 → 17/5 | freq:234.66666666666666 ]", + "[ 17/5 → 18/5 | freq:247.5 ]", + "[ 18/5 → 19/5 | freq:264 ]", + "[ 19/5 → 4/1 | freq:275 ]", +] +`; + exports[`runs examples > example "withValue" example index 0 1`] = ` [ "[ 0/1 → 1/3 | 10 ]", From 897a750f706f23b4bf26e7241e392c41665c7c87 Mon Sep 17 00:00:00 2001 From: tyow Date: Mon, 2 Feb 2026 16:10:47 -0500 Subject: [PATCH 21/29] tagging --- packages/xen/xen.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 4e6f75d0..206776f2 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -123,10 +123,11 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { /** * Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen` * Because `xen` defaults to `220Hz`, so will `withBase`. - * but you can specify a different original base with the standard optional array syntax `:` + * but you can specify a different original base with the standard optional array syntax '`:`' * @name withBase * @param {number} base * @param {number} (optional) originalBase + * @tags tonal * * @example * "[0 1 2 3] [3 4] [4 3 2 1]".xen("hexany23").withBase("<220 [300 200]>") @@ -167,6 +168,7 @@ export const withBase = register('withBase', (b, pat) => { * Transposes the frequency by `amt` edoSteps * @name ftranspose * @synonyms ftrans, fTrans, ftranspose, fTranspose + * @tags tonal * @param {number} amt * @param {number} edoSize (optional) * @returns {Pattern} From c74dd95bd0c074b1a79b36219644b2c7a8e22559 Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 23 Jan 2026 13:30:36 -0500 Subject: [PATCH 22/29] prelim work on uzu/strudel#1944 --- packages/xen/xen.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 206776f2..55ee4197 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -53,6 +53,7 @@ function getXenScale(scale, indices) { return scale.filter((_, i) => indices.includes(i)); } + function xenOffset(xenScale, offset, index = 0) { const i = _mod(index + offset, xenScale.length); const oct = Math.floor(offset / xenScale.length); From e6b824ebff0170dd82f1652f194ad011c7fc05b2 Mon Sep 17 00:00:00 2001 From: tyow Date: Sat, 24 Jan 2026 14:18:51 -0500 Subject: [PATCH 23/29] added jsdoc documentation for tune re uzu/strudel#1944 --- website/src/pages/learn/xen.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index a661c7b0..001f5c0f 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -13,6 +13,7 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. ### tune(scale) +{/* TODO (maybe): combine jsdoc things in tune.mjs with here */} {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} From eb3818eebb891801ed49f04374d0869626c334b8 Mon Sep 17 00:00:00 2001 From: tyow Date: Sat, 24 Jan 2026 14:30:15 -0500 Subject: [PATCH 24/29] formatting uzu/strudel#1944 --- packages/xen/xen.mjs | 1 - website/src/pages/learn/xen.mdx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 55ee4197..206776f2 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -53,7 +53,6 @@ function getXenScale(scale, indices) { return scale.filter((_, i) => indices.includes(i)); } - function xenOffset(xenScale, offset, index = 0) { const i = _mod(index + offset, xenScale.length); const oct = Math.floor(offset / xenScale.length); diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 001f5c0f..0a0acafe 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -13,6 +13,7 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. ### tune(scale) + {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} From 5da3ae8df418ffdd9616caebac7078b42f54591b Mon Sep 17 00:00:00 2001 From: tyow Date: Fri, 23 Jan 2026 13:30:36 -0500 Subject: [PATCH 25/29] prelim work on uzu/strudel#1944 --- packages/xen/xen.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 206776f2..55ee4197 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -53,6 +53,7 @@ function getXenScale(scale, indices) { return scale.filter((_, i) => indices.includes(i)); } + function xenOffset(xenScale, offset, index = 0) { const i = _mod(index + offset, xenScale.length); const oct = Math.floor(offset / xenScale.length); From 67bce82a62a5040031e5a29cff8b6d24732d5ee4 Mon Sep 17 00:00:00 2001 From: tyow Date: Sat, 24 Jan 2026 14:18:51 -0500 Subject: [PATCH 26/29] added jsdoc documentation for tune re uzu/strudel#1944 --- website/src/pages/learn/xen.mdx | 3 --- 1 file changed, 3 deletions(-) diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 0a0acafe..616e5ead 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -13,9 +13,6 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. ### tune(scale) - -{/* TODO (maybe): combine jsdoc things in tune.mjs with here */} - {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} From 32754ef97aa02518f363f518723f06817dd85cf2 Mon Sep 17 00:00:00 2001 From: tyow Date: Sat, 24 Jan 2026 14:30:15 -0500 Subject: [PATCH 27/29] formatting uzu/strudel#1944 --- packages/xen/xen.mjs | 1 - website/src/pages/learn/xen.mdx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 55ee4197..206776f2 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -53,7 +53,6 @@ function getXenScale(scale, indices) { return scale.filter((_, i) => indices.includes(i)); } - function xenOffset(xenScale, offset, index = 0) { const i = _mod(index + offset, xenScale.length); const oct = Math.floor(offset / xenScale.length); diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 616e5ead..a661c7b0 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -13,6 +13,7 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. ### tune(scale) + {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} From cbf472fe0a20c1bc41e44058df2a01d052182eab Mon Sep 17 00:00:00 2001 From: tyow Date: Wed, 4 Feb 2026 20:28:19 -0500 Subject: [PATCH 28/29] creation of `i` control for `tune` and `xen` functions --- packages/core/controls.mjs | 12 ++ packages/xen/tune.mjs | 17 +- packages/xen/xen.mjs | 34 ++-- test/__snapshots__/examples.test.mjs.snap | 189 +++++++++++++--------- website/src/pages/learn/xen.mdx | 21 ++- 5 files changed, 163 insertions(+), 110 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 291cfa9e..1a3f326a 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 7584b7f2..a70d8868 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 206776f2..762db61b 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 be58df2c..831e4876 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 a661c7b0..144a40f1 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: Date: Wed, 4 Feb 2026 20:29:44 -0500 Subject: [PATCH 29/29] format --- packages/xen/tune.mjs | 4 ++-- packages/xen/xen.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index a70d8868..a070515b 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -32,7 +32,7 @@ import { register } from '@strudel/core'; * 469.00678383895, * 523.2511306012 * ]).mul(220).freq(); - * + * * @tags tonal */ @@ -47,7 +47,7 @@ export const tune = register('tune', (scale, pat) => { tune.tonicize(1); return pat.withHap((hap) => { 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()`) + 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)} diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 762db61b..828cab6e 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -102,7 +102,7 @@ export const xen = register('xen', function (scaleNameOrRatios, pat) { 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()`) + 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);