diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index bd8b35ed..d83e271f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -579,6 +579,7 @@ const generic_params = [ ['dictionary', 'dict'], // which dictionary to use for the voicings ['anchor'], // the top note to align the voicing to, defaults to c5 ['offset'], // how the voicing is offset from the anchored position + ['octaves'], // how many octaves are voicing steps spread apart, defaults to 1 [['mode', 'anchor']], // below = anchor note will be removed from the voicing, useful for melody harmonization /** diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 56c3e35b..e2d24b27 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -76,11 +76,11 @@ export const midi2note = (midi, sharp = false) => { return pc + oct; }; -export function scaleStep(notes, offset) { +export function scaleStep(notes, offset, octaves = 1) { notes = notes.map((note) => (typeof note === 'string' ? noteToMidi(note) : note)); - const octOffset = Math.floor(offset / notes.length) * 12; - offset = _mod(offset, 12); - return notes[offset % notes.length] + octOffset; + const octOffset = Math.floor(offset / notes.length) * octaves * 12; + offset = _mod(offset, notes.length); + return notes[offset] + octOffset; } // different ways to resolve the note to compare the anchor to (see renderVoicing) @@ -90,7 +90,7 @@ let modeTarget = { above: (v) => v[0], }; -export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5' }) { +export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) { const [root, symbol] = tokenizeChord(chord); const rootChroma = pc2chroma(root); anchor = anchor?.note || anchor; @@ -124,7 +124,7 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below' notes = notes.filter((_, i) => voicingMidi[i] !== noteToMidi(anchor)); } if (n !== undefined) { - return [scaleStep(notes, n)]; + return [scaleStep(notes, n, octaves)]; } return notes; } diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index c7b4f6b8..2b5da32d 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -196,10 +196,10 @@ export const voicing = register('voicing', function (pat) { .fmap((value) => { // destructure voicing controls out value = typeof value === 'string' ? { chord: value } : value; - let { dictionary = 'default', chord, anchor, offset, mode, n, ...rest } = value; + let { dictionary = 'default', chord, anchor, offset, mode, n, octaves, ...rest } = value; dictionary = typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' }; - let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n }); + let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n, octaves }); return stack(...notes) .note()