Music Theory 2018-05-28

By Max Woerner Chase

In this post:


I spent some time thinking about what the beat structure concept means in terms of terminology and the rest of the code. Here's my first stab at laying it out:

A beat structure consists of a base duration and a sequence of beat lengths. The sum of all the beat lengths is the count. Taken as a whole, each beat length has an on_beat at the beginning of it, and the remaining beats within the length are primary off_beats, while a note that starts completely unaligned is a secondary off_beat. The first on_beat in the structure is a primary on_beat, there are one or two secondary on_beats, and the remainder are tertiary on_beats.

This requires a nested structure to pull off. 4/4 time gets ((1, 1), (1, 1)) (primary, tertiary, secondary, tertiary), while 6/8 gets ((3,), (3,)) (primary, off, off, secondary, off, off), and 12/8 gets ((3, 3), (3, 3)), and I think 2/2 should get ((1,), (1,))

The choice of structure determines the natural placement and intensities of accents.

Let's get that put together. Step one, create an enum.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"""Data structures for representing beat types."""

import enum


class BeatType(enum.Enum):
    """Enum of beat values."""

    PRIMARY_ON_BEAT = 1
    SECONDARY_ON_BEAT = 2
    TERTIARY_ON_BEAT = 3
    PRIMARY_OFF_BEAT = 4
    SECONDARY_OFF_BEAT = 5

Step two, realize that I jumped the gun defining BeatStructures last week.

I've changed it up to this, which mostly makes sense for the time signatures I care about, I think.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"""Data structures for representing durations."""

import typing

from . import duration


def beat_breakdown(parts: int, length: int) -> typing.Tuple[
        typing.Tuple[int, ...], ...]:
    """Break down the given number of beat lengths of the given length."""
    if parts == 3:
        return ((length,), (length,), (length,))
    if parts % 2:
        raise ValueError(f'Cannot break down into {parts} parts')
    return ((length,) * (parts // 2),) * 2


def is_compound(top: int) -> bool:
    """Return whether input is the top of a compound time signature."""
    return not (top == 3 or top % 3)


class BeatStructure(typing.NamedTuple):
    """Wrapper type representing a duration as a fraction of a whole note."""

    base_duration: duration.Duration = (
        duration.Duration.from_fraction(1, 4))  # pylint: disable=no-member
    durations: typing.Tuple[typing.Tuple[int, ...], ...] = ((1, 1), (1, 1))

    @classmethod
    def simple(cls, top: int, bottom: int):
        """Return the beat structure of a simple time signature."""
        if is_compound(top):
            raise ValueError(f'{top} is compound')
        return cls(
            duration.Duration.from_fraction(  # pylint: disable=no-member
                1, bottom), beat_breakdown(top, 1))

    @classmethod
    def compound(cls, top: int, bottom: int):
        """Return the beat structure of a compound time signature."""
        if not is_compound(top):
            raise ValueError(f'{top} is simple')
        return cls(
            duration.Duration.from_fraction(  # pylint: disable=no-member
                1, bottom), beat_breakdown(top // 3, 3))

For the current analysis, I actually... slightly... don't need most of this. I'm going to add a "count" property, and then most of this gets slotted into existing code without much inspection.

@property
def count(self) -> int:
    """Return the number of beats in the structure."""
    return sum(map(sum, self.durations))

It's a little golf-y, but there's an excellent reason: I didn't feel like typing out more code.

Anyway, I could work out the exact sequence of changes required to propagate the change from the "time signature" tuple to using BeatStructure, ooooor, hear me out on this, I could just replace it, and figure out after the fact what I just broke.

Okay, I got it all done. I had to rename the property above to beat_count, because I forgot that methods don't namespace in Python. I then had to rearrange things aggressively to fit in disables to compensate for a pylint bug. Looking into the stuff around that bug, I see that there have been some new releases of various libraries. I'll have to look into taking those later.

At some point, I should code in the analysis according to the ideas at the top of this post, but I can get away with skipping that for now. At the moment, I'm just glad to have finished this up.


Next week, I start looking into representing pitches.