Chapter 6 · Harmony as Context: Following the Current Chord

Chapter 5 ended on a promise: a pitch is a specification resolved late, and the richest thing to resolve it against is not a key but a living chord. This chapter makes good on it — we turn on Subsequence’s chord engine and write parts that follow wherever the harmony goes.

So far the “context” a pitch lands in has been static: one key, one scale, set once at the top. Now it moves. The composition grows a harmony underneath your patterns — a chord that changes every few beats — and any pattern can ask “what chord is sounding right now?” and build its notes from the answer. Write one bassline, and it walks a different root under every chord. Write one pad, and it re-voices itself bar by bar. This is the first time harmony enters the guide, and it rests entirely on the idea you already have: the same written part legitimately sounds different under different chords.

6.1 Turning on the chord engine (harmony())

A fresh Composition has no harmony — p.note(...) places exactly the pitches you write and nothing reacts to anything. You switch on the chord engine with one call, composition.harmony(...), after creating the composition:

import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120, key="E", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)

composition.render(bars=2, filename="harmony-on.mid")

That’s the whole switch. From now on the composition runs a harmonic clock alongside the beat clock: every cycle_beats beats it steps to a new chord, chosen by the style you named. The chords don’t make any sound on their own — the engine is a generator of context, not an instrument. Nothing is heard until a pattern reads the chord and plays something (that’s §6.2).

The two arguments here are the ones you’ll reach for first:

Argument

What it does

style="aeolian_minor"

The harmonic palette — which chords are likely, and how they tend to move from one to the next. "aeolian_minor" is natural-minor harmony with a dark cadence option; the full list is below.

cycle_beats=4

How many beats each chord lasts before the engine steps to the next one. 4 is one chord per bar in 4/4; 8 is one chord every two bars; 2 is two chords a bar. Defaults to 4.

Important

The chord engine needs a key. harmony() builds its chords relative to the composition’s key (the key="E" you set on the Composition), so calling it without a key raises a ValueError telling you to set one. A key is the home the whole harmony orbits — exactly the key/scale you met in Chapter 5, now doing double duty.

The built-in styles

style names a chord-transition palette — think of it as choosing the genre of the harmony before you write a note. Each shifts which chords appear and how they lead into each other. These are the built-in names:

Style name

Character

"functional_major" (alias "diatonic_major")

Standard major-key harmony — I, ii, iii, IV, V, vi. The default if you call harmony() with no style at all.

"hooktheory_major" (alias "pop_major")

Major harmony weighted toward common pop progressions.

"aeolian_minor"

Natural minor, with a Phrygian cadence option — dark, classic minor-key feel.

"phrygian_minor"

A darker, minimal minor palette (i–♭II–iv–v).

"dorian_minor"

Minor with a major IV — the soul/funk colour.

"lydian_major"

Bright and floating, with the raised-fourth (♯IV) colour.

"mixolydian"

Major with a flat 7th — open and unresolved (EDM, synthwave).

"chromatic_mediant"

Film-score third-relation shifts between distant major/minor chords.

"suspended"

An ambiguous sus2/sus4 palette — no decisive major or minor third.

"whole_tone"

A symmetrical augmented palette; dreamlike drift (IDM, ambient).

"diminished"

Minor-third symmetry; angular and disorienting (dark, experimental).

"turnaround"

A jazz ii–V–I turnaround, optionally modulating to the relative minor.

Tip

Pick a style the same way you picked a scale in Chapter 5: as a quick mood setting before any detail. "aeolian_minor" for brooding, "dorian_minor" for groovy-minor, "mixolydian" for an unresolved EDM lift, "functional_major" when you just want it to sound “correct.” An unknown name raises a ValueError listing the valid ones, so a typo fails loudly.

Reference

harmony()

6.2 Receiving the chord by parameter name (not a flag)

Here is the heart of the chapter, and the one idea to get exactly right. A normal pattern function takes a single argument, the builder p:

@composition.pattern(channel=6, beats=4)
def bass(p):
    ...

To make a pattern follow the harmony, you declare a second parameter named chord — and Subsequence injects the sounding chord into it on every cycle:

import subsequence
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120, key="E", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)

@composition.pattern(channel=6, beats=4)
def bass(p, chord):                       # ← the second parameter, named `chord`
    root = chord.root_note(notes.E2)      # the chord's root, in the bass register
    p.note(root, beat=0, duration=4.0)

composition.render(bars=4, filename="follow-chord.mid")

The name is the whole mechanism. Subsequence inspects your function’s parameters; if it sees one called chord, it passes the current chord in. If it doesn’t, your pattern simply never sees the harmony. There is no flag to set, no decorator argument to remember — you opt in by asking for it by name.

Important

