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 |
|---|---|
|
The harmonic palette — which chords are likely, and how they tend to move
from one to the next. |
|
How many beats each chord lasts before the engine steps to the next one.
|
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 |
|---|---|
|
Standard major-key harmony — I, ii, iii, IV, V, vi. The default if you call
|
|
Major harmony weighted toward common pop progressions. |
|
Natural minor, with a Phrygian cadence option — dark, classic minor-key feel. |
|
A darker, minimal minor palette (i–♭II–iv–v). |
|
Minor with a major IV — the soul/funk colour. |
|
Bright and floating, with the raised-fourth (♯IV) colour. |
|
Major with a flat 7th — open and unresolved (EDM, synthwave). |
|
Film-score third-relation shifts between distant major/minor chords. |
|
An ambiguous sus2/sus4 palette — no decisive major or minor third. |
|
A symmetrical augmented palette; dreamlike drift (IDM, ambient). |
|
Minor-third symmetry; angular and disorienting (dark, experimental). |
|
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
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 |
|---|---|
|
One MIDI note: the chord’s root, in the octave nearest |
|
The root shifted down by whole octaves (one by default) — a bass register below the chord voicing. |
|
A list of MIDI notes: the chord’s tones, voiced from the octave nearest
|
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.
Reference
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 |
|---|---|
|
Beats between successive notes. |
|
The order the tones cycle: |
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.
Reference
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, |
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
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 |
|---|---|
|
Beats per chord. Lower it for busy, fast-changing harmony; raise it for long,
static chords. |
|
Key gravity, |
|
Melodic inertia (Narmour expectation), |
|
Whether the engine may use V7 (dominant-seventh) chords. |
|
Root-repetition damping, |
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
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.