Music Theory in Elm

April 13, 2019 ยท 5 minute read

Lately I have been working on a music theory learning app. The app combines short video tutorials with music theory exercises, think Khan Academy combined with Duo Lingo.

In this post I am going to discuss my previous work with music computation libraries. I then discuss how Elm solves some of the problems I've had with my previous approaches. Using Elm's type system I can leverage the compiler as well as make more readable code. I finish by discussing how I generate random notes and chords using Elm's random package.

A Theory Engine

Several things needed to be in place before I could write an application around music theory. I needed a music theory engine. The engine would allow me to ask things like, "What is a major 6th above a given note?" or "What are the notes of an A major 7th chord in 1st inversion?"

I also needed a rendering engine. There are several other projects that render music in the browser, like VexFlow, but those projects were too big for what I was trying to do. I did not need to render a whole sheet of music to the browser. I only needed some intervals, chords, key signatures and such.

I first experimented rendering music in the browser using Elm's SVG package. It worked better than I expected. The other music rendering libraries usually support SVG and canvas. Being able to stick to SVG made things simpler.

Once I had a theory engine and a rendering engine I could do something like this (click the button):

Prior Work

Several years ago I had forked a previous Python project Mingus. I had augmented the project to use theory in a more semantic way. For example, in Mingus, there were no perfect intervals only major and minor. In actual music theory there is no such thing as a major 5th, only perfect, diminished, and augmented 5ths. I worked at it for a couple of weeks but was not happy with the result.

Compared to Python, the modeling of the domain as types in Elm is much more readable. Most music theory APIs make use of parsing strings to describe things, like Music.note('a4') or Music.note('a4').chord('maj7'). In Elm I can describe notes, chords, inversion, etc. all in types like getSeventhChord (Note A 4 Natural) Maj7.

Designing With Elm Types

I dabbled into Elm before starting this project. I got confused with routing in Elm 0.18, but the routing module of Elm 0.19 was easier to use. With the release of Elm 0.19, I decided to try to write a single page application (SPA) in Elm.

I started with the theory engine. Throughout the whole process, I wrote unit tests with Elm Test. When I finished I had something that just worked. I wrote the engine several months ago and have not had to touch the core logic since. Drawing inspiration from Making Impossible States Impossible I modeled everything in Types. Coming from imperative languages this was a new thing for me. My Note looked like this:

type alias Note =
    { name : NoteName, octave : Int, accidental : Accidental }

type NoteName
    = C
    | D
    | E
    | F
    | G
    | A
    | B

type Accidental
    = DoubleSharp
    | Sharp
    | Natural
    | None
    | Flat
    | DoubleFlat

In the render engine I use case matching to render the accidentals:

renderAccidental : Accidental -> Svg msg
renderAccidental accidental =
    case accidental of
        DoubleSharp ->
            svg [...] [...]
        
        Sharp ->
            svg [...] [...]
        
        ...

Generating Random Notes

The flow of doing random stuff in Elm is much different than JavaScript. You ask the Elm runtime to perform a Command. Once you get used to it, the mindset is really powerful.

This way of generating random things is especially useful for doing music exercises. When generating music stuff there is often an order of operations. I need one random thing before I can compute the next random thing. For example:

view : Model -> Html Msg
view model =
  ...
  button
    [ onClick GetRandomClef ]
    [ text "Next Note" ]

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        GetRandomClef ->
            ( model
            , Random.generate
                NewClef
                    <| Random.Array.sample
                    <| Array.fromList model.clefPool
            )

        NewClef clef ->
            let
                possibleNotes = notesForClef clef
            in
            ( { model | clef = clef }
            , Random.generate
                NewNote
                    <| Random.Array.sample
                    <| Array.fromList possibleNotes
            )

        NewNote note ->
            ( { model | note = note }, Cmd.none )

Random Chords

The order of precedence for generating a random 7th chord would be:

In order to get the root of the chord, the clef and the inversion must be known first, otherwise the chord might render off the page.

Clef Range Issue
Clef Range Issue

So the actual range of the clef needs to be reduced by the distance from the bottom note.

Actual Clef Range
Actual Clef Range
Random.generate
    NewNote
        <| Random.Array.sample
        <| Array.fromList
        <| possibleNotes model.clef inversion

Conclusion

Writing a music app in Elm has been delightful. Coming from JavaScript, Python, and other C-like languages the syntax may seem strange. After a few hours, this strangeness wears off. The type system also can make for some very readable code. It is nice to see that new languages, like Rust, are supporting types and pattern matching.