Music Theory 2018-05-28
In this post:
- I come up with a perhaps too-elaborate analysis of how to represent the intensity of accents in music, and figure out what that means for BeatStructures.
- I edit the BeatStructure code to account for that, then integrate it into the rest of the code.
- I don't actually write analysis using the new concepts.
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.