jevstrudel.git / packages / tonal / voicings.mjs

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()

111export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => {
112  Object.assign(voicingRegistry, { [name]: { dictionary, range } });
113};

new call signature

116export const registerVoicings = (name, dictionary, options = {}) => {
117  Object.assign(voicingRegistry, { [name]: { dictionary, ...options } });
118};
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};

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

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});

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()

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});

Turns chord symbols into voicings. You can use the following control params:

  • chord: Note, followed by chord symbol, e.g. C Am G7 Bb^7
  • dict: voicing dictionary to use, falls back to default dictionary
  • anchor: the note that is used to align the chord
  • mode: how the voicing is aligned to the anchor
    • below: top note <= anchor
    • duck: top note <= anchor, anchor excluded
    • above: bottom note >= anchor
    • root: bottom note is the lowest root of the chord >= anchor
    • oldabove : old (buggy) behavior of above, kept for legacy reason
    • oldroot : old (buggy) behavior of root, kept for legacy reason
  • offset: whole number that shifts the voicing up or down to the next voicing
  • n: 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});
224export function voicingAlias(symbol, alias, setOrSets) {
225  setOrSets = !Array.isArray(setOrSets) ? [setOrSets] : setOrSets;
226  setOrSets.forEach((set) => {
227    set[alias] = set[symbol];
228  });
229}

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}