jevstrudel.git / packages / edo / edoscale.mjs
1/*
2edoscale.mjs - EdoScale defines equal division of the octave (EDO) scale in Ls notation
3             - Port of pitfalls/lib/Scale.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Scale.lua>
4Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edoscale.mjs>
5This 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/>.
6*/
7const M = 2;
8const L = 1;
9const S = 0;
10const LABELS = ['s', 'L', 'M'];
11
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}