Written by Pete Corey on Dec 17, 2018.

Currently my Elixir-powered Chord project does a lot of really cool things. It can generate a huge number of guitar chords given a set of notes you want included in those chords. It also computes the voice leading distance and fingering distance between those chords, which let’s us map out “ideal” chord progressions.

While this functionality is awesome in and of itself, it’s missing a few key features that would bring it to the next level.

Traditionally, musicians quickly learn song with the help of lead sheets. A lead sheet consists of a melody laid out over a set of chords. It’s up to the musician to interpret and play those chords with the given melody in a way that makes sense for both the player and the listener.

An example lead sheet.

I want Chord to be able to generate possible interpretations of a lead sheet by giving us chord progressions that include optional notes and specific melody notes.

Supporting Optional Notes

It may be surprising to hear, but often times many of the notes that make up a chord are entirely optional!

For example, if I’m playing a Cmaj7 chord, which is made up of the root of the chord, the third, the fifth, and the major seventh, it’s usually acceptable to omit the fifth of the chord. The fifth usually just serves to add harmonic stability to the root note, and isn’t necessary to convey the color of the chord to the listener.

The ability to mark a note as optional drastically expands the possible set of chords we can generate for a given set of notes. For each optional note, we need to generate all of the possible chords that include that note, all of the possible chords that do not include it, and merge the results together.

Let’s update our Chord.Voicing module to do that now.

Within our Chord.Voicing module is a function, all_note_sets/1 that takes a set of notes, and returns a list of all possible “note sets” that can be spread across the strings of the guitar to build chords.

A note set is really just a collection of notes we want to play. For example, if we’re trying to play a Cmaj7 with an optional fifth, some of our note sets might look like this:


[[0, 4, 11],            # root, third, seventh
 [0, 4, 11, 0],         # root, third, seventh, root
 [0, 4, 11, 4],         # root, third, seventh, third
 [0, 4, 11, 11],        # root, third, seventh, seventh
 [0, 4, 11, 7],         # root, third, seventh, fifth
 [0, 4, 11, 7, 0],      # root, third, seventh, fifth, root
 ...
 [0, 4, 7, 11, 11, 11]] # root third, seventh, seventh, seventh, seventh

Notice that the smallest note set is the set of all three required notes. Also note that after those first three required notes is every possible permutation of every possible note in the chord, required and optional notes included.

We can implement this fairly easily in our all_note_sets/1 function. Let’s start by filtering the provided set of notes down to just the required notes:


required_notes =
  Enum.filter(notes, fn
    {:optional, note} -> false
    _ -> true
  end)

We’ll assume that optional notes are keyword tuples with :optional as the first element and the actual note as the second value. Require notes are simply bare note values.

Next, let’s filter notes down to the list of just optional notes:


optional_notes =
  Enum.filter(notes, fn
    {:optional, note} -> true
    _ -> false
  end)

Finally, let’s get a list together of all possible notes, optional and required included:


all_notes =
  Enum.map(notes, fn
    {_, note} -> note
    note -> note
  end)

Now that we’ve put our ducks in a row, generating all of our possible note sets if fairly straight forward.

We know that every note set will start with our set of required notes. That means that the length of each note set will range in length from the length of the required notes to 6, the number of strings on a guitar:


length(required_notes)..6

We also know that after the set of required notes the remaining space in the note set will be filled by every permutation of all possible notes (allowing repetition):


Permutation.generate(all_notes, length - length(required_notes), true)

We can loop over each of these sets of values and combine the results in a list comprehension to come up with our final list of note sets:


for length <- length(required_notes)..6,
    tail <- Permutation.generate(all_notes, length - length(required_notes), true) do
  required_notes ++ tail
end

Supporting Exact Pitches

Once we’ve built our note sets, we need to translate them into actual chords. Our Chord.Voicing module does this with the help of the all_notes/3 function, which takes a single note from our note set and finds all possible locations on the fretboard where that note can be played.

As we talked about in a previous article, it does this by building a complete fretboard and then filtering out, or sieving, any notes on the fretboard that aren’t the note we’re trying to play.

The original code that decided if the provided target_note matched the note at the given fret (index) and string looked something like this:


if rem(note, 12) == target_note do
  {string, index}
else
  nil
end

If the pitch class of the note (rem(note, 12)) matches our target_note, add the current string and fret to the list of tuples to be returned by our all_notes/3 function.

This solution assumes that all of the notes in our note sets are pitch classes, or values between 0 and 11. If we’re looking for a C and our target_note is 0, it will match on any octave of C it finds across the fretboard.

We can modify this solution to support exact pitches with minimal effort. If we assume that exact pitches will be passed in through the target_note parameter just like pitch classes (as plain numbers), we can add a fallback check to our condition that checks for exact equality:


cond do
  rem(note, 12) == target_note -> {string, index}
  note == target_note -> {string, index}
  true -> nil
end

If the pitch class of the current note doesn’t match our target_note, the untouched value of note still might. For example, if we’re looking specifically for a middle C (60), this condition would match on only those exact pitches, and not any higher or lower octaves of C.

Final Thoughts

Our Chord.Voicing module now supports building chords out of note sets that include both optional notes and exact pitches. We’re one step closer to modeling lead sheets!

As an interesting aside, when I started this refactor, I noticed that the original implementation of all_note_sets/1 was completely wrong. I’m not sure what was going through my mind when I wrote that first version, but it was only returning a small subset of all possible note sets. Equipped with the new implementation, Chord is generating many times the number of possible chords for us to play with.

Be sure to check out the entire Chord project on Github, and stay tuned for more updates and experiments.