diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index f38f1f25..5cf710f8 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -18,6 +18,7 @@ import { note2midi, midi2note, voiceBelow, + scaleStep, } from '../tonleiter.mjs'; describe('tonleiter', () => { @@ -102,6 +103,17 @@ describe('tonleiter', () => { expect(midi2note(61)).toBe('Db4'); expect(midi2note(61, true)).toBe('C#4'); }); + test('scaleStep', () => { + expect(scaleStep([60, 63, 67], 0)).toBe(60); + expect(scaleStep([60, 63, 67], 1)).toBe(63); + expect(scaleStep([60, 63, 67], 2)).toBe(67); + expect(scaleStep([60, 63, 67], 3)).toBe(72); + expect(scaleStep([60, 63, 67], 4)).toBe(75); + expect(scaleStep([60, 63, 67], -1)).toBe(55); + expect(scaleStep([60, 63, 67], -2)).toBe(51); + expect(scaleStep([60, 63, 67], -3)).toBe(48); + expect(scaleStep([60, 63, 67], -4)).toBe(43); + }); test('voiceBelow', () => { const voicingDictionary = { m7: [ diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 7cb04673..c628847e 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -61,7 +61,14 @@ export const midi2note = (midi, sharp = false) => { return pc + oct; }; -export function voiceBelow(maxNote, chord, voicingDictionary, offset = 0) { +export function scaleStep(notes, offset) { + notes = notes.map((note) => (typeof note === 'string' ? note2midi(note) : note)); + const octOffset = Math.floor(offset / notes.length) * 12; + offset = _mod(offset, 12); + return notes[offset % notes.length] + octOffset; +} + +export function voiceBelow(maxNote, chord, voicingDictionary, offset = 0, n) { const [root, symbol] = tokenizeChord(chord); const maxPc = note2pc(maxNote); const maxChroma = pc2chroma(maxPc); @@ -81,15 +88,18 @@ export function voiceBelow(maxNote, chord, voicingDictionary, offset = 0) { return diff; }); - const octDiff = - offset >= 0 ? Math.ceil(offset / voicings.length) * 12 : Math.floor(Math.abs(offset) / voicings.length) * -12; - bestIndex = _mod(bestIndex + offset, voicings.length); - const voicing = voicings[bestIndex]; + const octDiff = Math.ceil(offset / voicings.length) * 12; + const indexWithOffset = _mod(bestIndex + offset, voicings.length); + const voicing = voicings[indexWithOffset]; const maxMidi = note2midi(maxNote); - const topMidi = maxMidi - chromaDiffs[bestIndex] + octDiff; + const topMidi = maxMidi - chromaDiffs[indexWithOffset] + octDiff; const voicingMidi = voicing.map((v) => topMidi - voicing[voicing.length - 1] + v); - return voicingMidi.map((n) => midi2note(n)); + const notes = voicingMidi.map((n) => midi2note(n)); + if (n !== undefined) { + return [scaleStep(notes, n)]; + } + return notes; } // https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 2e7d0db8..28a4c949 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -139,13 +139,13 @@ export const rootNotes = register('rootNotes', function (octave, pat) { export const voicing = register('voicing', function (dictionary, pat) { return pat .fmap((value) => { - let { voiceMax: max, voiceBelow: below, voiceOffset: offset, chord, ...rest } = value; + let { voiceMax: max, voiceBelow: below, voiceOffset: offset, chord, n, ...rest } = value; let top = max || below; top = top?.note || top || 'c5'; if (typeof dictionary === 'string') { dictionary = voicingRegistry[dictionary]?.dictionary; } - let notes = voiceBelow(top, chord, dictionary, offset); + let notes = voiceBelow(top, chord, dictionary, offset, n); if (below) { notes = notes.filter((n) => note2midi(n) !== note2midi(top)); }