Chapter 7 · Progressions as Values: Building and Shaping Harmony¶
In Chapter 6 you let a chord engine generate harmony live and had your patterns follow it; now we write the chords down ourselves — as a plain, printable value you can inspect, voice, decorate, cap with a cadence, and hand to the engine or play directly.
A progression in Subsequence is not a live thing that runs — it’s a value, like a list of numbers. You build it once, look at it, transform it (each transform hands you a new progression, leaving the original untouched), and only then decide how it sounds. That separation — describe the harmony first, perform it second — is what this chapter is about. It carries the running example forward: the GM drum loop and bass from earlier chapters, now joined by chord parts that play a progression you wrote by hand.
7.1 Writing a progression as a value¶
The one verb you need is subsequence.progression(...). Give it a list of
chords and it returns a Progression — a frozen sequence of chords-with-durations
that knows nothing about playback yet. The list is the standard form, and you can
write each chord three different ways, mixing them freely:
import subsequence
from subsequence import progression
import subsequence.constants.durations as dur
# Three ways to name the same idea — pick whichever reads clearest to you.
by_degree = progression([1, 6, 4, 5]) # scale degrees, key-relative
by_roman = progression(["i", "VI", "iv", "V"]) # roman numerals, quality explicit
by_name = progression(["Am", "F", "Dm", "E"]) # concrete chord names
The three forms differ in how much they pin down:
Element |
Example |
What it means |
|---|---|---|
Int degree |
|
A scale degree (1-based). Quality is inferred from the key and scale
later, when the progression is bound — so |
Roman string |
|
A degree with its quality spelled out (uppercase major, lowercase minor,
|
Chord name |
|
A concrete chord, with no key needed. Sounds the same in every key. |
The first two are key-relative: they store a degree, not a pitch, so the same
written progression transposes by changing one key. The third is concrete:
"Am" is always A minor. (This is exactly the specification-resolved-late idea
from §5.5 — a progression is a bundle of pitch
specifications, resolved against a key when you ask.)
The chord-name strings are read by parse_chord(name), which you can also call
on its own to turn a single name into a chord object — handy for checking that a
name spells what you expect (it normalises enharmonics, so Db reads as C#):
from subsequence import parse_chord
print(parse_chord("Cm7").name()) # C minor 7th
print(parse_chord("Dbmaj7").name()) # spelled C#maj7
print(parse_chord("F#").name()) # plain major
Cm7
C#maj7
F#
By default each chord lasts one bar (4 beats in common time). To give chords
different lengths — a harmonic rhythm that breathes — pair each one with a beat
count using (chord, beats) tuples, or pass a beats= list that cycles:
# Two bars of Am, then one each of F and G — written as (chord, beats) tuples.
shaped = progression([("Am", 8), ("F", 4), ("G", 4)])
# The same shape with a cycled beats= list.
also_shaped = progression(["Am", "F", "G"], beats=[8, 4, 4])
You can also generate a progression instead of writing one — pass style= and a
key= and Subsequence walks a chord graph for you (the same engine from
Chapter 6, here producing a value rather than a live stream):
walk = progression(style="aeolian_minor", key="A", bars=8, seed=3)
Note
Generation wants a seed=. A generated progression with no seed is a fresh
random walk every time the file reloads, which breaks live coding. Pass seed= (any
number) to pin the walk so the value is the same on every run — exactly the
repeatability habit from §4.6. Hand-written lists need no
seed; they’re already fixed.
Reference
7.2 Inspecting it: .describe() and print¶
Because a progression is a value, you can look at it before you ever make a
sound. Every progression prints itself — print(prog) calls its .describe() —
giving a one-chord-per-line summary with the running beat positions. This is the
fastest way to check you wrote the harmony you meant.
verse = progression([1, 6, 4, 5])
print(verse.describe())
Progression — 4 chords over 16 beats
0.00 … 4.00 1 (4 beats)
4.00 … 8.00 6 (4 beats)
8.00 … 12.00 4 (4 beats)
12.00 … 16.00 5 (4 beats)
Unbound, a key-relative progression prints its degrees — there’s no key yet, so
there are no chord names to show. Hand .describe() a key (and scale) and it
resolves every degree to a concrete chord, so you can read the actual harmony:
print(verse.describe("A", "minor"))
Progression — 4 chords over 16 beats
0.00 … 4.00 Am (4 beats)
4.00 … 8.00 F (4 beats)
8.00 … 12.00 Dm (4 beats)
12.00 … 16.00 Em (4 beats)
Same value, two readings — 1 6 4 5 in the abstract, Am F Dm Em in A minor. A
few more properties are handy when you’re building progressions programmatically:
named = progression(["Am", "F", "C", "G"])
print(len(named)) # number of chords
print(named.length) # total length in beats
print(named.is_concrete) # True — no degrees/romans left to resolve
4
16.0
True
Tip
print(prog) is to harmony what the ASCII grid from §1.7 is to
rhythm: a no-instrument-needed sanity check. If a chord is in the wrong place or
the wrong quality, you’ll see it in the printout long before you render a note.
Reference
7.3 Voicing and voice-leading¶
So far a progression only says which chords and how long. Voicing is the next layer — how each chord is stacked: which note is on the bottom, how spread out the notes are. Two transforms shape it, and each returns a brand-new progression (the original is never touched):
.inversions(spec)rotates which chord tone sits in the bass.0is root position;1puts the third on the bottom;2the fifth. Pass one int for all chords, or a list that cycles per chord..spread(style)sets how wide the voicing is:"close"(the default, notes bunched together),"open"(drop-2 — the second voice from the top drops an octave), or"wide"(drop-2-and-4, the most open).
base = progression(["Am", "F", "C", "G"])
smooth = base.inversions([0, 1, 2, 0]) # vary the bass note chord to chord
airy = base.spread("open") # a wider, more open stack
Inversions and spread don’t change the chord names (so they don’t show in
.describe()), but they very much change the notes that sound. You can see that by
asking each chord for its tones() — the MIDI notes it would play around a given
root:
for chord, start, length in base.inversions([0, 1, 2, 0]):
print(chord.name(), chord.tones(root=60))
Am [57, 60, 64]
F [69, 72, 77]
C [67, 72, 76]
G [55, 59, 62]
Letting the placement smooth itself: voice_leading=True¶
Choosing inversions by hand so the chords connect smoothly is fiddly. Often you’d
rather just say “keep each chord close to the last one” and let Subsequence pick
the inversion that moves the fewest notes. That’s voice leading, and you turn it
on where the chords are placed, not on the progression value itself: add
voice_leading=True to the pattern decorator, and p.chord(...) automatically
chooses the smoothest inversion each time.
import subsequence
from subsequence import progression
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(progression=progression([1, 6, 4, 5]))
@composition.pattern(channel=3, beats=4, voice_leading=True)
def pad(p, chord):
p.chord(chord, root=52, velocity=80, sustain=True) # inversion chosen for you
composition.render(bars=8, filename="pad.mid")
That chord argument is the live chord injected into the pattern — declaring a
parameter named chord is how a pattern asks to follow the harmony, exactly as in
§6.2. Here the harmony comes from a progression we bound to
the engine; more on binding in §7.6.
Important
Inversion is set in two different places, and they don’t fight — one wins.
.inversions(...) on the progression value is a fixed choice baked into each
chord. voice_leading=True on the pattern overrides that, choosing the inversion
dynamically to minimise movement. Use .inversions(...) when you want a specific
shape every time; use voice_leading=True when you just want it to sound connected
and don’t care how. Setting both means voice leading wins.
Reference
7.4 Decoration and substitution¶
Beyond voicing, you can decorate the chords themselves — add colour tones, put the whole thing over a pedal bass, or swap individual chords out. Every one of these returns a new progression, so they chain: each call takes a progression and hands back a richer one, in the same fluent style as the builder verbs from §4.5.
.extend(...) adds extension tones to every chord — 7, 9, 11, 13 for
stacked sevenths and above, or "sus2", "sus4", "add9", "6" for the named
colours. Restrict it to particular chords with only= (a list of 1-based slots):
jazzy = progression([2, 5, 1]).extend(7) # ii7 V7 I7-style sevenths everywhere
print(jazzy.describe("C", "ionian"))
Progression — 3 chords over 12 beats
0.00 … 4.00 Dm7 (4 beats)
4.00 … 8.00 G7 (4 beats)
8.00 … 12.00 C7 (4 beats)
Notice the qualities came out diatonically correct: extending the degrees 2 5 1
in C major gave Dm7 G7 C7, with G getting its dominant seventh — the engine knows
the scale, so the extensions land in key. (Writing seventh chords by name —
["Dm7", "G7", "Cmaj7"] — works too, and pins the quality yourself.)
.over(bass) puts the whole progression over a slash/pedal bass — the
trance and house move, a single low note held while the chords shift above it.
Pass a note name ("E"), a pitch class, or "tonic" (which follows the key):
pedal = progression(["Am", "F", "C", "G"]).over("E") # Am/E, F/E, C/E, G/E
print(pedal.describe())
Progression — 4 chords over 16 beats
0.00 … 4.00 Am/E (4 beats)
4.00 … 8.00 F/E (4 beats)
8.00 … 12.00 C/E (4 beats)
12.00 … 16.00 G/E (4 beats)
.replace(slot, chord) swaps one chord for another, keeping its beats (slots
are 1-based, the musician’s way of counting chords). It’s the surgical edit for
trying a substitution:
swapped = progression(["Am", "F", "C", "G"]).replace(2, "Dm") # F → Dm
print(swapped.describe())
Progression — 4 chords over 16 beats
0.00 … 4.00 Am (4 beats)
4.00 … 8.00 Dm (4 beats)
8.00 … 12.00 C (4 beats)
12.00 … 16.00 G (4 beats)
For a bigger gesture, .elaborate(depth) approaches each chord by a chain of
secondary dominants — the jazz/blues move of inserting V7-of-the-next-chord
turnarounds. depth=1 puts one dominant before each chord; depth=2 a full ii–V.
It needs a concrete progression (so .resolve(key) first, or write names):
blues = progression("twelve_bar_blues").resolve("C")
chorus2 = blues.elaborate(2, seed=4) # ii–V turnarounds woven throughout
print(len(blues), "→", len(chorus2), "chords")
12 → 36 chords
Note
progression("twelve_bar_blues") is a preset — a bare string names one of a
small curated set of recognisable loops ("pop_axis", "doo_wop", "ii_v_i",
"andalusian", and more). Presets are key-relative romans, so .resolve("C")
pins them to a key. They’re a quick starting point you then shape with the
transforms above.
Reference
7.5 Cadences: strong, soft, open, fakeout¶
A cadence is how a phrase ends — the harmonic punctuation that makes it feel
finished, half-finished, or deliberately swerved. .cadence(name) rewrites the
tail of a progression with a named two-chord formula, keeping the beats of the
chords it replaces. The names are plain-language:
Name |
Formula |
The feeling |
|---|---|---|
|
V → I |
The full stop. Lands home, conclusively. (Theory: authentic.) |
|
IV → I |
The gentle “amen” close — softer than V→I. (Plagal.) |
|
IV → V |
Hangs on the dominant, unresolved — a question, a comma. (Half.) |
|
V → vi |
Sets up the full stop, then dodges to the submediant instead of home. (Deceptive.) |
Take a four-chord loop and cap it three different ways — the first two chords stay, the last two become the cadence:
loop = progression(["Am", "F", "C", "G"])
print(loop.cadence("open").describe("A", "minor")) # … Dm E — hangs open
Progression — 4 chords over 16 beats
0.00 … 4.00 Am (4 beats)
4.00 … 8.00 F (4 beats)
8.00 … 12.00 Dm (4 beats)
12.00 … 16.00 E (4 beats)
The open cadence replaced C G with Dm E (IV → V in A minor) — the phrase now
hangs on the E dominant instead of resolving. Swap "open" for "fakeout" and
the tail becomes E F instead: the ear is set up for the home chord (Am) by that
E dominant, then gets F — the submediant — and the close is dodged. (In a major
key the same formula is the textbook V → vi, swerving onto the relative minor;
under a minor key the sixth degree is major, so the swerve lands on a bright
chord instead.)
Tip
Cadences pair naturally with the structure tools coming later: an "open"
cadence at the end of a verse leaves the door ajar for the chorus, and a "strong"
cadence slams a section shut. You can also ask the live engine to aim for a
cadence as it walks — that’s the cadence= argument you’ll meet on
composition.freeze(...) in §7.6.
Reference
7.6 Placing harmony: p.progression, composition.chords, between()¶
A progression value makes no sound on its own — something has to place it. There are three seams, from most-hands-on to most-automatic.
Inside a pattern, chord by chord: p.progression(...)¶
p.progression(source, harmonic_rhythm=...) lays a progression end-to-end across
the current pattern and hands you back the realised timeline to loop over. Each
iteration gives (chord, start, length) — the chord, the beat it starts on, and
how long it lasts — so you decide exactly how to play each one:
import subsequence
from subsequence import progression
import subsequence.constants.durations as dur
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
@composition.pattern(channel=3, beats=16)
def pads(p):
for chord, start, length in p.progression([1, 6, 4, 5], harmonic_rhythm=dur.WHOLE):
p.chord(chord, root=48, beat=start, duration=length - 0.25, velocity=80)
composition.render(bars=4, filename="pads.mid")
The harmonic_rhythm is how long each chord lasts, in beats. dur.WHOLE (4
beats) gives one chord per bar. The duration constants from
§3.5 read musically: dur.HALF, dur.WHOLE, etc.
This is the part-level seam: it runs in its own little world, outside the global chord engine, so this part can have harmony nobody else follows. It re-realises fresh each cycle, which matters once the rhythm varies (below).
Variable harmonic rhythm: between()¶
For a static rhythm, pass a single number. For a shaped one, pass a list of
lengths that cycles ([dur.WHOLE, dur.HALF, dur.HALF]). For an irregular one —
each chord a fresh random length — pass between(low, high, step=...). The
step= snaps those lengths to a grid so they stay musical:
import subsequence
from subsequence import progression, between
import subsequence.constants.durations as dur
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
@composition.pattern(channel=3, beats=16)
def breathing(p):
# Each chord lasts 1, 2, or 3 whole notes — irregular but always on a bar line.
rhythm = between(dur.WHOLE, 3 * dur.WHOLE, step=dur.WHOLE)
for chord, start, length in p.progression("phrygian_minor", harmonic_rhythm=rhythm, seed=7):
p.chord(chord, root=48, beat=start, duration=length - 0.25, velocity=80)
composition.render(bars=4, filename="breathing.mid")
Note
between(low, high) is the harmonic-rhythm cousin of the (low, high) velocity
tuple from §1.5: a bounded random draw, one per chord. It’s spelled
between(...) rather than a bare tuple precisely so a plain list can keep its other
meaning — a sequence of lengths to cycle through.
A whole chord part in one call: composition.chords(...)¶
When you just want a block-chord part and don’t need to shape each chord yourself,
composition.chords(...) declares the entire pattern for you — progression,
harmonic rhythm, voicing, and all. It returns the realised timeline so you can
print it and see exactly what it chose:
import subsequence
from subsequence import progression
import subsequence.constants.durations as dur
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
timeline = composition.chords(
channel=3,
progression=["Am", "F", "C", "G"],
harmonic_rhythm=dur.WHOLE,
bars=4,
root=48,
voicing=(3, 4), # 3 or 4 notes per chord
velocity=80,
)
print(timeline.describe())
composition.render(bars=4, filename="chord-part.mid")
Progression — 4 chords over 16 beats
0.00 … 4.00 Am (4 beats)
4.00 … 8.00 F (4 beats)
8.00 … 12.00 C (4 beats)
12.00 … 16.00 G (4 beats)
This needs no composition.harmony(...) call and — with an explicit chord list or a
key= — no composition key either, so a drums-plus-one-chord-part sketch stays
tiny.
Driving the whole engine: composition.harmony(progression=...)¶
The third seam binds a progression to the global chord engine, so it becomes the
harmony that every chord-following pattern reads. This is the bridge back to
Chapter 6: instead of the engine generating chords, you hand
it a fixed progression, and the injected chord parameter delivers it to your
basses, pads, and arpeggios:
import subsequence
from subsequence import progression
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(progression=progression([1, 6, 4, 5])) # loops forever
@composition.pattern(channel=6, beats=4)
def bass(p, chord):
p.note(chord.bass_note(40), beat=0, duration=3.5, velocity=90) # root of each chord
@composition.pattern(channel=3, beats=4, voice_leading=True)
def pad(p, chord):
p.chord(chord, root=52, velocity=80, sustain=True)
composition.render(bars=8, filename="bound.mid")
Seam |
Reach for it when |
|---|---|
|
You want to play each chord your way (strummed, broken, voiced) inside one pattern — and possibly give that part its own private harmony. |
|
You want a complete block-chord part in a single call, no loop to write. |
|
You want this progression to be the harmony of the piece, followed by every
pattern that declares a |
Capturing live harmony: composition.freeze() (a preview)¶
One more source of progressions: you can let the live engine generate harmony and
then freeze a stretch of it into a progression value. composition.freeze(bars)
runs the engine forward and captures the chords it produces — useful for keeping a
section’s harmony fixed while the rest of the piece keeps generating:
import subsequence
composition = subsequence.Composition(bpm=120, key="A", scale="minor", seed=11)
composition.harmony(style="aeolian_minor", cycle_beats=4)
chorus = composition.freeze(4, cadence="strong") # 4 chords, ending V → i
print(chorus.describe("A", "minor"))
Progression — 4 chords over 16 beats
0.00 … 4.00 Am (4 beats)
4.00 … 8.00 Dm (4 beats)
8.00 … 12.00 E (4 beats)
12.00 … 16.00 Am (4 beats)
Notice cadence="strong" steered the walk to arrive at V → i (E → Am) — the same
cadence vocabulary from §7.5, here aiming the engine instead of
rewriting a tail. The full explore-then-capture workflow — seeds, locks, and what
freeze does to engine state — is Chapter 11; this is just the preview that it
hands you back an ordinary progression value, ready for everything in this chapter.
Note
Binding a frozen (or hand-written) progression to a section of a larger form is a
one-liner, composition.section_chords("chorus", chorus), so different parts of a
track can carry different harmony. Sections and forms are Chapter 10; we mention it
here only so you know the captured value has somewhere to go.
Reference
progression(),
chords(),
harmony(),
freeze(),
section_chords()
7.7 Strummed and broken chords¶
A block chord — all notes at once — is only one way to play harmony. Two verbs give
you the others, and both take the injected chord (or a chord from a
p.progression loop) exactly like p.chord does.
p.strum(...) plays the chord tones one after another with a tiny delay
between them — the guitar/harp gesture. spacing is the gap between notes (in
beats), and direction is "up" (low to high) or "down":
import subsequence
from subsequence import progression
import subsequence.constants.durations as dur
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
@composition.pattern(channel=3, beats=16)
def guitar(p):
for chord, start, length in p.progression([1, 6, 4, 5], harmonic_rhythm=dur.WHOLE):
p.strum(chord, root=48, beat=start, spacing=0.04, count=4,
duration=length - 0.25, velocity=75)
composition.render(bars=4, filename="strummed.mid")
p.broken_chord(...) spells the chord out as an arpeggio in an order you
choose — order is a list of indices into the chord tones (0 = root, 1 =
third, 2 = fifth, and on up, cycling into higher octaves). span says how many
beats to fill, so the figure repeats to cover the whole chord:
import subsequence
from subsequence import progression
import subsequence.constants.durations as dur
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
@composition.pattern(channel=4, beats=16)
def arp(p):
for chord, start, length in p.progression([1, 6, 4, 5], harmonic_rhythm=dur.WHOLE):
# root, third, fifth, third — a classic up-and-back arpeggio.
p.broken_chord(chord, root=60, order=[0, 1, 2, 1], spacing=dur.EIGHTH,
beat=start, span=length, velocity=70)
composition.render(bars=4, filename="broken.mid")
Putting it together: the running drum loop, a root bassline, a strummed chord part, and an arpeggio — all four reading the same bound progression — is the full chord-following arrangement this chapter has been building toward. Each part chooses how to voice the harmony; the progression value decides what the harmony is.
Tip
Both verbs accept a (low, high) velocity tuple for per-note humanising (each
strum note or arp note gets its own dynamic, like the hi-hat trick in
§1.5). p.strum additionally has a detached= option that
guarantees the figure clears before the next chord — handy on a synth with limited
polyphony; p.broken_chord instead shapes its release with duration and span.
One last thing a progression value lets you do is build longer harmony out of
shorter pieces: + joins two progressions end to end, and * repeats one. The
chords are decided once, as values, before anything plays:
intro = progression(["Am", "F"])
chorus = progression(["C", "G"])
song = intro + chorus # Am F C G — concatenation
vamp = intro * 4 # Am F Am F Am F Am F — repetition
print(song.describe())
Progression — 4 chords over 16 beats
0.00 … 4.00 Am (4 beats)
4.00 … 8.00 F (4 beats)
8.00 … 12.00 C (4 beats)
12.00 … 16.00 G (4 beats)
Note
Under the hood — sounding values and governing values
Subsequence sorts its musical values into two families, and the difference explains
both the +/* you just used and a rule you’ll meet if you try the wrong operator.
A Progression is a governing value — it describes context, the harmony that
other parts sound against. The Motifs and Phrases of later chapters are sounding
values — they describe actual notes. Both families compose the same way:
sequentially with + and by repetition with *. But only sounding values can be
merged in parallel with & (two melodies at once). Try to merge two
progressions in parallel — intro & chorus — and you get a deliberate error:
Progressions cannot be merged with
&— there is one current chord. Sequence them with+, or give a pattern its own part-level progression.
That’s not a missing feature — it’s the musical fact “there is only ever one
chord sounding at a time” written into the type system. Harmony is a single
context, not a stack of parallel parts. If you genuinely want two harmonic worlds at
once (polytonality), you give each part its own p.progression(...) — the
part-level seam from §7.6, which runs outside the one global chord.
The rule, in a sentence: sounding values stack; the governing chord does not.
Reference
You can now treat harmony as material you write down and shape: build a progression
from degrees, romans, or names; inspect it with print; voice it, decorate it, and
cap it with a cadence; and place it three ways — chord by chord, as a one-call part,
or as the engine’s global harmony — played as blocks, strums, or arpeggios. Next we
do the same for melody: naming a short figure as a reusable Motif and developing
it, so a whole part can grow from one idea.