mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 06:19:33 -04:00
Add packages/edo for Equal Division of the Octave scales
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# @strudel/edo
|
||||
|
||||
This package adds EDO scale functions to strudel Patterns.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel/edo --save
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import { n } from '@strudel/core';
|
||||
import '@strudel/edo';
|
||||
|
||||
// E.g. edoScale for Gorgo-6 scale, 16 EDO, LLsLLLs
|
||||
// base note C3, large step size 3, small step size 1:
|
||||
// C3:LLsLLL:3:1
|
||||
const [baseNote, sequence, largeStep, smallStep] = ['C3', 'LLsLLL', 3, 1]
|
||||
const pattern = n("0 2 4 6 4 2").edoScale([baseNote, sequence, largeStep, smallStep]);
|
||||
|
||||
const events = pattern.firstCycle().map((e) => e.show());
|
||||
console.log(events);
|
||||
```
|
||||
|
||||
yields:
|
||||
```
|
||||
[
|
||||
"[ 0/1 → 1/1 |
|
||||
{
|
||||
\"degree\":1,
|
||||
\"degreeIndexes\":[0,3,6,7,10,13],
|
||||
\"intLabels\":[null,\"S2\",\"d4\",\"N4\",\"s6\",\"s7\",\"P8\"],
|
||||
\"root\":\"130.8128\",
|
||||
\"freq\":130.813,
|
||||
\"edo\":16
|
||||
}
|
||||
]"
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
edo.mjs - Equal division of the octave (EDO) scale functions for strudel
|
||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edo.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/>.
|
||||
*/
|
||||
|
||||
import { register, pure, noteToMidi, isNote, tokenizeNote } from '@strudel/core';
|
||||
import { EdoScale } from './edoscale.mjs';
|
||||
import { Intervals } from './intervals.mjs';
|
||||
import { Pitches } from './pitches.mjs';
|
||||
|
||||
const pitchesCache = new Map();
|
||||
|
||||
export const edoScale = register(
|
||||
'edoScale',
|
||||
function (scaleDefinition, pat) {
|
||||
// console.log(scaleDefinition);
|
||||
|
||||
// if (Array.isArray(scale)) {
|
||||
const key = scaleDefinition.flat().join(':');
|
||||
// }
|
||||
// console.log(scaleDefinition);
|
||||
let pitches;
|
||||
if (pitchesCache.has(key)) {
|
||||
pitches = pitchesCache.get(key);
|
||||
} else {
|
||||
// console.log({ key });
|
||||
const [base_note, sequence, large, small] = scaleDefinition;
|
||||
const root_octave = tokenizeNote(base_note)[2] || 3;
|
||||
// console.log({ root_octave });
|
||||
const scale = new EdoScale(large, small, sequence);
|
||||
const intervals = new Intervals(scale);
|
||||
// console.log({ intervals });
|
||||
pitches = new Pitches(scale, intervals, 440, noteToMidi(base_note), root_octave);
|
||||
pitchesCache.set(key, pitches);
|
||||
// console.log({ base_note: noteToMidi(base_note) });
|
||||
// console.log({ sequence });
|
||||
// console.log({ edivisions: scale.edivisions });
|
||||
// console.log({ intervals });
|
||||
// console.log({ pat });
|
||||
// console.log({ pitches: pitches });
|
||||
}
|
||||
return pat
|
||||
.fmap((value) => {
|
||||
const isObject = typeof value === 'object';
|
||||
const n = isObject ? value.n : value;
|
||||
if (isObject) {
|
||||
delete value.n; // remove n so it won't cause trouble
|
||||
}
|
||||
if (isNote(n)) {
|
||||
// legacy..
|
||||
return pure(n);
|
||||
}
|
||||
const deg = (typeof n === 'string' ? parseInt(n, 10) : n) + 1;
|
||||
|
||||
const [oct, degree] = pitches.octdeg(deg);
|
||||
const freq = pitches.octdegfreq(oct, degree);
|
||||
const note = pitches.octdegmidi(oct, degree);
|
||||
const edo = pitches.scale.edivisions;
|
||||
const root = pitches.base_freq;
|
||||
const degreeIndexes = pitches.scale.divisions;
|
||||
const intLabels = pitches.intervals.intLabels;
|
||||
// const color = 'red';
|
||||
value = pure(isObject ? { ...value, degree, degreeIndexes, intLabels, root, freq, edo } : note);
|
||||
// value = pure(isObject ? { ...value, key } : note);
|
||||
// value = pure(isObject ? { ...value, edo } : note);
|
||||
// console.log({ value });
|
||||
return value;
|
||||
})
|
||||
.outerJoin()
|
||||
.withHap((hap) => hap.setContext({ ...hap.context, scaleDefinition }));
|
||||
},
|
||||
true,
|
||||
true, // preserve tactus
|
||||
);
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
const M = 2;
|
||||
const L = 1;
|
||||
const S = 0;
|
||||
const LABELS = ['s', 'L', 'M'];
|
||||
|
||||
export class EdoScale {
|
||||
constructor(large, small, sequence, medium) {
|
||||
this.stepbackup = [L, L, S, L, L, L, S, L, L, L, S, L, L, L, S, L];
|
||||
this.large = large;
|
||||
this.medium = medium || large;
|
||||
this.small = small;
|
||||
this.divisions = [];
|
||||
this.edivisions = null;
|
||||
this.sequence = null;
|
||||
this.tonic = 1;
|
||||
this.mode = 1;
|
||||
this.max_steps = 12;
|
||||
this.min_steps = 3;
|
||||
this.setSequence(sequence);
|
||||
}
|
||||
|
||||
hasMedium() {
|
||||
return this.step.some((_, i) => this.step[this.offset(i)] === M);
|
||||
}
|
||||
|
||||
stepSize(i) {
|
||||
return LABELS[this.step[this.offset(i)]];
|
||||
}
|
||||
|
||||
sequence() {
|
||||
return this.step.map((_, i) => this.stepSize(i)).join('');
|
||||
}
|
||||
|
||||
stepValue(i) {
|
||||
const step = this.step[this.offset(i)];
|
||||
return step === L ? this.large : step === M ? this.medium : this.small;
|
||||
}
|
||||
|
||||
offset(i) {
|
||||
if (this.mode === 1) {
|
||||
return i;
|
||||
} else {
|
||||
const offset = (this.mode - 1 + i) % this.length;
|
||||
return offset === 0 ? this.length : offset;
|
||||
}
|
||||
}
|
||||
|
||||
static setMaxSteps(max) {
|
||||
this.max_steps = max;
|
||||
}
|
||||
|
||||
static setMinSteps(min) {
|
||||
this.min_steps = min;
|
||||
}
|
||||
|
||||
setSequence(sequence) {
|
||||
if (this.sequence !== sequence) {
|
||||
this.sequence = sequence;
|
||||
this.length = sequence.length;
|
||||
this.step = [];
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const char = sequence[i];
|
||||
this.step[i] = char === 'L' ? L : char === 'M' ? M : S;
|
||||
}
|
||||
this.updateEdo();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setLarge(l) {
|
||||
if (this.large !== l) {
|
||||
this.large = l;
|
||||
this.updateEdo();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setMedium(m) {
|
||||
if (this.medium !== m) {
|
||||
this.medium = m;
|
||||
this.updateEdo();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setSmall(s) {
|
||||
if (this.small !== s) {
|
||||
this.small = s;
|
||||
this.updateEdo();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setMode(mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
setTonic(tonic) {
|
||||
this.tonic = tonic;
|
||||
}
|
||||
|
||||
changeMode(d) {
|
||||
const orig = this.mode;
|
||||
this.mode = Math.max(1, Math.min(this.mode + d, this.length));
|
||||
return orig !== this.mode;
|
||||
}
|
||||
|
||||
changeTonic(d) {
|
||||
const orig = this.tonic;
|
||||
this.tonic = Math.max(1, Math.min(this.tonic + d, this.edivisions));
|
||||
return orig !== this.tonic;
|
||||
}
|
||||
|
||||
updateEdo() {
|
||||
const orig = this.edivisions;
|
||||
this.edivisions = this.step.reduce((sum, _, i) => {
|
||||
this.divisions[i] = sum;
|
||||
return sum + this.stepValue(i);
|
||||
}, 0);
|
||||
// console.log(this.divisions);
|
||||
const changed = orig !== this.edivisions;
|
||||
if (changed) {
|
||||
this.tonic = Math.max(1, Math.min(this.tonic, this.edivisions));
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
changeStep(d, i) {
|
||||
const index = this.offset(i);
|
||||
const orig = this.step[index];
|
||||
this.step[index] = Math.max(S, Math.min(this.step[index] + d, M));
|
||||
this.stepbackup[index] = this.step[index];
|
||||
const changed = orig !== this.step[index];
|
||||
if (changed) {
|
||||
this.updateEdo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
changeLarge(d) {
|
||||
const orig = this.large;
|
||||
this.setLarge(Math.max(this.small + 1, Math.min(this.large + d, this.large + 1)));
|
||||
const changed = this.large !== orig;
|
||||
if (changed) {
|
||||
if (this.large <= this.medium) {
|
||||
this.setMedium(Math.max(this.small + 1, Math.min(this.large - 1, this.large)));
|
||||
}
|
||||
this.updateEdo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
changeMedium(d) {
|
||||
const orig = this.medium;
|
||||
this.setMedium(Math.max(this.small + 1, Math.min(this.medium + d, this.large - 1)));
|
||||
const changed = this.medium !== orig;
|
||||
if (changed) {
|
||||
this.updateEdo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
changeSmall(d) {
|
||||
const orig = this.small;
|
||||
const value = this.small + d;
|
||||
|
||||
if (this.hasMedium()) {
|
||||
this.setSmall(Math.max(1, Math.min(value, this.medium - 1)));
|
||||
} else {
|
||||
this.setSmall(Math.max(1, Math.min(value, this.large - 1)));
|
||||
}
|
||||
|
||||
const changed = this.small !== orig;
|
||||
if (changed) {
|
||||
if (this.small >= this.medium) {
|
||||
this.setMedium(Math.max(this.small + 1, Math.min(this.large)));
|
||||
}
|
||||
this.updateEdo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
changeLength(d) {
|
||||
const orig = this.length;
|
||||
if (d === 1) {
|
||||
this.length = Math.min(this.length + 1, this.max_steps);
|
||||
} else if (d === -1) {
|
||||
this.length = Math.max(this.length - 1, this.min_steps);
|
||||
}
|
||||
|
||||
const changed = this.length !== orig;
|
||||
if (changed) {
|
||||
this.mode = 1;
|
||||
if (d === 1) {
|
||||
this.step[this.length] = this.stepbackup[this.length] || L;
|
||||
} else if (d === -1 && this.length >= this.min_steps) {
|
||||
this.step.pop();
|
||||
}
|
||||
this.updateEdo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import './edo.mjs';
|
||||
|
||||
export * from './edo.mjs';
|
||||
|
||||
export const packageName = '@strudel/edo';
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
import ratiointervals from './ratios.mjs';
|
||||
|
||||
function ratio(division, edivisions) {
|
||||
return division === 0 ? 1 : Math.pow(2, division / edivisions);
|
||||
}
|
||||
|
||||
export class Intervals {
|
||||
constructor(scale) {
|
||||
this.scale = scale;
|
||||
this.intLabels = [];
|
||||
this.intNoms = [];
|
||||
this.intRatios = [];
|
||||
this.uniqLabels = [];
|
||||
this.intErrors = [];
|
||||
this.ratios = [];
|
||||
|
||||
const BLANK = '';
|
||||
let division = 0;
|
||||
const labToErr = {};
|
||||
const labToInd = {};
|
||||
|
||||
this.ratios[0] = 1;
|
||||
|
||||
for (let i = 0; i < scale.length; i++) {
|
||||
division += scale.stepValue(i);
|
||||
this.ratios[i + 1] = ratio(division, scale.edivisions);
|
||||
|
||||
if (i < scale.length) {
|
||||
const nearest = ratiointervals.nearestInterval(this.ratios[i + 1]);
|
||||
const closeness = nearest[0];
|
||||
const ratio = nearest[1];
|
||||
const intLabel = ratiointervals.key(ratio);
|
||||
this.intLabels[i + 1] = intLabel;
|
||||
this.intErrors[i + 1] = closeness;
|
||||
this.intNoms[i + 1] = ratio ? ratiointervals.nom(ratio) : 0;
|
||||
this.intRatios[i + 1] = ratio ? `${ratiointervals.nom(ratio)}/${ratiointervals.denom(ratio)}` : '';
|
||||
this.uniqLabels[i + 1] = BLANK;
|
||||
|
||||
if (intLabel && intLabel !== 'P1' && intLabel !== 'P8') {
|
||||
if (!labToErr[intLabel]) {
|
||||
this.uniqLabels[i + 1] = intLabel;
|
||||
labToInd[intLabel] = i + 1;
|
||||
labToErr[intLabel] = closeness;
|
||||
} else if (closeness < labToErr[intLabel]) {
|
||||
this.uniqLabels[labToInd[intLabel]] = BLANK;
|
||||
this.uniqLabels[i + 1] = intLabel;
|
||||
labToInd[intLabel] = i + 1;
|
||||
labToErr[intLabel] = closeness;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ratio(i) {
|
||||
return this.ratios[i];
|
||||
}
|
||||
|
||||
intervalLabel(i) {
|
||||
return this.intLabels[i];
|
||||
}
|
||||
|
||||
intervalNominator(i) {
|
||||
return this.intNoms[i];
|
||||
}
|
||||
|
||||
intervalRatio(i) {
|
||||
return this.intRatios[i];
|
||||
}
|
||||
|
||||
uniqIntervalLabel(i) {
|
||||
return this.uniqLabels[i];
|
||||
}
|
||||
|
||||
intervalError(i) {
|
||||
return this.intErrors[i];
|
||||
}
|
||||
|
||||
nearestDegreeTo(r, threshold) {
|
||||
let min = 1;
|
||||
let degree = null;
|
||||
|
||||
for (const [i, v] of Object.entries(this.ratios)) {
|
||||
const diff = Math.abs((r - v) / r);
|
||||
if (diff < min) {
|
||||
min = diff;
|
||||
degree = parseInt(i, 10);
|
||||
}
|
||||
}
|
||||
|
||||
if (threshold == null) {
|
||||
return degree;
|
||||
} else {
|
||||
return min < threshold ? degree : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@strudel/edo",
|
||||
"version": "0.1.0",
|
||||
"description": "Equal division of the octave (EDO) scale functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://codeberg.org/uzu/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Rob McKinnon <rob@movingflow>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://codeberg.org/uzu/strudel/issues"
|
||||
},
|
||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@tonaljs/tonal": "^4.10.0",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"webmidi": "^3.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
pitches.mjs - defines Pitches for equal division of the octave (EDO) scale
|
||||
- Port of pitfalls/lib/Pitches.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Pitches.lua>
|
||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/pitches.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/>.
|
||||
*/
|
||||
|
||||
function ratio(division, edivisions) {
|
||||
return division === 0 ? 1 : Math.pow(2, division / edivisions);
|
||||
}
|
||||
|
||||
// This function gets frequency based on index, edo, octave, and base frequency
|
||||
function get_freq(base_freq, edo, index, oct, base_octave) {
|
||||
let f = base_freq * ratio(index - 1, edo);
|
||||
if (oct < base_octave) {
|
||||
f /= Math.pow(2, base_octave - oct);
|
||||
} else if (oct > base_octave) {
|
||||
f *= Math.pow(2, oct - base_octave);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// Convert MIDI number to Hz using a given tuning multiplier
|
||||
function midi_to_hz(n, tuning) {
|
||||
return tuning * Math.pow(2, (n - 69) / 12.0);
|
||||
}
|
||||
|
||||
const denom = Math.log(2);
|
||||
function hz_to_midi(freq, tuning) {
|
||||
return 12.0 * (Math.log(freq / tuning) / denom) + 69;
|
||||
}
|
||||
|
||||
export class Pitches {
|
||||
constructor(scale, intervals, tuning, midi_start, root_octave) {
|
||||
this.scale = scale;
|
||||
this.intervals = intervals;
|
||||
this.base_freq = midi_to_hz(midi_start, tuning);
|
||||
this.root_octave = root_octave;
|
||||
this.freqs = {};
|
||||
this.midis = {};
|
||||
this.degrees = {};
|
||||
this.octdegfreqs = {};
|
||||
this.octdegmidis = {};
|
||||
let index = 0;
|
||||
let f = null;
|
||||
|
||||
for (let oct = 0; oct <= 8; oct++) {
|
||||
this.octdegfreqs[oct] = {};
|
||||
this.octdegmidis[oct] = {};
|
||||
f = get_freq(this.base_freq, scale.edivisions, scale.tonic, oct, this.root_octave);
|
||||
for (let deg = 0; deg < scale.length; deg++) {
|
||||
index = index + 1;
|
||||
this.freqs[index] = parseFloat((f * intervals.ratio(deg)).toFixed(3));
|
||||
this.midis[index] = parseFloat(hz_to_midi(f * intervals.ratio(deg), tuning).toFixed(4));
|
||||
this.degrees[index] = deg;
|
||||
this.octdegfreqs[oct][deg + 1] = this.freqs[index];
|
||||
this.octdegmidis[oct][deg + 1] = this.midis[index];
|
||||
}
|
||||
}
|
||||
this.base_freq = parseFloat(this.base_freq).toFixed(4);
|
||||
}
|
||||
|
||||
base_freq() {
|
||||
return this.base_freq;
|
||||
}
|
||||
|
||||
degree(index) {
|
||||
return this.degrees[index];
|
||||
}
|
||||
|
||||
freq(index) {
|
||||
return this.freqs[index];
|
||||
}
|
||||
|
||||
octdeg(deg) {
|
||||
const higherOcatve = deg > this.scale.length;
|
||||
const octave = this.root_octave + (higherOcatve ? Math.floor((deg - 1) / this.scale.length) : 0);
|
||||
const degree = higherOcatve ? (deg % this.scale.length === 0 ? this.scale.length : deg % this.scale.length) : deg;
|
||||
console.log([octave, degree]);
|
||||
return [octave, degree];
|
||||
}
|
||||
|
||||
octdegfreq(oct, deg) {
|
||||
if (this.octdegfreqs[oct]) {
|
||||
return this.octdegfreqs[oct][deg];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
octdegmidi(oct, deg) {
|
||||
if (this.octdegmidis[oct]) {
|
||||
return this.octdegmidis[oct][deg];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
ratios.mjs - lists whole number ratios for pitch intervals
|
||||
- Port of pitfalls/lib/ratios.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/ratios.lua>
|
||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/ratios.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/>.
|
||||
*/
|
||||
const ratiointervals = {};
|
||||
|
||||
// FJS Calculators - https://misotanni.github.io/fjs/en/calc.html
|
||||
// List of pitch intervals - https://en.wikipedia.org/wiki/List_of_pitch_intervals
|
||||
// Gallery of just intervals - https://en.xen.wiki/w/Gallery_of_just_intervals
|
||||
// Two letter codes changed to make different interval labels more unique.
|
||||
ratiointervals.list = new Map([
|
||||
[1, ['P1', 'P1', 'P1', 1, 1]], // unison P1
|
||||
[16 / 15, ['m2', 'm2', 'm2_5', 16, 15]], // minor second m2
|
||||
[15 / 14, ['A1', 'A1', 'A1^5_7', 15, 14]], // augmented unison
|
||||
[13 / 12, ['t2', 'm2', 'm2^13', 13, 12]], // tridecimal neutral second
|
||||
[12 / 11, ['N2', 'M2', 'M2_11', 12, 11]], // undecimal neutral second
|
||||
[11 / 10, ['n2', 'm2', 'm2^11_5', 11, 10]], // undecimal submajor second
|
||||
[10 / 9, ['T2', 'M2', 'M2^5', 10, 9]], // classic (whole) tone
|
||||
[9 / 8, ['M2', 'M2', 'M2', 9, 8]], // major second M2
|
||||
[8 / 7, ['S2', 'M2', 'M2_7', 8, 7]], // septimal major second
|
||||
[7 / 6, ['s3', 'm3', 'm3^7', 7, 6]], // septimal minor third
|
||||
[19 / 16, ['o3', 'm3', 'm3^19', 19, 16]], // otonal minor third
|
||||
[6 / 5, ['m3', 'm3', 'm3_5', 6, 5]], // minor third m3
|
||||
[17 / 14, ['t3', 'm3', 'm3^17_7', 17, 14]], // septendecimal supraminor third
|
||||
[11 / 9, ['n3', 'm3', 'm3^11', 11, 9]], // undecimal neutral third
|
||||
[5 / 4, ['M3', 'M3', 'M3^5', 5, 4]], // major third M3
|
||||
[9 / 7, ['S3', 'M3', 'M3_7', 9, 7]], // septimal major third SM3
|
||||
[13 / 10, ['d4', 'd4', 'd4^13_5', 13, 10]], // Barbados third
|
||||
[4 / 3, ['P4', 'P4', 'P4', 4, 3]], // perfect fourth P4
|
||||
[19 / 14, ['N4', 'P4', 'P4^19_7', 19, 14]], // undevicesimal wide fourth
|
||||
[11 / 8, ['n4', 'P4', 'P4^11', 11, 8]], // super-fourth
|
||||
[25 / 18, ['a4', 'A4', 'A4^5,5', 25, 18]], // classic augmented fourth
|
||||
[7 / 5, ['sT', 'd5', 'd5^7_5', 7, 5]], // lesser septimal tritone
|
||||
[45 / 32, ['A4', 'A4', 'A4^5', 45, 32]], // just augmented fourth
|
||||
[17 / 12, ['d5', 'd5', 'd5^17', 17, 12]], // larger septendecimal tritone
|
||||
[10 / 7, ['ST', 'A4', 'A4^5_7', 10, 7]], // greater septimal tritone
|
||||
[13 / 9, ['t5', 'd5', 'd5^13', 13, 9]], // tridecimal diminished fifth
|
||||
[3 / 2, ['P5', 'P5', 'P5', 3, 2]], // perfect fifth P5
|
||||
[14 / 9, ['s6', 'M6', 'm6^7', 14, 9]], // subminor sixth or septimal sixth
|
||||
[25 / 16, ['a5', 'A5', 'A5^5,5', 25, 16]], // classic augmented fifth
|
||||
[11 / 7, ['A5', 'P5', 'P5^11_7', 11, 7]], // undecimal minor sixth
|
||||
[8 / 5, ['m6', 'm6', 'm6_5', 8, 5]], // minor sixth m6
|
||||
[13 / 8, ['N6', 'm6', 'm6^13', 13, 8]], // tridecimal neutral sixth
|
||||
[18 / 11, ['n6', 'M6', 'M6_11', 18, 11]], // undecimal neutral sixth
|
||||
[5 / 3, ['M6', 'M6', 'M6^5', 5, 3]], // just major sixth M6
|
||||
[128 / 75, ['d7', 'd7', 'd7_5,5', 128, 75]], // diminished seventh
|
||||
[17 / 10, ['T6', 'd7', 'd7^17_5', 17, 10]], // septendecimal diminished seventh
|
||||
[12 / 7, ['S6', 'M6', 'M6_7', 12, 7]], // septimal major sixth
|
||||
[7 / 4, ['s7', 'm7', 'm7^7', 7, 4]], // septimal minor seventh
|
||||
[16 / 9, ['m7', 'm7', 'm7', 16, 9]], // lesser minor seventh
|
||||
[9 / 5, ['g7', 'm7', 'm7_5', 9, 5]], // greater just minor seventh
|
||||
[11 / 6, ['n7', 'm7', 'm7^11', 11, 6]], // undecimal neutral seventh
|
||||
[13 / 7, ['N7', 'm7', 'm7^13_7', 13, 7]], // tridecimal neutral seventh
|
||||
[15 / 8, ['M7', 'M7', 'M7^5', 15, 8]], // major seventh
|
||||
[17 / 9, ['T7', 'd8', 'd8^17', 17, 9]], // large septendecimal major seventh
|
||||
[19 / 10, ['d8', 'd8', 'd8^19_5', 19, 10]], // large undevicesimal major seventh
|
||||
[2, ['P8', 'P8', 'P8', 2, 1]], // octave P8
|
||||
]);
|
||||
|
||||
ratiointervals.key = function (ratio) {
|
||||
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[0] || '';
|
||||
};
|
||||
|
||||
ratiointervals.label = function (ratio) {
|
||||
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[1] || '';
|
||||
};
|
||||
|
||||
ratiointervals.fjs = function (ratio) {
|
||||
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[2] || '';
|
||||
};
|
||||
|
||||
ratiointervals.nom = function (ratio) {
|
||||
return ratio == null ? null : ratiointervals.list.get(ratio)?.[3] || null;
|
||||
};
|
||||
|
||||
ratiointervals.denom = function (ratio) {
|
||||
return ratio == null ? null : ratiointervals.list.get(ratio)?.[4] || null;
|
||||
};
|
||||
|
||||
ratiointervals.nearestInterval = function (v) {
|
||||
let min = 1;
|
||||
let match = null;
|
||||
for (const [ratio, _labels] of ratiointervals.list) {
|
||||
const diff = Math.abs((ratio - v) / ratio);
|
||||
if (diff < min) {
|
||||
min = diff;
|
||||
match = ratio;
|
||||
}
|
||||
}
|
||||
return min < 0.01 ? [min, match] : [null, null];
|
||||
};
|
||||
|
||||
export default ratiointervals;
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
edo.test.mjs - tests of edo.mjs
|
||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonal.test.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/>.
|
||||
*/
|
||||
import '../edo.mjs';
|
||||
import { n } from '@strudel/core';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('edoScale', () => {
|
||||
it('Should run tonal functions ', () => {
|
||||
// base note A3 = 220Hz
|
||||
let baseNote = 'A3';
|
||||
let root = 220;
|
||||
let freq = 220;
|
||||
let pattern = n('0').edoScale([baseNote, 'LLsLLLs', 2, 1]);
|
||||
let cycle = pattern.firstCycleValues[0];
|
||||
expect(cycle.freq).toEqual(freq);
|
||||
expect(cycle.edo).toEqual(12);
|
||||
expect(cycle.degree).toEqual(1);
|
||||
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
|
||||
|
||||
// base note A4 = 440Hz
|
||||
baseNote = 'A4';
|
||||
root = 440;
|
||||
freq = 880;
|
||||
pattern = n('7').edoScale([baseNote, 'LLsLLLs', 2, 1]);
|
||||
cycle = pattern.firstCycleValues[0];
|
||||
expect(cycle.freq).toEqual(freq);
|
||||
expect(cycle.edo).toEqual(12);
|
||||
expect(cycle.degree).toEqual(1);
|
||||
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
edoscale.test.mjs - tests of EdoScale from edoscale.mjs
|
||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/edoscale.test.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/>.
|
||||
*/
|
||||
import '../edoscale.mjs'; // need to import this to add prototypes
|
||||
import { EdoScale } from '../edoscale.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('EdoScale', () => {
|
||||
it('updates edivisions', () => {
|
||||
let scale = new EdoScale(2, 1, ['L', 'L', 's', 'L', 'L', 'L', 's']);
|
||||
expect(scale.stepSize(0)).toEqual('L');
|
||||
expect(scale.stepValue(0)).toEqual(2);
|
||||
|
||||
expect(scale.stepSize(2)).toEqual('s');
|
||||
expect(scale.stepValue(2)).toEqual(1);
|
||||
|
||||
expect(scale.edivisions).toEqual(12);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
ratios.test.mjs - tests of ratios.mjs
|
||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/ratios.test.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/>.
|
||||
*/
|
||||
import ratiointervals from '../ratios.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ratiointervals', () => {
|
||||
it('updates edivisions', () => {
|
||||
let ratio = 9 / 7;
|
||||
expect(ratiointervals.key(ratio)).toEqual('S3');
|
||||
expect(ratiointervals.label(ratio)).toEqual('M3');
|
||||
expect(ratiointervals.fjs(ratio)).toEqual('M3_7');
|
||||
expect(ratiointervals.nom(ratio)).toEqual(9);
|
||||
expect(ratiointervals.denom(ratio)).toEqual(7);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user