jevstrudel.git / packages / edo / pitches.mjs
1/*
2pitches.mjs - defines Pitches for equal division of the octave (EDO) scale
3            - Port of pitfalls/lib/Pitches.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Pitches.lua>
4Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/pitches.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*/
7
8function ratio(division, edivisions) {
9  return division === 0 ? 1 : Math.pow(2, division / edivisions);
10}
11
12// This function gets frequency based on index, edo, octave, and base frequency
13function get_freq(base_freq, edo, index, oct, base_octave) {
14  let f = base_freq * ratio(index - 1, edo);
15  if (oct < base_octave) {
16    f /= Math.pow(2, base_octave - oct);
17  } else if (oct > base_octave) {
18    f *= Math.pow(2, oct - base_octave);
19  }
20  return f;
21}
22
23// Convert MIDI number to Hz using a given tuning multiplier
24function midi_to_hz(n, tuning) {
25  return tuning * Math.pow(2, (n - 69) / 12.0);
26}
27
28const denom = Math.log(2);
29function hz_to_midi(freq, tuning) {
30  return 12.0 * (Math.log(freq / tuning) / denom) + 69;
31}
32
33export class Pitches {
34  constructor(scale, intervals, tuning, midi_start, root_octave) {
35    this.scale = scale;
36    this.intervals = intervals;
37    this.base_freq = midi_to_hz(midi_start, tuning);
38    this.root_octave = root_octave;
39    this.freqs = {};
40    this.midis = {};
41    this.degrees = {};
42    this.octdegfreqs = {};
43    this.octdegmidis = {};
44    let index = 0;
45    let f = null;
46
47    for (let oct = 0; oct <= 8; oct++) {
48      this.octdegfreqs[oct] = {};
49      this.octdegmidis[oct] = {};
50      f = get_freq(this.base_freq, scale.edivisions, scale.tonic, oct, this.root_octave);
51      for (let deg = 0; deg < scale.length; deg++) {
52        index = index + 1;
53        this.freqs[index] = parseFloat((f * intervals.ratio(deg)).toFixed(3));
54        this.midis[index] = parseFloat(hz_to_midi(f * intervals.ratio(deg), tuning).toFixed(4));
55        this.degrees[index] = deg;
56        this.octdegfreqs[oct][deg + 1] = this.freqs[index];
57        this.octdegmidis[oct][deg + 1] = this.midis[index];
58      }
59    }
60    this.base_freq = parseFloat(this.base_freq).toFixed(4);
61  }
62
63  base_freq() {
64    return this.base_freq;
65  }
66
67  degree(index) {
68    return this.degrees[index];
69  }
70
71  freq(index) {
72    return this.freqs[index];
73  }
74
75  octdeg(deg) {
76    const higherOcatve = deg > this.scale.length;
77    const octave = this.root_octave + (higherOcatve ? Math.floor((deg - 1) / this.scale.length) : 0);
78    const degree = higherOcatve ? (deg % this.scale.length === 0 ? this.scale.length : deg % this.scale.length) : deg;
79    // console.log([octave, degree]);
80    return [octave, degree];
81  }
82
83  octdegfreq(oct, deg) {
84    if (this.octdegfreqs[oct]) {
85      return this.octdegfreqs[oct][deg];
86    } else {
87      return null;
88    }
89  }
90
91  octdegmidi(oct, deg) {
92    if (this.octdegmidis[oct]) {
93      return this.octdegmidis[oct][deg];
94    } else {
95      return null;
96    }
97  }
98}