Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voicing Package #223

Closed
felixroos opened this issue Nov 11, 2020 · 4 comments
Closed

Voicing Package #223

felixroos opened this issue Nov 11, 2020 · 4 comments

Comments

@felixroos
Copy link
Contributor

felixroos commented Nov 11, 2020

As mentioned here, it would be cool if tonal could handle voicings. I am stoked that you offered me to contribute :)

I have written about most of my ideas here:

For the start, I would just concentrate on generating voicings from dictionaries, as it is way simpler and I am mostly done implementing it.

UPDATE: further changes in #224

I'll make a rough proposal by writing a pseudo doc + some test for methods that could be available:

VoicingDictionary

export declare type VoicingDictionary = {
  [symbol: string]: string[];
};

Maps a chord symbol to a set of voicings (interval string). The Voicings package could provide a set of common voicings. Could this also be a seperate package?!

export const triads: VoicingDictionary = {
  M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
  m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
  o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
  aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};

export const lefthand: VoicingDictionary = {
  m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
  '7': ['3M 6M 7m 9M', '7m 9M 10M 13M'],
  '^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
  '69': ['3M 5P 6A 9M'],
  m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
  '7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
  '7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
  o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
  '7#11': ['7m 9M 11A 13A'],
  '7#9': ['3M 7m 9A'],
  mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
  m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};

Maybe this could also be in array format.

Voicing.search

export declare function search(chord: string, range?: string[], dictionary?: VoicingDictionary): string[][];

This method returns all possible voicings of the given chord, as defined in the dictionary, inside the given range:

test('C major triad inversions', () => {
    // if no range + dictionary given, use defaults (dictionray is this case triads, but the real default should contain all chords)
    expect(Voicing.search('C')).toEqual([
      ['C3', 'E3', 'G3'],
      ['C4', 'E4', 'G4'],
      ['E3', 'G3', 'C4'],
      ['E4', 'G4', 'C5'],
      ['G3', 'C4', 'E4'],
    ])
  })
});
// here, we override range and dictionary
test('C^7 lefthand', () => {
  expect(Voicing.search('C^7', ['E3', 'D5'], lefthand)).toEqual([
    ['E3', 'G3', 'B3', 'D4'],
    ['E4', 'G4', 'B4', 'D5'],
    ['B3', 'D4', 'E4', 'G4'],
  ])
})
// this shows that even symbols that are not part of chord-type could be used, as long as they are present in the dictionary
test('Cminor7 lefthand', () => {
  expect(Voicing.search('Cminor7', ['E3', 'D5'], { 'minor7': ['3m 5P 7m 9M', '7m 9M 10m 12P'] })).toEqual([
    ['Eb3', 'G3', 'Bb3', 'D4'],
    ['Eb4', 'G4', 'Bb4', 'D5'],
    ['Bb3', 'D4', 'Eb4', 'G4'],
  ])
})

changes:

  • renamed from inRange to search
  • changed param order (dictionary last, as most probable to be left default)
  • optional range + dictionary

Voicing.get

export declare function get(
  chord: string,
  range?: string[],
  dictionary?: VoicingDictionary,
  voiceLeading?: VoiceLeading,
  lastVoicing?: string[]
): string[];

This method returns the best voicing for chord after the optional lastVoicing, using voiceLeading. Internally calls Voicing.search to generate the available voicings.

test('getBestVoicing', () => {
  // all default => pretty useless but
  expect(Voicing.get('Dm7')).toEqual(['F3', 'A3', 'C4', 'E4']);
  // without lastVoicing
  expect(Voicing.get('Dm7', ['F3', 'A4'], lefthand, topNoteDiff)).toEqual(['F3', 'A3', 'C4', 'E4']);
  // with lastVoicing
  expect(Voicing.get('Dm7', ['F3', 'A4'], lefthand, topNoteDiff, ['C4', 'E4', 'G4', 'B4'])).toEqual([
    'C4',
    'E4',
    'F4',
    'A4',
  ]);
});

