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

1, 6, 4

A scale degree (1-based). Quality is inferred from the key and scale later, when the progression is bound — so 1 is major in a major key, minor in a minor key.

Roman string

"V", "ii7", "bVII7"

A degree with its quality spelled out (uppercase major, lowercase minor, 7 for a seventh, b/# for chromatic degrees). Still key-relative.

Chord name

"Am", "Cmaj7", "F#"

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.

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

describe()

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. 0 is root position; 1 puts the third on the bottom; 2 the 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

inversions(), spread()

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.

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

"strong"

V → I

The full stop. Lands home, conclusively. (Theory: authentic.)

"soft"

IV → I

The gentle “amen” close — softer than V→I. (Plagal.)

"open"

IV → V

Hangs on the dominant, unresolved — a question, a comma. (Half.)

"fakeout"

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

cadence()

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")
Which placement seam?

Seam

Reach for it when

p.progression(...) in a pattern

You want to play each chord your way (strummed, broken, voiced) inside one pattern — and possibly give that part its own private harmony.

composition.chords(...)

You want a complete block-chord part in a single call, no loop to write.

composition.harmony(progression=...)

You want this progression to be the harmony of the piece, followed by every pattern that declares a chord parameter.

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.

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

strum(), broken_chord()


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.