Music Theory 2018-04-30

By Max Woerner Chase

In this post:

I was a little too ambitious, I think, in my previous ideas. I got a nice unified music theory guide that I don't need to click hyperlinks to get around in, so I'm going to try to teach my code the stuff this book is (re-)teaching me.

First up, beats. The code must have a concept of a common-time measure (other time signatures come later), and note durations to fill it with.

(... God dammit, this section is called "Meeting the Beat". Cluckin fassy, guys.)

Anyway, I'm going to do the following:

And not yet stuff like tuplets, which are confusing.

A note is going to consist of an exponent to apply to 2 (usually ranging down from 1, rarely lower than -5), an augmentation dot count (usually 0, sometimes 1, rarely 2 or more) and a "tied" boolean.

Okay, that's the plan, let's get started. I have my "tiny_music" project, which I've cloned from in preparation for making a cookie-cutter, but I don't need that yet. First, I confirm that the tests are all green. Next, I add a file. Before anything else, I rerun the tests. The docstyle test fails because the module is undocumented. Document it, and all green again.

Next, I add the class I described above. The file now looks like:

"""Data structures for representing note durations."""

import typing

class NoteDuration(typing.NamedTuple):
    """Class representing a single note as written."""

    duration_power: int = 0
    augmentation_dots: int = 0
    tied_to_next: bool = False

I run the tests, and, oh no!, two failures! One of them is because I missed properly configuring pylint. The too-few-public-methods lint can go die in a fire. The other is because this new module is never imported, and now that it has code in it, that's a problem.

... Hold on.


# ...

        # ...


The machine uprising has a tastefully understated beginning.

Anyway, it's time to enter the world of yak shaving. Step 1, generate a fresh pylintrc and check for discrepancies.

Got it. The greatest trick the devil ever pulled was allowing multi-line sequences to require a comma between items, but not mandatorily end with a comma, and say nothing if there's just this random indented text after the list. (It's like goto-fail, but for your config files!)

Anyway, back to green. Let's make some constants.

BREVE = NoteDuration(1)
WHOLE = NoteDuration()
HALF = NoteDuration(-1)
QUARTER = NoteDuration(-2)
EIGHTH = NoteDuration(-3)
SIXTEENTH = NoteDuration(-4)
THIRTY_SECOND = NoteDuration(-5)

DOTTED_HALF = NoteDuration(-1, 1)
DOTTED_QUARTER = NoteDuration(-2, 1)

Simple enough so far. The tests still all pass, because this code is run at import time.

Anyway, synthesis is a separate issue, but we still want to establish that the code "understands" these durations, in some sense. First, we need to be able to contain the notes in measures. Let's add another file for that, because I like small modules.

"""Data structures for representing measures."""

import fractions
import typing

from . import note

class Measure(typing.NamedTuple):
    """Class representing a measure as a collection of notes."""

    notes: typing.Tuple[note.NoteDuration, ...]

    # This does not account for beat division.
    length: fractions.Fraction = fractions.Fraction(1)

I add basic tests, and that all passes. Long-term, measures will use an ABC rather than an explicit reference to NoteDuration, but I don't need to write that yet.

Now, we need to confirm that measures cannot "overflow". So, let's write some tests, first containing various valid combinations of durations, then more that are invalid.

Well, I added that test, and then realized I'd need more features to implement it. If one failing test is good, two must be great! The necessary feature is the ability to determine the "beats as written" (that is, ignoring ties) of a note, given the number of beats in a whole note.

... Huh. Turns out I can't override the constructors or initializers of typing.NamedTuple to introduce stuff like validation logic. I'm willing to change the interface to accommodate this. Now, you can construct any measure you want, but maybe it will not be valid, which is why you want to check the is_valid property.

Now that we have the ability to represent a written measure in code, let's get the computer to analyze it. I don't have any synthesis capabilities set up, so I'll instead write functions that generate "spoken-out" renditions of the duration structure. Like, three measures of a whole note each would be "CLAP 2 3 4 CLAP 2 3 4 CLAP 2 3 4". First, this needs the ability to consider several measures in sequence.

I got really into porting examples into tests and getting everything working. I just want to state for the record that getting eighth notes to sort of work right was really obnoxious.

Next up, respecting ties when it comes to notating the claps. This is going to suck. I mean, it "shouldn't" be so bad, but the big reason I have ties treated the way they are so far is, they let a note duration cross measure boundaries, which is pretty important. Here's what I need: if a note is tied to a note within the measure, the correct course of action is to add its length to a local and continue. If a note is not tied, reset the local. If a note is tied at the end of a measure, process it normally and set a flag. When I see that flag, do not notate a clap, then unset the flag.

Let's start off simple, though. I made a test. The test fails. I honestly feel like working on other posts instead, so maybe I'm done with this for the week.

LATE CHANGE: except that, I realized that, for later extensions, I want to store whether a note is tied to the previous note, instead of the next. So it's a good thing I didn't start on that algorithm yet.

Next week, I'll try to get ties working properly, then move on to rests.