changes:

  • swap range & dictionary (like in Voicing.search)
  • be able to use defaults (optional params)

VoiceLeading

export declare type VoiceLeading = (voicings: string[][], lastVoicing?: string[]) => string[];

A function that decides which of a set of voicings is picked as a follow up to lastVoicing.

expect(
  topNoteDiff(
    [
      ['F3', 'A3', 'C4', 'E4'],
      ['C4', 'E4', 'F4', 'A4'],
    ],
    ['C4', 'E4', 'G4', 'B4']
  )
).toEqual(['C4', 'E4', 'F4', 'A4']);

The lib could include some common voice leading strategies:

  • pick voicing with minimal top note difference (topNoteDiff)
  • pick voicing with minimal midi average difference (midiAverageDiff)
  • pick voicing with minimal total movement of voices (minimalVoiceMovement)

changes:

  • swap lastVoicing & voicings + lastVoicing can be optional

VoicingDictionary.lookup

export declare function lookup(chordSymbol: string, dictionary: VoicingDictionary);

Get possible interval sets for given chord in given dictionary:

expect(VoicingDictionary.lookup('M7', lefthand)).toEqual([['3M 5P 7M 9M', '7M 9M 10M 12P']]);
// could also be used with chord symbol (ignore root)
expect(VoicingDictionary.lookup('CM7', lefthand)).toEqual([['3M 5P 7M 9M', '7M 9M 10M 12P']]);

Note that it works, even if the chord symbol "M7" is just an alias of the "^7" symbol used in the dictionary.

changes:

  • renamed from searchSets to lookup

Optional: Voicing.analyze

export declare function analyze(
  voicing: string[]
): {
  topNote: string;
  bottomNote: string;
  midiAverage: number;
};

Returns some useful info on the given voicing:

expect(Voicing.analyze(['C4', 'E4', 'G4', 'B4'])).toEqual({
  topNote: 'B4',
  bottomNote: 'C4',
  midiAverage: 85.4, // did not check :)
  // many more values possible
});

Optional: Voicing.analyzeTransition

export declare function analyzeTransition(from: string[], to: string[]): {
  topNoteDiff: number,
  bottomNoteDiff: number,
  movement: number
}

Returns some useful info on the given voice transition

expect(Voicing.analyzeTransition(['C4', 'E4', 'G4', 'B4'],  ['D4', 'F4', 'A4', 'C5'])).toEqual({
topNoteDiff: 1,
bottomNoteDiff: 2,
movement: 5
})

Could also use intervals instead of semitones (but semitones are easier to compare)

Optional: Voicing.searchSets

export declare function searchSets(intervalSets: string[][], range: string[], root: string);

Renders all sets of notes that represent any of the interval sets inside the given range, relative to the root:

expect(
  Voicing.searchSets(
    [
      ['1P', '3M', '5P'],
      ['3M', '5P', '8P'],
    ],
    ['C3', 'G4'],
    'C'
  )
).toEqual([
  ['C3', 'E3', 'G3'],
  ['E3', 'G3', 'C4'],
  ['C4', 'E4', 'G4'],
]);

changes:

  • renamed to searchSets (similar to Voicing.search)

Note.enharmonicEquivalent

export declare function enharmonicEquivalent(note: string, pitchClass: string): string;

For my implementation of Voicing.get to work, I would also need a helper function that returns enharmonic equivalents. As this is a rather general purpose method, I would propose this as part of Note:

test('enharmonicEquivalent', () => {
  expect(Note.enharmonicEquivalent('F2', 'E#')).toBe('E#2');
  expect(Note.enharmonicEquivalent('B2', 'Cb')).toBe('Cb3');
  expect(Note.enharmonicEquivalent('C2', 'B#')).toBe('B#1');
});

edit: this feature is now covered by Note.enharmonic


That's it for a start. What do you think? UPDATE: Of course, I am open to suggestions of any sort!