The chord is injected by declaring a parameter named chord — there is no chord=True. Earlier drafts of Subsequence used a chord=True flag on the decorator; that is retired and will do nothing now. The current, only form is to add chord to your function’s parameter list:

def bass(p, chord): ✓ receives the live chord def bass(p): ✓ valid, but never sees the harmony

This mirrors how p itself arrives — you don’t request the builder with a flag, you name a parameter and it’s handed to you. The chord is the same: name it, and the engine fills it in fresh every cycle.

What chord actually is

The chord you receive is a small read-only object that knows the sounding chord and the composition’s key context, so the MIDI notes it hands back are already transposed correctly. You never construct it; you only ask it questions. Three methods cover almost everything:

Method

What it returns

chord.root_note(near)

One MIDI note: the chord’s root, in the octave nearest near. The single most useful call for a root-driven bass.

chord.bass_note(near, octave_offset=-1)

The root shifted down by whole octaves (one by default) — a bass register below the chord voicing.

chord.tones(root=near, count=N)

A list of MIDI notes: the chord’s tones, voiced from the octave nearest root. count= cycles tones up into higher octaves so you can ask for as many voices as you like.

Every one of these takes a reference MIDI note (near / root) and finds the chord tone in the octave closest to it. You’re saying “give me this chord, around here” — notes.E2 for a bass, notes.C4 for a mid pad. The chord supplies the which note; you supply the register.

Note

chord is the live counterpart of “a pitch resolved late.” In Chapter 5 a written degree resolved against a fixed key. Here chord.root_note(notes.E2) is a specification — “the root, near E2” — that resolves against whatever chord is sounding this cycle. Same call, different answer each time the harmony moves. The pattern function re-runs every cycle (the rebuild loop from Chapter 2), and each run gets the chord for that cycle, so the resolution happens fresh, on the beat.

6.3 Basslines from the chord

A bass that follows the harmony is the most immediate payoff, so let’s build one onto the running drum loop. We keep the channel-10 GM drums exactly as they’ve been since Chapter 0, and add a bass on its own channel that plays the root of whatever chord is sounding:

import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes

DRUMS_CHANNEL = 10
BASS_CHANNEL  = 6

composition = subsequence.Composition(bpm=120, key="E", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4, gravity=0.8)

@composition.pattern(channel=DRUMS_CHANNEL, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
    p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
    p.hit_steps("snare_1", [4, 12], velocity=100)
    p.hit_steps("hi_hat_closed", range(16), velocity=80)

@composition.pattern(channel=BASS_CHANNEL, beats=4)
def bass(p, chord):
    root = chord.root_note(notes.E2)            # the sounding chord's root, low
    p.sequence(steps=[0, 4, 8, 12], pitches=root)   # a note on every beat
    p.legato(0.9)                                # hold each almost to the next

composition.render(bars=8, filename="chord-bass.mid")

The drums know nothing about harmony — no chord parameter, so the engine never touches them. The bass declares chord, so every cycle it gets the current chord, takes its root near E2, and plays it four times. Across eight bars the engine walks through several chords, and the bass walks with it — a different root under each one, all of it in E minor because that’s the key the harmony lives in.

You can do more than thump the root. Because chord.tones(...) hands back a plain list of MIDI numbers — exactly like the scale_notes lists from §5.3 — you index into it to pick which chord tone to play. Here the bass alternates root and fifth, a classic walking feel, by reaching for elements [0] and [2] of the chord:

import subsequence
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120, key="E", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)

@composition.pattern(channel=6, beats=4)
def root_fifth_bass(p, chord):
    tones = chord.tones(root=notes.E2, count=3)   # [root, third, fifth] near E2
    p.note(tones[0], beat=0, duration=1.0)        # root
    p.note(tones[2], beat=1, duration=1.0)        # fifth
    p.note(tones[0], beat=2, duration=1.0)        # root
    p.note(tones[2], beat=3, duration=1.0)        # fifth

composition.render(bars=4, filename="root-fifth-bass.mid")

tones[0] is the root, tones[1] the third, tones[2] the fifth — the chord’s notes in order, just as scale_notes(...)[0] was the root of a scale. The difference is that this list is rebuilt every cycle from the current chord, so tones[2] is the fifth of an A-minor chord in one bar and the fifth of an F-major chord in the next, with no extra work from you.

Tip

bass_note is root_note an octave (or more) down. For a bass that sits well below a mid-register pad, chord.bass_note(notes.E3) gives you the root one octave under E3 in a single call, instead of computing root_note and subtracting 12 yourself. Pass octave_offset=-2 to drop it two octaves for a sub-bass.

6.4 Pads & arpeggios from chord tones

