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);