1/* 2voicings.mjs - <short description TODO> 3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/tonal/voicings.mjs> 4This 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/>. 5*/ 6 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 12 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); 85// e.g. typeof setDefaultVoicings !== 'undefined' && setDefaultVoicings('legacy'); 86 87export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range); 88 89/** 90 * Adds a new custom voicing dictionary. 91 * 92 * @name addVoicings 93 * @tags tonal 94 * @memberof Pattern 95 * @param {string} name identifier for the voicing dictionary 96 * @param {Object} dictionary maps chord symbol to possible voicings 97 * @param {Array} range min, max note 98 * @returns Pattern 99 * @example 100 * addVoicings('cookie', { 101 * 7: ['3M 7m 9M 12P 15P', '7m 10M 13M 16M 19P'], 102 * '^7': ['3M 6M 9M 12P 14M', '7M 10M 13M 16M 19P'], 103 * m7: ['8P 11P 14m 17m 19P', '5P 8P 11P 14m 17m'], 104 * m7b5: ['3m 5d 8P 11P 14m', '5d 8P 11P 14m 17m'], 105 * o7: ['3m 6M 9M 11A 15P'], 106 * '7alt': ['3M 7m 10m 13m 15P'], 107 * '7#11': ['7m 10m 13m 15P 17m'], 108 * }, ['C3', 'C6']) 109 * "<C^7 A7 Dm7 G7>".voicings('cookie').note() 110 */ 111export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => { 112 Object.assign(voicingRegistry, { [name]: { dictionary, range } }); 113}; 114 115// new call signature 116export const registerVoicings = (name, dictionary, options = {}) => { 117 Object.assign(voicingRegistry, { [name]: { dictionary, ...options } }); 118}; 119 120const getVoicing = (chord, dictionaryName, lastVoicing) => { 121 const { dictionary, range } = voicingRegistry[dictionaryName]; 122 return dictionaryVoicing({ 123 chord, 124 dictionary, 125 range, 126 picker: minTopNoteDiff, 127 lastVoicing, 128 }); 129}; 130 131/** 132 * DEPRECATED: still works, but it is recommended you use .voicing instead (without s). 133 * Turns chord symbols into voicings, using the smoothest voice leading possible. 134 * Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings). 135 * 136 * @name voicings 137 * @tags tonal 138 * @memberof Pattern 139 * @param {string} dictionary which voicing dictionary to use. 140 * @returns Pattern 141 * @example 142 * stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note() 143 */ 144 145let lastVoicing; // this now has to be global until another solution is found :-/ 146// it used to be local to the voicings function at evaluation time 147// but since register will patternify by default, means that 148// the function is called over and over again, resetting the lastVoicing variables 149export const voicings = register('voicings', function (dictionary, pat) { 150 return pat 151 .fmap((value) => { 152 lastVoicing = getVoicing(value, dictionary, lastVoicing); 153 return stack(...lastVoicing); 154 }) 155 .outerJoin(); 156}); 157 158/** 159 * Maps the chords of the incoming pattern to root notes in the given octave. 160 * 161 * @name rootNotes 162 * @tags tonal 163 * @memberof Pattern 164 * @param {octave} octave octave to use 165 * @returns Pattern 166 * @example 167 * "<C^7 A7 Dm7 G7>".rootNotes(2).note() 168 */ 169export const rootNotes = register('rootNotes', function (octave, pat) { 170 return pat.fmap((value) => { 171 const chord = value.chord || value; 172 const root = chord.match(/^([a-gA-G][b#]?).*$/)[1]; 173 const note = root + octave; 174 return value.chord ? { note } : note; 175 }); 176}); 177 178/** 179 * Turns chord symbols into voicings. You can use the following control params: 180 * 181 * - `chord`: Note, followed by chord symbol, e.g. C Am G7 Bb^7 182 * - `dict`: voicing dictionary to use, falls back to default dictionary 183 * - `anchor`: the note that is used to align the chord 184 * - `mode`: how the voicing is aligned to the anchor 185 * - `below`: top note <= anchor 186 * - `duck`: top note <= anchor, anchor excluded 187 * - `above`: bottom note >= anchor 188 * - `root`: bottom note is the lowest root of the chord >= anchor 189 * - `oldabove` : old (buggy) behavior of above, kept for legacy reason 190 * - `oldroot` : old (buggy) behavior of root, kept for legacy reason 191 * - `offset`: whole number that shifts the voicing up or down to the next voicing 192 * - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved 193 * 194 * All of the above controls are optional, except `chord`. 195 * If you pass a pattern of strings to voicing, they will be interpreted as chords. 196 * 197 * @name voicing 198 * @tags tonal 199 * @returns Pattern 200 * @example 201 * n("0 1 2 3").chord("<C Am F G>").voicing() 202 */ 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}); 223 224export function voicingAlias(symbol, alias, setOrSets) { 225 setOrSets = !Array.isArray(setOrSets) ? [setOrSets] : setOrSets; 226 setOrSets.forEach((set) => { 227 set[alias] = set[symbol]; 228 }); 229} 230 231// no symbol = major chord 232voicingAlias('^', '', [simple, complex]); 233 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}