edo.mjsannotatededo.mjssource114 lines · 4.6 KB · raw
1/*
2edo.mjs - Equal division of the octave (EDO) scale functions for strudel
3Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edo.mjs>
4This 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/>.
5*/
6
7import { register, pure, noteToMidi, isNote, tokenizeNote } from '@strudel/core';
8import { EdoScale } from './edoscale.mjs';
9import { Intervals } from './intervals.mjs';
10import { Pitches } from './pitches.mjs';
11
12const pitchesCache = new Map();
13
14/**
15 * Turns numbers into notes in the given EDO scale (zero indexed).
16 *
17 * An EDO scale definition looks like this:
18 *
19 * e.g. C:LLsLLLs:2:1 <- this is the C major scale, 12 EDO
20 *
21 * e.g. C:LLsLLL:3:1 <- this is the Gorgo 6 note scale, 16 EDO
22 *
23 * An EDO scale, e.g. C:LLsLLLs:2:1, consists of a root note (e.g. C)
24 * followed by semicolon (':')
25 * and then a [Large/small step notation sequence](https://en.xen.wiki/w/MOS_scale)
26 * (e.g. LLsLLLs)
27 * followed by semicolon, then the large step size (e.g. 2)
28 * followed by semicolon, then the small step size (e.g. 1).
29 *
30 * The number of divisions of the octave is calculated as the sum
31 * of the steps in the EDO scale definition.
32 *
33 * e.g. C:LLsLLLs:2:1 is 2+2+1+2+2+2+1 = 12 EDO, 7 note scale
34 *
35 * e.g. C:LLsLLL:3:1 is 3+3+1+3+3+3 = 16 EDO, 6 note scale
36 *
37 * The root note defaults to octave 3, if no octave number is given.
38 *
39 * @name edoScale
40 * @param {string} scale Definition of EDO scale.
41 * @returns Pattern
42 * @example
43 * n("0 2 4 6 4 2").edoScale("C:LLsLLLs:2:1")
44 * @example
45 * n("[0,7] 4 [2,7] 4")
46 * .edoScale("G2:<LLsLLL LLLLsL>:3:1")
47 * .s("piano")._pitchwheel()
48 * @example
49 * n(rand.range(0,5).segment(6))
50 * .edoScale("<G2 C3>:LLsLL:3:1")
51 * .s("piano")._pitchwheel()
52 */
53export const edoScale = register(
54  'edoScale',
55  function (scaleDefinition, pat) {
56    // console.log(scaleDefinition);
57
58    // if (Array.isArray(scale)) {
59    const key = scaleDefinition.flat().join(':');
60    // }
61    // console.log(scaleDefinition);
62    let pitches;
63    if (pitchesCache.has(key)) {
64      pitches = pitchesCache.get(key);
65    } else {
66      // console.log({ key });
67      const [base_note, sequence, large, small] = scaleDefinition;
68      const root_octave = tokenizeNote(base_note)[2] || 3;
69      // console.log({ root_octave });
70      const scale = new EdoScale(large, small, sequence);
71      const intervals = new Intervals(scale);
72      // console.log({ intervals });
73      pitches = new Pitches(scale, intervals, 440, noteToMidi(base_note), root_octave);
74      pitchesCache.set(key, pitches);
75      // console.log({ base_note: noteToMidi(base_note) });
76      // console.log({ sequence });
77      // console.log({ edivisions: scale.edivisions });
78      // console.log({ intervals });
79      // console.log({ pat });
80      // console.log({ pitches: pitches });
81    }
82    return pat
83      .fmap((value) => {
84        const isObject = typeof value === 'object';
85        const n = isObject ? value.n : value;
86        if (isObject) {
87          delete value.n; // remove n so it won't cause trouble
88        }
89        if (isNote(n)) {
90          // legacy..
91          return pure(n);
92        }
93        const deg = (typeof n === 'string' ? parseInt(n, 10) : Number.isInteger(n) ? n : Math.round(n)) + 1;
94
95        const [oct, degree] = pitches.octdeg(deg);
96        const freq = pitches.octdegfreq(oct, degree);
97        const note = pitches.octdegmidi(oct, degree);
98        const edo = pitches.scale.edivisions;
99        const root = pitches.base_freq;
100        const degreeIndexes = pitches.scale.divisions;
101        const intLabels = pitches.intervals.intLabels;
102        // const color = 'red';
103        value = pure(isObject ? { ...value, degree, degreeIndexes, intLabels, root, freq, edo } : note);
104        // value = pure(isObject ? { ...value, key } : note);
105        // value = pure(isObject ? { ...value, edo } : note);
106        // console.log({ value });
107        return value;
108      })
109      .outerJoin()
110      .withHap((hap) => hap.setContext({ ...hap.context, scaleDefinition }));
111  },
112  true,
113  true, // preserve tactus
114);