In a previous post, I wrote about a representation of musical notes that hard-coded a list of notes, and (some of) their enharmonic equivalents. While this approach worked for what we were trying to accomplish, I wasn’t very extensible.
For example, imagine we’re working with the Db scale (Db, Eb, F, Gb, Ab, Bb, and C). A I IV plagal cadence in that key would be spelled I (Db, F, Ab), IV (Gb, Bb, Db). But changing that to a I iv progression requires we flatten the third of the IV chord, while keeping its note name (B), so we’d be left with a Bbb.
Our current Note implementation doesn’t support this. Let’s fix that.
First, let’s simplify our module attributes and only define the set of natural notes for convenience:
@naturals [
{"C", 0},
{"D", 2},
{"E", 4},
{"F", 5},
{"G", 7},
{"A", 9},
{"B", 11}
]
The simplest case we can handle is creating a new note of length 1. We know that it will be a natural, so we can just look it up from our list:
def new(<<_::binary-size(1)>> = name) do
%__MODULE__{
name: name,
pitch_class:
@naturals
|> Enum.find_value(fn
{^name, pitch_class} -> pitch_class
_ -> nil
end)
}
end
The next case is handling a note with one or more trailing accidentals:
def new(<<natural_name::binary-size(1), accidentals::binary>> = name) do
...
end
First, we’re create our base note, without accidentals:
note = new(natural_name)
Next, let’s count the number of sharps and flats we’ve been given:
sharps = accidentals |> String.replace(~r/[^#]/, "") |> String.length()
flats = accidentals |> String.replace(~r/[^b]/, "") |> String.length()
Now all we have to do is modify the name of the note we constructed to include the (reduced) accidentals, and increment or decrement the pitch_class of the note by the offset given by the sharps and flats we added up earlier:
%{
note
| name: String.replace(name, ~r/#b|b#/, ""),
pitch_class: rem(note.pitch_class + 12 + sharps - flats, 12)
}
Note that the purpose of the + 12 and rem(..., 12) when modifying the pitch_class is to ensure that the pitch_class value stays between 0 and 11, wrapping in either direction when needed.
With those changes, we can happily work with any kind of note we can imagine:
assert [
%Core.Note{name: "Gb", pitch_class: 6},
%Core.Note{name: "Bbb", pitch_class: 9},
%Core.Note{name: "Db", pitch_class: 1}
] =
Core.Chord.get_notes(%Core.Chord{
root: Core.Note.new("Gb"),
quality: %Core.Quality{
name: "m",
degrees: ["1", "b3", "5"],
quality: [0, 3, 7]
}
})
assert %Core.Note{name: "Ab", pitch_class: 8} = Core.Note.new("Ab#b#bb#")
Fantastic!