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}