voicings.mjs - <short description TODO> Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/tonal/voicings.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/.
7import { stack, register, silence, logger } from '@strudel/core'; 8import { renderVoicing } from './tonleiter.mjs'; 9import _voicings from 'chord-voicings'; 10import { complex, simple } from './ireal.mjs'; 11const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
13const lefthand = { 14 m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'], 15 7: ['3M 6M 7m 9M', '7m 9M 10M 13M'], 16 '^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'], 17 69: ['3M 5P 6A 9M'], 18 m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'], 19 '7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'], 20 '7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'], 21 o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'], 22 '7#11': ['7m 9M 11A 13A'], 23 '7#9': ['3M 7m 9A'], 24 mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'], 25 m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'], 26}; 27 28const guidetones = { 29 m7: ['3m 7m', '7m 10m'], 30 m9: ['3m 7m', '7m 10m'], 31 7: ['3M 7m', '7m 10M'], 32 '^7': ['3M 7M', '7M 10M'], 33 '^9': ['3M 7M', '7M 10M'], 34 69: ['3M 6M'], 35 6: ['3M 6M', '6M 10M'], 36 m7b5: ['3m 7m', '7m 10m'], 37 '7b9': ['3M 7m', '7m 10M'], 38 '7b13': ['3M 7m', '7m 10M'], 39 o7: ['3m 6M', '6M 10m'], 40 '7#11': ['3M 7m', '7m 10M'], 41 '7#9': ['3M 7m', '7m 10M'], 42 mM7: ['3m 7M', '7M 10m'], 43 m6: ['3m 6M', '6M 10m'], 44}; 45 46const triads = { 47 '': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'], 48 M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'], 49 m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'], 50 o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'], 51 aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'], 52}; 53 54const defaultDictionary = { 55 // triads 56 '': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'], 57 M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'], 58 m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'], 59 o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'], 60 aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'], 61 // sevenths chords 62 m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'], 63 7: ['3M 6M 7m 9M', '7m 9M 10M 13M'], 64 '^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'], 65 69: ['3M 5P 6A 9M'], 66 m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'], 67 '7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'], 68 '7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'], 69 o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'], 70 '7#11': ['7m 9M 11A 13A'], 71 '7#9': ['3M 7m 9A'], 72 mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'], 73 m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'], 74}; 75 76export const voicingRegistry = { 77 lefthand: { dictionary: lefthand, range: ['F3', 'A4'], mode: 'below', anchor: 'a4' }, 78 triads: { dictionary: triads, mode: 'below', anchor: 'a4' }, 79 guidetones: { dictionary: guidetones, mode: 'above', anchor: 'a4' }, 80 legacy: { dictionary: defaultDictionary, mode: 'below', anchor: 'a4' }, 81}; 82 83let defaultDict = 'ireal'; 84export const setDefaultVoicings = (dict) => (defaultDict = dict);
e.g. typeof setDefaultVoicings !== 'undefined' && setDefaultVoicings('legacy');
87export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
Adds a new custom voicing dictionary.
@name addVoicings @tags tonal @memberof Pattern @param {string} name identifier for the voicing dictionary @param {Object} dictionary maps chord symbol to possible voicings @param {Array} range min, max note @returns Pattern @example addVoicings('cookie', { 7: ['3M 7m 9M 12P 15P', '7m 10M 13M 16M 19P'], '^7': ['3M 6M 9M 12P 14M', '7M 10M 13M 16M 19P'], m7: ['8P 11P 14m 17m 19P', '5P 8P 11P 14m 17m'], m7b5: ['3m 5d 8P 11P 14m', '5d 8P 11P 14m 17m'], o7: ['3m 6M 9M 11A 15P'], '7alt': ['3M 7m 10m 13m 15P'], '7#11': ['7m 10m 13m 15P 17m'], }, ['C3', 'C6']) "<C^7 A7 Dm7 G7>".voicings('cookie').note()
new call signature
DEPRECATED: still works, but it is recommended you use .voicing instead (without s). Turns chord symbols into voicings, using the smoothest voice leading possible. Uses chord-voicings package.
@name voicings @tags tonal @memberof Pattern @param {string} dictionary which voicing dictionary to use. @returns Pattern @example stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()
145let lastVoicing; // this now has to be global until another solution is found :-/
it used to be local to the voicings function at evaluation time but since register will patternify by default, means that the function is called over and over again, resetting the lastVoicing variables
Maps the chords of the incoming pattern to root notes in the given octave.
@name rootNotes @tags tonal @memberof Pattern @param {octave} octave octave to use @returns Pattern @example "<C^7 A7 Dm7 G7>".rootNotes(2).note()
Turns chord symbols into voicings. You can use the following control params:
chord: Note, followed by chord symbol, e.g. C Am G7 Bb^7dict: voicing dictionary to use, falls back to default dictionaryanchor: the note that is used to align the chordmode: how the voicing is aligned to the anchorbelow: top note <= anchorduck: top note <= anchor, anchor excludedabove: bottom note >= anchorroot: bottom note is the lowest root of the chord >= anchoroldabove: old (buggy) behavior of above, kept for legacy reasonoldroot: old (buggy) behavior of root, kept for legacy reason
offset: whole number that shifts the voicing up or down to the next voicingn: if set, the voicing is played like a scale. Overshooting numbers will be octaved
All of the above controls are optional, except chord.
If you pass a pattern of strings to voicing, they will be interpreted as chords.
@name voicing @tags tonal @returns Pattern @example n("0 1 2 3").chord("<C Am F G>").voicing()
203export const voicing = register('voicing', function (pat) { 204 return pat 205 .fmap((value) => { 206 // destructure voicing controls out 207 value = typeof value === 'string' ? { chord: value } : value; 208 let { dictionary = defaultDict, chord, anchor, offset, mode, n, octaves, ...rest } = value; 209 dictionary = 210 typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' }; 211 try { 212 let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n, octaves }); 213 return stack(...notes) 214 .note() 215 .set(rest); // rest does not include voicing controls anymore! 216 } catch (err) { 217 logger(`[voicing]: unknown chord "${chord}"`); 218 return silence; 219 } 220 }) 221 .outerJoin(); 222});
no symbol = major chord
232voicingAlias('^', '', [simple, complex]);
234Object.keys(simple).forEach((symbol) => { 235 // add aliases for "-" === "m" 236 if (symbol.includes('-')) { 237 let alias = symbol.replace('-', 'm'); 238 voicingAlias(symbol, alias, [complex, simple]); 239 } 240 // add aliases for "^" === "M" 241 if (symbol.includes('^')) { 242 let alias = symbol.replace('^', 'M'); 243 voicingAlias(symbol, alias, [complex, simple]); 244 } 245 // add aliases for "+" === "aug" 246 if (symbol.includes('+')) { 247 let alias = symbol.replace('+', 'aug'); 248 voicingAlias(symbol, alias, [complex, simple]); 249 } 250}); 251 252registerVoicings('ireal', simple); 253registerVoicings('ireal-ext', complex); 254 255export function resetVoicings() { 256 lastVoicing = undefined; 257 setDefaultVoicings('ireal'); 258}