A bass takes one note from the chord; a pad takes them all, and an arpeggio takes them all but one at a time. Both come straight off chord.tones(...), but Subsequence gives you two verbs that take the chord itself and do the voicing for you — no need to pull the list out by hand.

p.chord(...) plays a chord as a held block. Hand it the chord parameter, a root register, and how many voices you want:

import subsequence
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120, key="E", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)

@composition.pattern(channel=2, beats=4)
def pad(p, chord):
    # Four voices of the current chord, near E3, ringing almost the whole bar.
    p.chord(chord, root=notes.E3, count=4, velocity=70, detached=0.25)

composition.render(bars=4, filename="chord-pad.mid")

p.chord(chord, root=notes.E3, count=4) voices four notes of the sounding chord from the octave nearest E3. velocity=70 keeps a pad soft, and detached=0.25 makes every voice release a quarter-beat before the next chord arrives — a tidy habit that stops one bar’s pad from bleeding into the next chord’s. The pad re-voices itself automatically: when the engine steps to a new chord next cycle, the rebuild runs again and p.chord lays down that chord’s tones.

p.arpeggio(...) takes the same chord and rolls its tones out one at a time instead of stacking them. It’s a one-word swap from p.chord:

import subsequence
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120, key="E", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)

@composition.pattern(channel=3, beats=4)
def arp(p, chord):
    # The same chord, but as a rising-then-falling sixteenth-note arpeggio.
    p.arpeggio(chord, root=notes.E4, count=4, spacing=0.25,
               velocity=85, direction="up_down")

composition.render(bars=4, filename="chord-arp.mid")

The arguments echo p.chord — same chord, same root, same count — plus two that shape the roll:

Argument

What it does

spacing=0.25

Beats between successive notes. 0.25 is a sixteenth note, so a four-voice arp covers a beat per pass and repeats across the bar.

direction="up_down"

The order the tones cycle: "up" (low to high), "down", "up_down" (ascend then descend, ping-pong), or "random".

Note

p.arpeggio also takes a plain list of pitches, not only a chord. Pass it a list — say a fragment of scale_notes(...) — and it arpeggiates exactly those notes; in that case you drop root/count (there’s no chord to voice). The chord form here is the harmony-following version: p.arpeggio(chord, root=..., count=...) re-voices off the live chord every cycle, just like p.chord.

Tip

p.chord, p.arpeggio, and p.strum all share the same (chord, root=, count=, inversion=) front end, so “play this chord as a block, a roll, or a strum” is a single verb swap with the same arguments. (A fourth verb, p.broken_chord, plays the tones in an order you spell out — it takes the same chord/root but an explicit order= list rather than a count=.) That’s the “learn one verb, predict the rest” pattern from §4.5 showing up again, this time for harmony.

6.5 “The same notes sound different under different chords”

This is the idea the whole chapter is built to land, and it’s worth seeing on the page. A musician knows it in their hands: hum “root, third, fifth” over a C chord and you get C–E–G; hum the same “root, third, fifth” over an A-minor chord and you get A–C–E. The instruction didn’t change. The chord did. The sound followed the chord.

Your chord parameter behaves exactly the same way. chord.tones(root=notes.C4, count=3) is one written instruction — “root, third, fifth, near C4” — and it produces different MIDI notes depending on which chord is asked. Here is that made literal: the identical line of code, run against two different chords, printing the pitches that come out.

import subsequence
import subsequence.constants.midi_notes as notes

# Two chords from the same key. (parse_chord turns a chord name into a chord
# object with the same .tones() the engine injects — handy for showing the point.)
for name in ("Am", "F"):
    chord = subsequence.parse_chord(name)
    voiced = chord.tones(root=notes.C4, count=3)   # SAME instruction both times
    print(name, "->", voiced)
Am -> [57, 60, 64]
F -> [65, 69, 72]

The written specification was byte-for-byte identical — chord.tones(root=notes.C4, count=3) both times. Under A minor it resolved to A–C–E (57, 60, 64); under F major it resolved to F–A–C (65, 69, 72). Same spec, different sound — because the tones were voiced against the chord late, at the moment the chord was known, not when you typed the call.

This is precisely the structural-vs-sonic distinction from §5.5, now with the chord as the context instead of the key:

Kind of “same”

What it means here

Structural equality

The two calls are written identically — same instruction, chord.tones(root= notes.C4, count=3). They are structurally the same.

Sonic equality

The two calls sound the same. They do not — A minor and F major resolve the instruction to different pitches.

That gap is the entire point of following the chord. You write one bass, one pad, one arpeggio — structurally a single part — and the moving harmony makes it sonically new under every chord, for free. The part that took ten lines to write plays a whole progression’s worth of music.

Note

