edoscale.mjs - EdoScale defines equal division of the octave (EDO) scale in Ls notation
- Port of pitfalls/lib/Scale.lua - see https://github.com/robmckinnon/pitfalls/blob/main/lib/Scale.lua Copyright (C) 2025 Rob McKinnon and Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edoscale.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/.
12export class EdoScale { 13 constructor(large, small, sequence, medium) { 14 this.stepbackup = [L, L, S, L, L, L, S, L, L, L, S, L, L, L, S, L]; 15 this.large = large; 16 this.medium = medium || large; 17 this.small = small; 18 this.divisions = []; 19 this.edivisions = null; 20 this.sequence = null; 21 this.tonic = 1; 22 this.mode = 1; 23 this.max_steps = 12; 24 this.min_steps = 3; 25 this.setSequence(sequence); 26 } 27 28 hasMedium() { 29 return this.step.some((_, i) => this.step[this.offset(i)] === M); 30 } 31 32 stepSize(i) { 33 return LABELS[this.step[this.offset(i)]]; 34 } 35 36 sequence() { 37 return this.step.map((_, i) => this.stepSize(i)).join(''); 38 } 39 40 stepValue(i) { 41 const step = this.step[this.offset(i)]; 42 return step === L ? this.large : step === M ? this.medium : this.small; 43 } 44 45 offset(i) { 46 if (this.mode === 1) { 47 return i; 48 } else { 49 const offset = (this.mode - 1 + i) % this.length; 50 return offset === 0 ? this.length : offset; 51 } 52 } 53 54 static setMaxSteps(max) { 55 this.max_steps = max; 56 } 57 58 static setMinSteps(min) { 59 this.min_steps = min; 60 } 61 62 setSequence(sequence) { 63 if (this.sequence !== sequence) { 64 this.sequence = sequence; 65 this.length = sequence.length; 66 this.step = []; 67 for (let i = 0; i < sequence.length; i++) { 68 const char = sequence[i]; 69 this.step[i] = char === 'L' ? L : char === 'M' ? M : S; 70 } 71 this.updateEdo(); 72 } else { 73 return false; 74 } 75 } 76 77 setLarge(l) { 78 if (this.large !== l) { 79 this.large = l; 80 this.updateEdo(); 81 } else { 82 return false; 83 } 84 } 85 86 setMedium(m) { 87 if (this.medium !== m) { 88 this.medium = m; 89 this.updateEdo(); 90 } else { 91 return false; 92 } 93 } 94 95 setSmall(s) { 96 if (this.small !== s) { 97 this.small = s; 98 this.updateEdo(); 99 } else { 100 return false; 101 } 102 } 103 104 setMode(mode) { 105 this.mode = mode; 106 } 107 108 setTonic(tonic) { 109 this.tonic = tonic; 110 } 111 112 changeMode(d) { 113 const orig = this.mode; 114 this.mode = Math.max(1, Math.min(this.mode + d, this.length)); 115 return orig !== this.mode; 116 } 117 118 changeTonic(d) { 119 const orig = this.tonic; 120 this.tonic = Math.max(1, Math.min(this.tonic + d, this.edivisions)); 121 return orig !== this.tonic; 122 } 123 124 updateEdo() { 125 const orig = this.edivisions; 126 this.edivisions = this.step.reduce((sum, _, i) => { 127 this.divisions[i] = sum; 128 return sum + this.stepValue(i); 129 }, 0); 130 // console.log(this.divisions); 131 const changed = orig !== this.edivisions; 132 if (changed) { 133 this.tonic = Math.max(1, Math.min(this.tonic, this.edivisions)); 134 } 135 return changed; 136 } 137 138 changeStep(d, i) { 139 const index = this.offset(i); 140 const orig = this.step[index]; 141 this.step[index] = Math.max(S, Math.min(this.step[index] + d, M)); 142 this.stepbackup[index] = this.step[index]; 143 const changed = orig !== this.step[index]; 144 if (changed) { 145 this.updateEdo(); 146 } 147 return changed; 148 } 149 150 changeLarge(d) { 151 const orig = this.large; 152 this.setLarge(Math.max(this.small + 1, Math.min(this.large + d, this.large + 1))); 153 const changed = this.large !== orig; 154 if (changed) { 155 if (this.large <= this.medium) { 156 this.setMedium(Math.max(this.small + 1, Math.min(this.large - 1, this.large))); 157 } 158 this.updateEdo(); 159 } 160 return changed; 161 } 162 163 changeMedium(d) { 164 const orig = this.medium; 165 this.setMedium(Math.max(this.small + 1, Math.min(this.medium + d, this.large - 1))); 166 const changed = this.medium !== orig; 167 if (changed) { 168 this.updateEdo(); 169 } 170 return changed; 171 } 172 173 changeSmall(d) { 174 const orig = this.small; 175 const value = this.small + d; 176 177 if (this.hasMedium()) { 178 this.setSmall(Math.max(1, Math.min(value, this.medium - 1))); 179 } else { 180 this.setSmall(Math.max(1, Math.min(value, this.large - 1))); 181 } 182 183 const changed = this.small !== orig; 184 if (changed) { 185 if (this.small >= this.medium) { 186 this.setMedium(Math.max(this.small + 1, Math.min(this.large))); 187 } 188 this.updateEdo(); 189 } 190 return changed; 191 } 192 193 changeLength(d) { 194 const orig = this.length; 195 if (d === 1) { 196 this.length = Math.min(this.length + 1, this.max_steps); 197 } else if (d === -1) { 198 this.length = Math.max(this.length - 1, this.min_steps); 199 } 200 201 const changed = this.length !== orig; 202 if (changed) { 203 this.mode = 1; 204 if (d === 1) { 205 this.step[this.length] = this.stepbackup[this.length] || L; 206 } else if (d === -1 && this.length >= this.min_steps) { 207 this.step.pop(); 208 } 209 this.updateEdo(); 210 } 211 return changed; 212 } 213}