Chapter 10 · Form, Sections, and Energy: Arranging a Track¶
In Chapter 9 you grew whole melodic phrases from a single idea; now we arrange those phrases — and the chords and beats around them — into a track with a beginning, a middle, and an end. This is the layer that stops the music looping forever.
Everything so far has been a loop. The drums repeat, the bass follows the chord, the phrase walks itself cycle by cycle — but nothing ever says “now we’re in the chorus.” A form is that missing instruction: a named list of sections the piece moves through, each one a different length, each one able to change what the patterns do. Once the composition knows it’s eight bars into a verse with a chorus coming next, every pattern can ask where am I? and arrange itself accordingly — drop the hats in the intro, open the hi-hats in the chorus, fire a fill into the turnaround, hand the verse and the chorus their own progressions and their own lead lines.
Form is a governing value, exactly like a Progression (the family from
§7.7): you write it down as data, inspect it, edit it, and bind
it to the composition. It governs when things happen; the patterns still decide
what. That separation — write the structure first, perform it second — is the
same one that ran through harmony and motifs, now one level up, at the scale of
the whole song.
10.1 Defining a form (list and weighted graph)¶
The one verb is composition.form(sections). The simplest thing you can hand
it is a list of (name, bars) tuples — a fixed running order, top to bottom,
exactly the way you’d sketch a song on paper:
import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([
("intro", 4),
("verse", 8),
("chorus", 8),
("verse", 8),
("chorus", 8),
("outro", 4),
])
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
composition.render(bars=40, filename="form-list.mid")
That’s the whole structure: a 4-bar intro, then verse/chorus twice, then a 4-bar outro — 40 bars in all. The composition now runs a form clock alongside the beat clock and the harmonic clock from Chapter 6: every bar it knows which section is sounding, and when one section’s bars run out it steps to the next.
Important
A form does not make sound, and it does not change your patterns by itself. It
is context — the same kind of read-only fact as the current chord. The drums
above play identically in every section because nothing in drums reads the
section yet. The form is in place; §10.2 is where patterns
start to listen to it.
What happens at the end: at_end¶
A list form is finite. By default, when the last section’s bars run out the form stops — patterns see no section and (if you’re following the form) fall silent. Two other endings are a keyword away:
|
When the list runs out… |
|---|---|
|
The form finishes. Patterns that gate on the section go quiet — the natural end of a rendered track. |
|
The final section repeats until you navigate away from it — handy for a closing vamp you’ll end by hand live. |
|
Start over from the first section. The whole arrangement cycles forever.
|
# The same running order, but looping the whole arrangement forever.
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("verse", 8), ("chorus", 8)], loop=True)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
composition.render(bars=32, filename="form-loop.mid")
Form as an editable value: Section and Form¶
The list of tuples is the quick form. Its grown-up sibling is a Form value
built from Section values — the same write-it-down-as-data idea as a
Progression. A Section carries a name and bars, and (the part that earns
the upgrade) an energy level and an optional key/scale override. A Form is
a frozen sequence of them, and like a progression it prints itself, edits into new
copies, and joins with +:
S = subsequence.Section
body = subsequence.Form([
S("verse", 8),
S("chorus", 8, energy=0.9), # the chorus carries more energy (see §10.3)
])
# Editing returns a NEW Form — the original is untouched (immutability, as ever).
longer = body.replace(2, bars=16) # stretch the chorus to 16 bars
with_intro = subsequence.Form([S("intro", 4)]) + body # join two forms end to end
print(body.describe())
Form — 2 sections over 16 bars
1. bars 1–8 verse (8 bars) energy=0.5
2. bars 9–16 chorus (8 bars) energy=0.9
Tip
Repetition is plain Python — there’s no letters-DSL. A whole AABA is just list
arithmetic before you build the form: [a, a, b, a] is [a] * 2 + [b] + [a]. The
sections are values, so you assemble the running order the way you’d assemble any
list. composition.form([...]) accepts a list of Sections, a list of
(name, bars) tuples, or a finished Form value — they’re the same form.
A form that decides as it goes: the weighted graph¶
A list is a fixed running order. The other way to write a form is a weighted graph: instead of “verse then chorus then verse,” you say “after a verse, most likely a chorus, but sometimes another verse.” The composition walks the graph live, rolling weighted dice at each section boundary — a form that arranges itself differently every play, the structural cousin of the generative harmony from Chapter 6.
You write the graph as a dict mapping each section name to a
(bars, transitions) pair. transitions is a list of (target, weight) tuples —
where this section can go next, and how strongly it leans each way — or None to
mark a terminal section where the form ends:
composition = subsequence.Composition(bpm=120, key="A", scale="minor", seed=7)
composition.form({
"intro": (4, [("verse", 1)]), # intro → always verse
"verse": (8, [("chorus", 3), ("verse", 1)]), # usually chorus, sometimes again
"chorus": (8, [("verse", 2), ("bridge", 1), ("outro", 1)]),
"bridge": (4, [("chorus", 1)]),
"outro": (4, None), # terminal — the form ends here
}, start="intro")
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
composition.render(bars=48, filename="form-graph.mid")
start="intro" names where the walk begins (without it, the first key in the dict
is the start). From "verse", the weights 3 and 1 mean a chorus is three times
as likely as another verse; from "chorus" the piece might loop back, take the
bridge, or head for the door. It only stops when the walk reaches "outro",
whose None transitions make it terminal.
Important
Seed a generative form, exactly like a generated progression. A graph form
with no seed= walks a fresh random path every time the file reloads — which
breaks live coding and makes a render unrepeatable. Setting seed= on the
Composition (as above) pins the walk so the structure is the same on every run.
This is the repeatability habit from §4.6 and
§7.1, now governing the shape of the whole track.
Form shape |
Reach for it when |
|---|---|
List / |
You know the running order — a fixed arrangement you’ll render or perform the same way each time. Navigable and editable. |
Weighted-graph dict |
You want the structure to vary — a generative jam that picks its own path, or a live set where you’ll steer the next section by hand (§10.6). |
10.2 Reading the section: p.section¶
A form on its own changes nothing. Patterns make it matter by reading
p.section — the same way they read p.cycle or the injected chord. It’s a
small read-only snapshot of where the playhead is right now in the form, and
every pattern gets it for free (no parameter to declare — it’s an attribute on the
builder, like p.bar).
p.section is None when no form is configured or the form has finished, so the
safe shape is “if there’s a section, and it’s the chorus, do the chorus thing”:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("intro", 4), ("verse", 8), ("chorus", 8)], loop=True)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100) # the floor, every section
if p.section and p.section.name != "intro":
p.hit_steps("snare_1", [4, 12], velocity=90) # backbeat once the intro's over
if p.section and p.section.name == "chorus":
p.hit_steps("hi_hat_closed", range(16), velocity=70) # busy hats only in the chorus
composition.render(bars=20, filename="section-aware-drums.mid")
One pattern, three textures: a bare kick in the intro, kick-and-snare in the verse,
the full kit in the chorus — and it’s the same drums function each time,
re-running every cycle (the rebuild loop from Chapter 2) and
asking the form where it stands.
What p.section tells you¶
The snapshot is a SectionInfo. These are the fields and properties you’ll reach
for:
On |
What it gives you |
|---|---|
|
The section’s name ( |
|
How far through the section you are, |
|
|
|
|
|
The name of the section coming next (or |
|
|
|
The section’s energy level — the arranging dial of §10.3. |
.progress turns a static section into a moving one. Here a chorus hi-hat line
opens soft and swells as the section runs — a riser written in one line, with no
counters to keep, because the form already knows how far in we are:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("verse", 8), ("chorus", 8)], loop=True)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
if p.section and p.section.name == "chorus":
# 0.0 → 1.0 across the chorus: hats climb from soft to slamming.
velocity = int(60 + 50 * p.section.progress)
p.hit_steps("hi_hat_closed", range(16), velocity=velocity)
composition.render(bars=16, filename="section-riser.mid")
.next_section lets a pattern look ahead. Because the upcoming section’s name is
known the moment the current one starts, you can lead into it — laying off the hats
in the last bar of a verse only when a chorus is actually next:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("verse", 8), ("chorus", 8)], loop=True)
@composition.pattern(channel=10, 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("hi_hat_closed", range(16), velocity=60)
# On the very last bar before a DIFFERENT section, clear the hats for a breath
# and roll the snare — a hand-written lead-in. (.ending is exactly this test.)
if p.section and p.section.ending and p.section.next_section == "chorus":
p.hit_steps("snare_1", [8, 10, 12, 13, 14, 15], velocity=(80, 110))
composition.render(bars=16, filename="section-leadin.mid")
Note
.ending is shorthand for “.last_bar and a different section is coming.” A
section repeating itself (a verse that loops to another verse in a graph form) is
deliberately not an ending — you don’t want a turnaround fill before more of
the same. When you specifically want the last bar regardless of what follows, use
.last_bar. §10.4 automates the fill so you rarely hand-write
this, but it’s worth seeing the raw reading once.
Reference
10.3 Energy as the arranging dial¶
Branching on section names gets you a long way, but it scatters
if p.section.name == ... checks through every pattern. Energy is the tidier
idea: one number per section, 0.0 (empty, sparse) to 1.0 (full, slamming),
that patterns read and arrange themselves against. An intro at 0.2, a verse at
0.55, a chorus at 0.95 — set the levels once, and every part can consult the
single dial instead of memorising the running order.
You set energy two ways. A Section value can carry its own energy=
(§10.1); or — the performance-level dial — you name the levels
in one dict with composition.energy(...):
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("intro", 4), ("verse", 8), ("chorus", 8)], loop=True)
composition.energy({"intro": 0.2, "verse": 0.55, "chorus": 0.95})
Patterns read the resolved level as p.energy (a plain float — 0.5 when no
energy source is configured). Use it as a continuous control, not just a switch —
scale a velocity, a note count, a probability:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("intro", 4), ("verse", 8), ("chorus", 8)], loop=True)
composition.energy({"intro": 0.2, "verse": 0.55, "chorus": 0.95})
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
# The kick hits harder as the section's energy rises — one dial, no name checks.
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=int(70 + 50 * p.energy))
# Hats only appear once the energy crosses a threshold.
if p.energy >= 0.5:
p.hit_steps("hi_hat_closed", range(16), velocity=int(50 + 40 * p.energy))
composition.render(bars=20, filename="energy-drums.mid")
Important
The energy() dict overrides any energy a Section carries. A Section’s
own energy= is the value baked into the form (the writer’s intent); the
composition.energy() dict is the later, performance-level dial that wins on top
(the arranger’s intent). Set energy on the Section for a form you’ll hand around
as a value; reach for composition.energy() when you’re tuning levels live and
want one place to see them all. When neither is set, every section reads 0.5.
Automatic gating: min_energy=¶
The commonest energy move — “this part is silent until the energy is high
enough” — is so common it has a shortcut. Declare min_energy= on the pattern
decorator and Subsequence mutes the whole pattern whenever the current section’s
energy is below the threshold. No if inside the body at all:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)
composition.form([("intro", 4), ("verse", 8), ("chorus", 8)], loop=True)
composition.energy({"intro": 0.2, "verse": 0.55, "chorus": 0.95})
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
# This pad sits out the low-energy intro and only enters from the verse onward.
@composition.pattern(channel=3, beats=4, voice_leading=True, min_energy=0.5)
def pad(p, chord):
p.chord(chord, root=52, velocity=70, sustain=True)
# A bright counter-line reserved for the chorus — silent below 0.9.
@composition.pattern(channel=4, beats=4, min_energy=0.9)
def topline(p, chord):
p.arpeggio(chord, root=72, count=4, spacing=0.25, velocity=85, direction="up")
composition.render(bars=20, filename="min-energy.mid")
The pad enters at the verse (0.55 ≥ 0.5), the topline waits for the chorus
(0.95 ≥ 0.9), and the kick plays throughout. Arranging by addition — parts
joining as the energy climbs — is most of what a build-and-drop arrangement is,
and min_energy= writes it declaratively.
Warning
min_energy= does nothing useful unless an energy source exists — a
composition.energy() dict or Section energy payloads. With no source, every
section reads the default 0.5, so the gate can’t track the form: the part either
always plays (threshold at or below 0.5) or is silenced for the whole track
(threshold above it) — never the section-by-section behaviour you wanted.
Subsequence warns you at render so the mistake doesn’t pass quietly. A performer
mute always wins over the gate: a part you muted by hand stays muted regardless of
energy.
A build that ramps: the (start, end) tuple¶
A section can do more than hold one level — it can climb. In the
composition.energy() dict, give a section a (start, end) tuple instead of a
single number and the energy interpolates across the section, reaching the end
level on its final bar. That’s a riser written as a form value: every
energy-reading pattern swells together, with no per-pattern ramp logic:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("build", 4), ("drop", 8)], loop=True)
composition.energy({
"build": (0.2, 1.0), # climbs from 0.2 to 1.0 across the 4-bar build
"drop": 1.0, # then slams in at full
})
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=int(60 + 60 * p.energy))
if p.energy >= 0.6:
p.hit_steps("hi_hat_closed", range(16), velocity=int(50 + 50 * p.energy))
composition.render(bars=24, filename="energy-build.mid")
Reference
10.4 Boundaries: transitions, fills, and mutes¶
The interesting things in an arrangement happen at the seams — the crash and
snare-roll into the chorus, the pads dropping out for the breakdown, the bar of
silence before the drop. You saw in §10.2 how to
hand-write a lead-in by reading .ending. composition.transition(...) does
the same job declaratively: it states, once, what should happen at a boundary, and
the form clock fires it automatically every time that boundary comes round.
A transition takes a before= — the name of the incoming section it triggers on
(or "*" for any different section) — and one or both of two actions: a fill
to play, and patterns to mute on the approach.
A fill into the boundary¶
fill= takes a Motif (the reusable material from Chapter 8) and
plays it in the last bar before the boundary, on the channel you give. Here a
snare roll fires into every section change, starting halfway through the
outgoing section’s final bar:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("verse", 8), ("chorus", 8)], loop=True)
# A fill is a Motif. Motif.hits places one drum name across a list of beats;
# the (80, 110) velocity tuple humanises each hit, as in §1.5.
ROLL = subsequence.Motif.hits(
"snare_1", [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5],
velocities=(80, 110), length=4,
)
composition.transition(before="*", fill=ROLL, channel=10, beat=2.0,
drum_note_map=gm_drums.GM_DRUM_MAP)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
composition.render(bars=16, filename="transition-fill.mid")
beat=2.0 starts the roll on beat 3 of the final bar; before="*" fires it on
any change of section (a section repeating itself never triggers it). The fill
borrows the drum map from a pattern on the same channel if you don’t pass
drum_note_map=, but giving it explicitly is the clearer habit.
Note
The fill is an ordinary Motif, so a melodic fill works the same way. A
degree-bearing motif placed as a fill resolves against the outgoing section’s key
and scale, so a turnaround riff lands in the right key even when the next section
modulates. Anything you can build in Chapter 8 — a euclidean burst, a
captured lick, a tom run — can be a fill=.
Muting on the approach¶
mute= takes a list of pattern names to silence over the approach to a boundary,
re-opening them exactly at the section change. This is the subtractive gesture —
pulling the pads out for the bar before the drop so the drop lands with more
weight:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)
composition.form([("verse", 8), ("drop", 8)], loop=True)
# For the 4 beats before the drop, silence the pad — then it snaps back in.
composition.transition(before="drop", mute=["pad"], beats=4)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
@composition.pattern(channel=3, beats=4, voice_leading=True)
def pad(p, chord):
p.chord(chord, root=52, velocity=70, sustain=True)
composition.render(bars=16, filename="transition-mute.mid")
The mute= names must match the pattern function names ("pad" is the function
def pad). Muting is bar-granular, so beats= rounds up to whole bars; a mute you
applied yourself by hand is left alone. Fills and mutes combine in one call, and
transitions stack — call transition(...) once per rule:
|
What it does |
|---|---|
|
The incoming section name to trigger on, or |
|
A Motif to play in the final bar before the boundary (needs |
|
Where in that final bar the fill starts (default |
|
Pattern names to silence on the approach, re-opened at the boundary. |
|
How long the mute window is (rounded up to whole bars; defaults to one bar). |
Reference
10.5 Binding harmony, motifs, and phrases to sections¶
So far the form changes texture — which parts play, how hard. The richest use of sections is to give each one its own material: the verse its progression and its lead line, the chorus a brighter progression and a different melody. Because progressions, motifs, and phrases are all values you already know how to build, binding them to a section is a one-liner each.
Harmony per section: composition.section_chords()¶
composition.section_chords(name, progression) binds a Progression
(Chapter 7) to a named section. Whenever that section plays, the
harmonic clock walks that progression instead of generating live chords — and
every chord-following pattern (anything declaring a chord parameter) hears it,
unchanged. Sections you don’t bind keep generating live, so you can mix written and
generated harmony in one track:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4) # the live fallback
composition.form([("verse", 8), ("chorus", 8)], loop=True)
# Each section gets its own written progression.
composition.section_chords("verse", progression([1, 6, 4, 5]))
composition.section_chords("chorus", progression([6, 4, 1, 5]))
@composition.pattern(channel=2, beats=4)
def bass(p, chord):
p.note(chord.bass_note(40), beat=0, duration=3.5, velocity=95) # follows whichever is bound
@composition.pattern(channel=3, beats=4, voice_leading=True)
def pad(p, chord):
p.chord(chord, root=52, velocity=70, sustain=True)
composition.render(bars=16, filename="section-chords.mid")
The bass and pad never mention sections — they just follow the injected chord,
exactly as in Chapter 6. The form swaps the harmony
underneath them: 1 6 4 5 in the verse, 6 4 1 5 in the chorus. This is the
delivery the freeze-and-bind note at the end of §7.6 promised —
a frozen-or-written progression finds its home on a section.
Note
A key-relative progression (degrees or romans) bound to a section resolves late,
against that section’s effective key — so a Section with a key= override plays
the same numbered progression transposed to its own tonic. That’s how a bridge can
genuinely lift to a new key: bind degrees, and re-key the section. Concrete chord
names ("Am") are absolute and never move, just as in §7.1.
Melody per section: section_motifs() and phrase_part()¶
The melodic counterpart is composition.section_motifs(name, value, part=...),
which binds a Motif or Phrase to a section (an optional part= label lets one
section carry several — a lead and a counter-line). One pattern then plays
“whatever is bound to the current section,” and the cleanest way to write that
pattern is the one-call composition.phrase_part(...):
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)
composition.form([("verse", 8), ("chorus", 8)], loop=True)
# Two phrases, grown from two ideas the way Chapter 9 builds them.
verse_idea = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
verse_line = subsequence.sentence(verse_idea, bars=8, cadence="open", seed=11)
chorus_idea = subsequence.motif([1, 2, 3, 5, None, 5, 3, 1])
chorus_line = subsequence.sentence(chorus_idea, bars=8, cadence="strong", seed=12)
composition.section_motifs("verse", verse_line, part="lead")
composition.section_motifs("chorus", chorus_line, part="lead")
# ONE part plays the bound line of whichever section is sounding.
composition.phrase_part(channel=4, part="lead", root=72, bars=2)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
composition.render(bars=16, filename="section-motifs.mid")
phrase_part(channel=4, part="lead") registers a pattern that, each cycle, reads
the section’s bound "lead" value and walks one window of it — the verse line in
the verse, the chorus line in the chorus, with no branching you write. A section
with nothing bound for a part is simply silent for it; there’s no fallback
guessing.
Tip
If you’d rather keep your own pattern, read the binding directly with
p.section_motif("lead") and place it yourself: it returns the bound Motif/Phrase
or None, so the shape is line = p.section_motif("lead") then if line is not None: p.phrase(line, root=72). phrase_part(...) is exactly that loop, packaged.
The subsequence.roles bundles (**subsequence.roles.LEAD) are handy splat-in
defaults for the root/velocity/fit of such a part.
Stacking these bindings is how a full arrangement comes together: bind a progression and a lead phrase to each section, set the energy, declare a fill at the boundary, and the verse and chorus become genuinely different music — same patterns, different material, governed entirely by the form.
Reference
section_chords(),
section_motifs(),
phrase_part(),
section_motif()