UPDATE: changed some method names + param orderings (see changes under each section)

@felixroos
Copy link
Contributor Author

Example usage for voicing multiple chords:

function sequence(chords, range?, dictionary?, voiceLeading?, lastVoicing?) {
  return chords.reduce(
    ({ voicings, lastVoicing }, chord) => {
      lastVoicing = Voicing.get(chord, range, dictionary, topNoteDiff, lastVoicing);
      voicings.push(lastVoicing);
      return { voicings, lastVoicing };
    },
    { voicings: [], lastVoicing }
  ).voicings;
}

const voicings = sequence(['C', 'F', 'G'], ['F3', 'A4'], triads, VoiceLeading.topNoteDiff);
/* [
  [ 'C4', 'E4', 'G4' ], // root position
  [ 'A3', 'C4', 'F4' ], // first inversion (F4 closest to G4)
  [ 'B3', 'D4', 'G4' ] // first inversion (G4 closest to F4)
] */

Maybe, sequence could also be a part of Voicing itself...

@danigb
Copy link
Collaborator

danigb commented Nov 12, 2020

Hi @felixroos

I am stoked with your contribution ;-)

I did a quick read, but I think I'll need some time to digest and understand this well.

Anyway, some questions/thoughts arose in this first read:

  • What lefthand exactly means? (sorry if it's a dumb question)
  • Some voicings, like the triads one, can be relatively easy to generate from the chord itself. I'm wondering if it would be possible to generate voicings instead of having a dictionary. Or more specifically: to have a dictionary of meta-voicings, how to distribute a given chord into a voicing. Some thing we can apply to the chords to generate all possible?/common? combinations.
  • I think Voicing.search and Voincing.get are not named consistenly with the rest of the library. I thing search should be namedget (and probably the opposite :-D). So something to discuss in Voicing Package #224
  • I think it's probably to better separate those things in different modules: voicing-dictionary, voicing, voice-leading, voicing-analyze (not sure about organisation or names, just an idea)

@felixroos
Copy link
Contributor Author

What lefthand exactly means?

From my understanding, lefthand voicings are piano voicings that can be played with one hand, which is mostly the left one as the right is commonly used to play a melody above. But you can also play them with the right hand and use the left for a bassline. Also, they rarely contain the root note, as the root is often played by a bass instrument (or the left hand if the right hand plays the "lefthand" voicing). Sometimes they are also called rootless voicings, but I think the term "rootless" is a little more open, where lefthand are mostly a relatively small set of voicings that are first taught to jazz piano beginners.

Some voicings, like the triads one, can be relatively easy to generate from the chord itself. I'm wondering if it would be possible to generate voicings instead of having a dictionary. Or more specifically: to have a dictionary of meta-voicings, how to distribute a given chord into a voicing. Some thing we can apply to the chords to generate all possible?/common? combinations.

Yeah, that was exactly my intention here. But this is a rather complicated topic.. Generally, a chord has a sort of hierarchy of note importance. As a rule of thumb the 3 and 7 are essential, while the rest is optional. Also, you can play as many or as little notes as you like. Plus, there are many "rules" to be aware of, like lower interval limits.. After a LOT of experimentation with the topic, I found that using a "combinatorial search" with flexible rules is a pretty good solution to generate any voicing.

I think Voicing.search and Voincing.get are not named consistenly with the rest of the library. I thing search should be namedget (and probably the opposite :-D). So something to discuss in #224

Yes, it's kind of inconsistent. I found it difficult to name the "search" method, as it starts with Voicing.* but the result contains mutiple VoicingS...

I think it's probably to better separate those things in different modules: voicing-dictionary, voicing, voice-leading, voicing-analyze (not sure about organisation or names, just an idea)

That could be done. On the other hand, I think there are not that many voice-leading algorithms out there. Also, I think the aim for voicing-dictionary should'nt be completeness, as this is just impossible...

@felixroos
Copy link
Contributor Author

closing this, as #224 exists

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants