xen.mjsannotatedxen.mjssource232 lines · 8.2 KB · raw

xen.mjs - <short description TODO> Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/xen/xen.mjs This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.

7import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core';
8import Tune from './tunejs.js';

returns a list of frequency ratios for given edo scale

11export function edo(name) {
12  if (!/^[1-9]+[0-9]*edo$/.test(name)) {
13    throw new Error('not an edo scale: "' + name + '"');
14  }
15  const [_, divisions] = name.match(/^([1-9]+[0-9]*)edo$/);
16  return Array.from({ length: divisions }, (_, i) => Math.pow(2, i / divisions));
17}
19const presets = {
20  '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],
21};

Given a base frequency such as 220 and an edo scale, returns an array of frequencies representing the given edo scale in that base

25function _withBase(freq, scale) {
26  return scale.map((r) => r * freq);
27}
29const defaultBase = 220;
30
31const 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

35function getXenScale(scale, indices) {
36  let tune = new Tune();
37  if (typeof scale === 'string') {
38    if (isEdo(scale)) {
39      scale = edo(scale);
40    } else if (presets[scale]) {
41      scale = presets[scale];
42    } else if (tune.isValidScale(scale)) {
43      tune.loadScale(scale);
44      scale = tune.scale;
45    } else {
46      throw new Error('unknown scale name: "' + scale + '"');
47    }
48  }
49  scale = _withBase(defaultBase, scale);
50  if (!indices) {
51    return scale;
52  }
53  return scale.filter((_, i) => indices.includes(i));
54}
56function xenOffset(xenScale, offset, index = 0) {
57  const i = _mod(index + offset, xenScale.length);
58  const oct = Math.floor(offset / xenScale.length);
59  return xenScale[i] * Math.pow(2, oct);
60}
61
62const 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.

@name xen @returns Pattern @memberof Pattern @param {(string | number[] )} scaleNameOrRatios @tags tonal @example // A minor triad in 31edo: i("0 8 18").xen("31edo").piano() @example // You can also use xen with frequency ratios. // This is equivalent to the above: i("0 1 2").xen([ Math.pow(2, 0/31), Math.pow(2, 8/31), Math.pow(2, 18/31), ]).piano() @example // xen also supports all scale names that // tune does: i("0 1 2 3 4 5").xen("hexany15") // equiv to: // "0 1 2 3 4 5".tune("hexany15").mul("220").freq() @example i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>")

99export const xen = register('xen', function (scaleNameOrRatios, pat) {
100  return pat.withHaps((haps) => {
101    haps = haps.map((hap) => {
102      let hVal = hap.value;
103      const isObject = typeof hVal === 'object';
104      if (!isObject) {
105        throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`);
106      }
107      const { i, ...otherValues } = hVal;
108      const scale = getXenScale(scaleNameOrRatios);
109      let freq = xenOffset(scale, parseNumeral(hVal.i));
110      // 10 is somewhat arbitrary
111      freq = trimFreq(freq);
112      hap.value = { ...otherValues, freq };
113      return isEdo(scaleNameOrRatios)
114        ? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] })
115        : hap;
116    });
117    return removeUndefineds(haps);
118  });
119});

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 @tags tonal

@example 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()

@returns Pattern

138export const withBase = register('withBase', (b, pat) => {
139  let base;
140  let originalBase = 220;
141  if (Array.isArray(b)) {
142    base = b[0];
143    originalBase = b[1];
144  } else {
145    base = b;
146  }
147  return pat.withHaps((haps) => {
148    haps = haps.map((hap) => {
149      let hVal = hap.value;
150      const isObject = typeof hVal === 'object';
151      let freq = isObject ? hVal.freq : hVal;
152      if (!freq) return hap;
153      freq = (freq * base) / originalBase;
154      hap.value = isObject ? { ...hap.value, freq } : { freq };
155      return hap;
156    });
157    return removeUndefineds(haps);
158  });
159});

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("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 @tags tonal @param {number} amt @param {number} edoSize (optional) @returns {Pattern}

@example i("0 1 2").xen("12edo").ftrans("7") // n("0 1 2").scale("A:chromatic").trans("7") @example i("0 8 18").xen("31edo").ftrans("<8 -8>") @example // to transpose by steps of an edo, use "step:edo" : i("0 7 8 18").xen("31edo").ftrans("<0 1:31 1:12>") @example // it can also work with frequency values directly freq("200 300 400").ftrans("<0 7:31 7>")

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

194export const { ftrans, fTrans, ftranspose, fTranspose } = register(
195  ['ftrans', 'fTrans', 'ftranspose', 'fTranspose'],
196  (amt, pat) => {
197    let edoSize;
198    let numSteps;
199    if (Array.isArray(amt)) {
200      edoSize = amt[1];
201      numSteps = amt[0];
202    } else {
203      numSteps = amt;
204    }
205    return pat.withHaps((haps) => {
206      haps = haps.map((hap) => {
207        let hVal = hap.value;
208        const isObject = typeof hVal === 'object';
209        hVal = isObject ? hVal : { freq: hVal };
210        let { freq, ...otherValues } = hVal;
211        if (edoSize == undefined && hap.context.edoSize != undefined) {
212          edoSize = hap.context.edoSize;
213        } else if (edoSize == undefined) {
214          edoSize = 12;
215        }
216        freq = freq * Math.pow(2, numSteps / edoSize);
217        freq = trimFreq(freq);
218        hap.value = isObject ? { ...otherValues, freq } : freq;
219        return hap.setContext({ ...hap.context, edoSize });
220      });
221      return removeUndefineds(haps);
222    });
223  },
224);

not sure there's a point to having this and the above, seems like a proto version of the above.

227const tuning = register('tuning', function (ratios, pat) {
228  return pat.withHap((hap) => {
229    const frequency = xenOffset(ratios, parseNumeral(hap.value));
230    return hap.withValue(() => frequency);
231  });
232});