Under the hood: why the chord object knows the right octave. Each chord carries a root pitch class (0–11, where C = 0) and a quality (major, minor, dominant 7th, …) — a pitch class, not a specific octave, exactly like a key. When you call chord.tones(root=notes.C4), the object finds the copy of its root pitch class nearest your reference note, then stacks the quality’s intervals on top. That’s why the same chord gives [48, 52, 55] near C3 and [60, 64, 67] near C4 — it resolves the abstract chord into your chosen register on demand. The injected chord adds one thing the bare chord lacks: the composition’s key context, so a key-relative engine chord transposes to the right absolute pitches. A chord, like a pitch, is a specification resolved late.

Reference

parse_chord()

6.6 Steering the engine (gravity, cycle_beats)

Left to its defaults, the engine wanders the style’s palette on its own. A few arguments to harmony() let you steer how it wanders — how restless it is, how fast it moves, how much it favours home. You’ve met cycle_beats; here are the rest you’ll actually reach for:

Argument

What it does

cycle_beats=4

Beats per chord. Lower it for busy, fast-changing harmony; raise it for long, static chords. 8 = one chord every two bars.

gravity=1.0

Key gravity, 0.01.0. High values pull the harmony back toward the home chord — it stays close to the tonic and resolves often. Low values let it roam further before coming home. Default 1.0.

nir_strength=0.5

Melodic inertia (Narmour expectation), 0.01.0. How strongly the engine prefers smooth, expected chord moves over surprising leaps — the same Narmour model scored on melody in §12.8. Default 0.5.

dominant_7th=True

Whether the engine may use V7 (dominant-seventh) chords. False keeps the harmony plainer, on triads. Default True.

root_diversity=0.4

Root-repetition damping, 0.01.0. Lower values discourage landing on the same root twice in a row, pushing the harmony to keep moving; 1.0 disables the nudge entirely. Default 0.4.

A worked example: a brooding, slow-moving minor harmony that rarely strays far from home — long chords (cycle_beats=8), strong pull to the tonic (gravity=0.9), and triads only (dominant_7th=False):

import subsequence
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(
    style="aeolian_minor",
    cycle_beats=8,        # one chord every two bars — slow, brooding
    gravity=0.9,          # stays close to the home chord
    dominant_7th=False,   # plain triads, no V7
)

@composition.pattern(channel=6, beats=8)
def slow_bass(p, chord):
    p.note(chord.bass_note(notes.A3), beat=0, duration=4.0)
    p.note(chord.bass_note(notes.A3), beat=4, duration=4.0)

composition.render(bars=8, filename="steered-harmony.mid")

Turn gravity down to 0.3 and the same engine roams much further from A minor before resolving; push cycle_beats to 2 and it changes chord twice a bar. These are mood dials — set them by ear, the way you’d set a filter cutoff, rather than calculating them.

Tip

You can re-call harmony() to change the steering live. Calling it again with new arguments updates the engine on the fly — the chord currently sounding finishes out, and the new settings take over from the next change. That’s how you’d open up the harmony (gravity down) for a bridge and pull it home again for the final chorus.

Asking the composition directly: current_chord()

Patterns receive the chord automatically, but sometimes you want to read the sounding chord from outside a pattern — to print it, log it, or drive your own logic. composition.current_chord() returns the chord at the playhead (or None if you never called harmony()):

import subsequence

# seed= makes the engine's chord walk reproducible (see §4.6), so this prints
# the same chord every run — handy for a doctested example.
composition = subsequence.Composition(bpm=120, key="C", scale="major", seed=42)
composition.harmony(style="functional_major", cycle_beats=4)

composition.render(bars=2, filename="read-chord.mid")
print("Sounding now:", composition.current_chord().name())
Sounding now: C

.name() prints a human-friendly label like C, Am, or G7 — the same method the injected chord offers, useful for sanity-checking what the engine is doing. (Without a seed= the engine still walks the same style, but picks a different specific path each run — that’s the point of a generative harmony.)

Note

A preview of voice leading. The pad in §6.4 re-voices off the current chord each bar, but by default it always voices in root position — which can make the top notes jump around as chords change. Registering the pattern with @composition.pattern(channel=2, beats=4, voice_leading=True) tells Subsequence to choose, each bar, the inversion of the chord that moves the fewest voices from the last one — smooth, connected part-writing instead of jumps. It’s a single flag and it works today; the full treatment of voicing and voice leading is Chapter 7.

Reference

current_chord()


You can now grow a living harmony under a piece, follow it from any pattern by declaring a chord parameter, and turn the sounding chord into a bassline, a pad, or an arpeggio — all resting on the one idea that one written part legitimately sounds different under different chords. So far the engine has chosen the chords for you. Next we take the wheel: in Chapter 7 we build progressions as values — writing exact chord sequences, voicing and voice-leading them, and placing them with p.progression and composition.chords.