diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index 5cf710f8..e5d44917 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -17,7 +17,7 @@ import { note2oct, note2midi, midi2note, - voiceBelow, + renderVoicing, scaleStep, } from '../tonleiter.mjs'; @@ -56,8 +56,9 @@ describe('tonleiter', () => { expect(pc2chroma('D')).toBe(2); expect(pc2chroma('Db')).toBe(1); expect(pc2chroma('Dbb')).toBe(0); - //lowercase - // expect(pc2chroma('c')).toBe(0); // TODO + expect(pc2chroma('bb')).toBe(10); + expect(pc2chroma('f')).toBe(5); + expect(pc2chroma('c')).toBe(0); }); test('rotateChroma', () => { 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], -4)).toBe(43); }); - test('voiceBelow', () => { + test('renderVoicing', () => { const voicingDictionary = { m7: [ '3 7 10 14', // b3 5 b7 9 '10 14 15 19', // b7 9 b3 5 ], }; - expect(voiceBelow('Bb4', 'Em7', voicingDictionary)).toEqual(['G3', 'B3', 'D4', 'Gb4']); - expect(voiceBelow('D5', 'Cm7', voicingDictionary)).toEqual(['Eb4', 'G4', 'Bb4', 'D5']); - expect(voiceBelow('G5', 'Cm7', voicingDictionary)).toEqual(['Bb4', 'D5', 'Eb5', 'G5']); + expect(renderVoicing('Em7', 'Bb4', voicingDictionary)).toEqual(['G3', 'B3', 'D4', 'Gb4']); + expect(renderVoicing('Cm7', 'D5', voicingDictionary)).toEqual(['Eb4', 'G4', 'Bb4', 'D5']); + 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']); // TODO: test with offset }); diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index d2be75ca..ee5a4d16 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -1,14 +1,16 @@ 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 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 accs = { b: -1, '#': 1 }; export const pc2chroma = (pc) => { 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; @@ -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) => { if (typeof x === 'number') { return x; @@ -77,13 +87,13 @@ export function scaleStep(notes, offset) { 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 maxPc = note2pc(maxNote); + const maxPc = note2pc(anchor); const maxChroma = pc2chroma(maxPc); const rootChroma = pc2chroma(root); 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; @@ -100,7 +110,7 @@ export function voiceBelow(maxNote, chord, voicingDictionary, offset = 0, n) { const octDiff = Math.ceil(offset / voicings.length) * 12; const indexWithOffset = _mod(bestIndex + offset, voicings.length); const voicing = voicings[indexWithOffset]; - const maxMidi = note2midi(maxNote); + const maxMidi = note2midi(anchor); const topMidi = maxMidi - chromaDiffs[indexWithOffset] + octDiff; const voicingMidi = voicing.map((v) => topMidi - voicing[voicing.length - 1] + v); diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index c40d7193..79722d01 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -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 { voiceBelow, note2midi, x2midi } from './tonleiter.mjs'; +import { renderVoicing, note2midi, x2midi } from './tonleiter.mjs'; import _voicings from 'chord-voicings'; 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') { dictionary = voicingRegistry[dictionary]?.dictionary; } - let notes = voiceBelow(anchor, chord, dictionary, offset, n); + let notes = renderVoicing(chord, anchor, dictionary, offset, n); if (mode === 'below') { notes = notes.filter((n) => x2midi(n) !== note2midi(anchor)); }