jevstrudel.git / packages / edo / intervals.mjs

intervals.mjs - defines Intervals for equal division of the octave (EDO) scale

  • Port of pitfalls/lib/Intervals.lua - see https://github.com/robmckinnon/pitfalls/blob/main/lib/Intervals.lua Copyright (C) 2025 Rob McKinnon and Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/intervals.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 ratiointervals from './ratios.mjs';
9function ratio(division, edivisions) {
10  return division === 0 ? 1 : Math.pow(2, division / edivisions);
11}
12
13export class Intervals {
14  constructor(scale) {
15    this.scale = scale;
16    this.intLabels = [];
17    this.intNoms = [];
18    this.intRatios = [];
19    this.uniqLabels = [];
20    this.intErrors = [];
21    this.ratios = [];
22
23    const BLANK = '';
24    let division = 0;
25    const labToErr = {};
26    const labToInd = {};
27
28    this.ratios[0] = 1;
29
30    for (let i = 0; i < scale.length; i++) {
31      division += scale.stepValue(i);
32      this.ratios[i + 1] = ratio(division, scale.edivisions);
33
34      if (i < scale.length) {
35        const nearest = ratiointervals.nearestInterval(this.ratios[i + 1]);
36        const closeness = nearest[0];
37        const ratio = nearest[1];
38        const intLabel = ratiointervals.key(ratio);
39        this.intLabels[i + 1] = intLabel;
40        this.intErrors[i + 1] = closeness;
41        this.intNoms[i + 1] = ratio ? ratiointervals.nom(ratio) : 0;
42        this.intRatios[i + 1] = ratio ? `${ratiointervals.nom(ratio)}/${ratiointervals.denom(ratio)}` : '';
43        this.uniqLabels[i + 1] = BLANK;
44
45        if (intLabel && intLabel !== 'P1' && intLabel !== 'P8') {
46          if (!labToErr[intLabel]) {
47            this.uniqLabels[i + 1] = intLabel;
48            labToInd[intLabel] = i + 1;
49            labToErr[intLabel] = closeness;
50          } else if (closeness < labToErr[intLabel]) {
51            this.uniqLabels[labToInd[intLabel]] = BLANK;
52            this.uniqLabels[i + 1] = intLabel;
53            labToInd[intLabel] = i + 1;
54            labToErr[intLabel] = closeness;
55          }
56        }
57      }
58    }
59  }
60
61  ratio(i) {
62    return this.ratios[i];
63  }
64
65  intervalLabel(i) {
66    return this.intLabels[i];
67  }
68
69  intervalNominator(i) {
70    return this.intNoms[i];
71  }
72
73  intervalRatio(i) {
74    return this.intRatios[i];
75  }
76
77  uniqIntervalLabel(i) {
78    return this.uniqLabels[i];
79  }
80
81  intervalError(i) {
82    return this.intErrors[i];
83  }
84
85  nearestDegreeTo(r, threshold) {
86    let min = 1;
87    let degree = null;
88
89    for (const [i, v] of Object.entries(this.ratios)) {
90      const diff = Math.abs((r - v) / r);
91      if (diff < min) {
92        min = diff;
93        degree = parseInt(i, 10);
94      }
95    }
96
97    if (threshold == null) {
98      return degree;
99    } else {
100      return min < threshold ? degree : 1;
101    }
102  }
103}