mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-24 20:18:34 +00:00
fix: support existing voicing dicts in new logic
+ rename voiceBelow to renderVoicing
This commit is contained in:
parent
0b3a8a5f65
commit
b0cbeda796
@ -17,7 +17,7 @@ import {
|
|||||||
note2oct,
|
note2oct,
|
||||||
note2midi,
|
note2midi,
|
||||||
midi2note,
|
midi2note,
|
||||||
voiceBelow,
|
renderVoicing,
|
||||||
scaleStep,
|
scaleStep,
|
||||||
} from '../tonleiter.mjs';
|
} from '../tonleiter.mjs';
|
||||||
|
|
||||||
@ -56,8 +56,9 @@ describe('tonleiter', () => {
|
|||||||
expect(pc2chroma('D')).toBe(2);
|
expect(pc2chroma('D')).toBe(2);
|
||||||
expect(pc2chroma('Db')).toBe(1);
|
expect(pc2chroma('Db')).toBe(1);
|
||||||
expect(pc2chroma('Dbb')).toBe(0);
|
expect(pc2chroma('Dbb')).toBe(0);
|
||||||
//lowercase
|
expect(pc2chroma('bb')).toBe(10);
|
||||||
// expect(pc2chroma('c')).toBe(0); // TODO
|
expect(pc2chroma('f')).toBe(5);
|
||||||
|
expect(pc2chroma('c')).toBe(0);
|
||||||
});
|
});
|
||||||
test('rotateChroma', () => {
|
test('rotateChroma', () => {
|
||||||
expect(rotateChroma(0, 1)).toBe(1);
|
expect(rotateChroma(0, 1)).toBe(1);
|
||||||
@ -114,16 +115,17 @@ describe('tonleiter', () => {
|
|||||||
expect(scaleStep([60, 63, 67], -3)).toBe(48);
|
expect(scaleStep([60, 63, 67], -3)).toBe(48);
|
||||||
expect(scaleStep([60, 63, 67], -4)).toBe(43);
|
expect(scaleStep([60, 63, 67], -4)).toBe(43);
|
||||||
});
|
});
|
||||||
test('voiceBelow', () => {
|
test('renderVoicing', () => {
|
||||||
const voicingDictionary = {
|
const voicingDictionary = {
|
||||||
m7: [
|
m7: [
|
||||||
'3 7 10 14', // b3 5 b7 9
|
'3 7 10 14', // b3 5 b7 9
|
||||||
'10 14 15 19', // b7 9 b3 5
|
'10 14 15 19', // b7 9 b3 5
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
expect(voiceBelow('Bb4', 'Em7', voicingDictionary)).toEqual(['G3', 'B3', 'D4', 'Gb4']);
|
expect(renderVoicing('Em7', 'Bb4', voicingDictionary)).toEqual(['G3', 'B3', 'D4', 'Gb4']);
|
||||||
expect(voiceBelow('D5', 'Cm7', voicingDictionary)).toEqual(['Eb4', 'G4', 'Bb4', 'D5']);
|
expect(renderVoicing('Cm7', 'D5', voicingDictionary)).toEqual(['Eb4', 'G4', 'Bb4', 'D5']);
|
||||||
expect(voiceBelow('G5', 'Cm7', voicingDictionary)).toEqual(['Bb4', 'D5', 'Eb5', 'G5']);
|
expect(renderVoicing('Cm7', 'G5', voicingDictionary)).toEqual(['Bb4', 'D5', 'Eb5', 'G5']);
|
||||||
|
expect(renderVoicing('Cm7', 'g5', voicingDictionary)).toEqual(['Bb4', 'D5', 'Eb5', 'G5']);
|
||||||
// expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']);
|
// expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']);
|
||||||
// TODO: test with offset
|
// TODO: test with offset
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { isNote, isNoteWithOctave, _mod } from '@strudel.cycles/core';
|
import { isNote, isNoteWithOctave, _mod } from '@strudel.cycles/core';
|
||||||
|
import { Interval } from '@tonaljs/tonal';
|
||||||
|
|
||||||
// https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515
|
// https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515
|
||||||
|
|
||||||
const flats = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
const flats = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||||
|
const pcs = ['c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'bb', 'b'];
|
||||||
const sharps = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
const sharps = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||||
const accs = { b: -1, '#': 1 };
|
const accs = { b: -1, '#': 1 };
|
||||||
|
|
||||||
export const pc2chroma = (pc) => {
|
export const pc2chroma = (pc) => {
|
||||||
const [letter, ...rest] = pc.split('');
|
const [letter, ...rest] = pc.split('');
|
||||||
return flats.indexOf(letter) + rest.reduce((sum, sign) => sum + accs[sign], 0);
|
return pcs.indexOf(letter.toLowerCase()) + rest.reduce((sum, sign) => sum + accs[sign], 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rotateChroma = (chroma, steps) => (chroma + steps) % 12;
|
export const rotateChroma = (chroma, steps) => (chroma + steps) % 12;
|
||||||
@ -54,6 +56,14 @@ export const x2chroma = (x) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const step2semitones = (x) => {
|
||||||
|
let num = Number(x);
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
return Interval.semitones(x);
|
||||||
|
};
|
||||||
|
|
||||||
export const x2midi = (x) => {
|
export const x2midi = (x) => {
|
||||||
if (typeof x === 'number') {
|
if (typeof x === 'number') {
|
||||||
return x;
|
return x;
|
||||||
@ -77,13 +87,13 @@ export function scaleStep(notes, offset) {
|
|||||||
return notes[offset % notes.length] + octOffset;
|
return notes[offset % notes.length] + octOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function voiceBelow(maxNote, chord, voicingDictionary, offset = 0, n) {
|
export function renderVoicing(chord, anchor, voicingDictionary, offset = 0, n) {
|
||||||
const [root, symbol] = tokenizeChord(chord);
|
const [root, symbol] = tokenizeChord(chord);
|
||||||
const maxPc = note2pc(maxNote);
|
const maxPc = note2pc(anchor);
|
||||||
const maxChroma = pc2chroma(maxPc);
|
const maxChroma = pc2chroma(maxPc);
|
||||||
const rootChroma = pc2chroma(root);
|
const rootChroma = pc2chroma(root);
|
||||||
const voicings = voicingDictionary[symbol].map((voicing) =>
|
const voicings = voicingDictionary[symbol].map((voicing) =>
|
||||||
typeof voicing === 'string' ? voicing.split(' ').map((n) => parseInt(n, 10)) : voicing,
|
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
|
||||||
);
|
);
|
||||||
|
|
||||||
let minDistance, bestIndex;
|
let minDistance, bestIndex;
|
||||||
@ -100,7 +110,7 @@ export function voiceBelow(maxNote, chord, voicingDictionary, offset = 0, n) {
|
|||||||
const octDiff = Math.ceil(offset / voicings.length) * 12;
|
const octDiff = Math.ceil(offset / voicings.length) * 12;
|
||||||
const indexWithOffset = _mod(bestIndex + offset, voicings.length);
|
const indexWithOffset = _mod(bestIndex + offset, voicings.length);
|
||||||
const voicing = voicings[indexWithOffset];
|
const voicing = voicings[indexWithOffset];
|
||||||
const maxMidi = note2midi(maxNote);
|
const maxMidi = note2midi(anchor);
|
||||||
const topMidi = maxMidi - chromaDiffs[indexWithOffset] + octDiff;
|
const topMidi = maxMidi - chromaDiffs[indexWithOffset] + octDiff;
|
||||||
|
|
||||||
const voicingMidi = voicing.map((v) => topMidi - voicing[voicing.length - 1] + v);
|
const voicingMidi = voicing.map((v) => topMidi - voicing[voicing.length - 1] + v);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { stack, register } from '@strudel.cycles/core';
|
import { stack, register } from '@strudel.cycles/core';
|
||||||
import { voiceBelow, note2midi, x2midi } from './tonleiter.mjs';
|
import { renderVoicing, note2midi, x2midi } from './tonleiter.mjs';
|
||||||
import _voicings from 'chord-voicings';
|
import _voicings from 'chord-voicings';
|
||||||
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
|
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ export const voicing = register('voicing', function (pat) {
|
|||||||
if (typeof dictionary === 'string') {
|
if (typeof dictionary === 'string') {
|
||||||
dictionary = voicingRegistry[dictionary]?.dictionary;
|
dictionary = voicingRegistry[dictionary]?.dictionary;
|
||||||
}
|
}
|
||||||
let notes = voiceBelow(anchor, chord, dictionary, offset, n);
|
let notes = renderVoicing(chord, anchor, dictionary, offset, n);
|
||||||
if (mode === 'below') {
|
if (mode === 'below') {
|
||||||
notes = notes.filter((n) => x2midi(n) !== note2midi(anchor));
|
notes = notes.filter((n) => x2midi(n) !== note2midi(anchor));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user