Chapter 8 — The Song Is a Data Structure

Part I built six instrument loaders behind one shared interface. Part II is about the band that plays them, and it starts where songs start: structure. The claim of this chapter is that a song’s arrangement — who plays when, what patterns they play, how the sections stack up — is best written not as code that does things but as data that means things: plain lists and dictionaries that a simple loop walks through. The payoff lands immediately and compounds all book long: data can be diffed, swept in a loop, and eventually generated — and “double the quiet section and cut verse two” becomes a two-line edit you can hear ninety seconds later.

Three layers, three lifetimes

Arrangement information changes at three different speeds, so we keep it in three separate structures:

Harmony: the chord table

CHORDS = [
    {"name": "Am", "bass": 45, "gtr": [57, 60, 64, 69], "pad": [57, 64]},
    {"name": "F",  "bass": 41, "gtr": [53, 57, 60, 65], "pad": [53, 60]},
    {"name": "G",  "bass": 43, "gtr": [55, 59, 62, 67], "pad": [55, 62]},
    {"name": "Em", "bass": 40, "gtr": [52, 55, 59, 64], "pad": [52, 59]},
]

(The numbers are MIDI note numbers — piano keys, where 60 is middle C and each step is one semitone.) Look at what this table encodes beyond “which chords”: a voicing per instrument. The guitar’s four notes sit mid-neck, where a guitarist’s hand would actually be. The pad gets only two notes — root and fifth — because a pad playing the chord’s third muddies the guitar playing the same third. The bass gets one note. All your knowledge about each instrument’s register lives here, written once.

The rest of the song asks only CHORDS[bar_number % 4] — the % (remainder) makes the four chords cycle forever, bar after bar.

Patterns: one bar of behavior

# Verse bass: relentless eighth notes on the chord's root note, with a
# little pickup into the next bar.  Each entry is:
#   (position_in_bar_in_beats, length_in_beats, pitch_offset_in_semitones)
BASS_DRIVE = []
for i in range(7):
    BASS_DRIVE.append((i * 0.5, 0.5, 0))     # seven eighth-notes on the root
BASS_DRIVE.append((3.5, 0.5, 7))             # last eighth jumps up a fifth

# Chorus bass: bouncing through the octave — the hook.
BASS_HOOK = [
    (0.0, 0.5, 0), (0.5, 0.5, 0), (1.0, 0.5, 12), (1.5, 0.5, 0),
    (2.0, 0.5, 10), (2.5, 0.5, 0), (3.0, 0.5, 7), (3.5, 0.5, 5),
]

The crucial trick is that patterns are relative: they say “the chord’s bass note, plus 7 semitones,” never “the note A.” One pattern therefore works over all four chords, and the verse writes itself for as many bars as needed. The guitar’s pattern is even simpler — a list of indexes into the chord’s guitar voicing ([0, 2, 1, 2, 3, 2, 1, 2] — a picking pattern across the chord shape).

Longer thoughts — the lead melody — add a bar coordinate: (bar, beat, length, note). At that scale the data is the melody, visible and editable note by note. When the melody’s third phrase felt crowded, the fix was deleting one tuple.

Form: the movements list

movements = [
    dict(n_bars=4,  bass="hook"),                      # bass opens alone
    dict(n_bars=4,  bass="drive", drums=True),         # the machine locks in
    dict(n_bars=16, bass="drive", drums=True, arp="muted"),   # verse 1
    dict(n_bars=16, bass="hook",  drums=True, arp="open",
         pad=True, lead=True),                         # chorus 1
    dict(n_bars=8,  bass="drive", drums=True, arp="muted"),   # verse 2
    dict(n_bars=16, bass="hook",  drums=True, arp="open",
         pad=True, lead=True),                         # chorus 2
    dict(n_bars=8,  arp="open", pad=True),             # the quiet breakdown
    dict(n_bars=16, bass="hook",  drums=True, arp="open",
         pad=True, lead=True),                         # final chorus
    dict(n_bars=8,  bass="drive", drums=True),         # outro
]

Each movement is a cast list: which band members play, and in what mode. The values pull double duty as pattern selectors — bass="drive" versus bass="hook", guitar "muted" versus "open" — so a section’s character and its lineup live in one dictionary.

Read the list top to bottom and you can see the song: the lone bass intro, the drum machine entering, verse/chorus alternation, the breakdown where the rhythm section drops out and the guitar hangs in the reverb, the full final statement, the outro that returns to the opening image. Arrangement craft — tension, release, symmetry — has become list-shaped, and that’s the chapter’s whole point: in a DAW, structure is implicit in a thousand clip positions; here it’s explicit in twenty lines you can read.

The walker

The code that turns the data into Chapter 2 note events is deliberately boring:

bar_cursor = 0                             # running position, in bars
for m in movements:
    for bar_i in range(m["n_bars"]):
        bar_time = (bar_cursor + bar_i) * BAR          # seconds
        chord = CHORDS[bar_i % 4]
        write_bar(bar_time, chord,
                  bass=m.get("bass"),           # .get returns None/False
                  drums=m.get("drums", False),  # if the key is absent —
                  arp=m.get("arp", False),      # absent means "sits out"
                  pad=m.get("pad", False))
    if m.get("lead"):
        write_lead(bar_cursor, m["n_bars"])
    bar_cursor += m["n_bars"]

write_bar dispatches to small per-instrument functions that walk a pattern and emit notes. Every interesting decision already happened — in the data. When something’s wrong with the song, you debug a list, not a timeline.

One deliberate subtlety: bar_cursor accumulates as it goes, so no section knows its absolute position in the song. That’s what makes structural surgery free: insert a movement, delete one, or duplicate the breakdown with movements[6:7] * 2 (Python list-repeat), and everything downstream re-flows automatically.

When one song becomes twelve

At album scale, the movements list graduates into a named template. The synth album’s writer names its section types and lets each song’s spec pick a sequence:

sections = ["intro", "A", "A2", "breath", "B", "A3",
            "breath", "B2", "A4", "outro"]

A sections carry the song’s hook at rising intensity (A2 adds the arpeggio and percussion, A3 adds a bell doubling the melody an octave up); B sections shift to a second chord progression; "breath" is two bars of pads alone — the exhale between statements. Twelve songs share this skeleton with different keys, tempos, melodies, and drum machines, and the album coheres the way a band’s album coheres: same structural vocabulary, different songs. Building that engine is Part III’s case study; the point here is that it’s the same three-layer idea, one level up.

Practical notes

The arrangement now specifies everything — and if you render it as written, it sounds programmed, because every note lands exactly where the data says. The next chapter is about ruining that perfection. Carefully. By a few milliseconds at a time.