jevstrudel.git / packages / tonal / voicings.mjs
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}