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:

at_end=

When the list runs out…

"stop" (default)

The form finishes. Patterns that gate on the section go quiet — the natural end of a rendered track.

"hold"

The final section repeats until you navigate away from it — handy for a closing vamp you’ll end by hand live.

"loop"

Start over from the first section. The whole arrangement cycles forever. loop=True is sugar for this.

# 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.

Which form shape?

Form shape

Reach for it when

List / Form value

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).

Reference

form(), Section, Form, replace()

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 p.section

What it gives you

.name

The section’s name ("chorus") — what you branch on.

.progress

How far through the section you are, 0.0 at the start rising toward 1.0 — the dial for a build or a riser.

.first_bar

True on the section’s opening bar — for a downbeat accent or a “new section” gesture.

.last_bar

True on the section’s closing bar — where a turnaround or pickup belongs.

.next_section

The name of the section coming next (or None if the form ends after this one). Decided when the section begins, so a pattern can plan its lead-in.

.ending

True on the last bar before a different section — the precise place for a fill (a repeat, verse→verse, is not an ending).

.energy

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

SectionInfo

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

energy()

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:

transition(...) argument

What it does

before=

The incoming section name to trigger on, or "*" for any different section.

fill=

A Motif to play in the final bar before the boundary (needs channel=).

beat=

Where in that final bar the fill starts (default 0.0).

mute=

Pattern names to silence on the approach, re-opened at the boundary.

beats=

How long the mute window is (rounded up to whole bars; defaults to one bar).

Reference

transition(), hits()

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.

10.6 Live navigation and generative forms

A form clock you can only watch is half the fun. The last piece is steering it — jumping to a section on a keypress in a live set, or letting a graph form improvise its own structure and then capturing the result.

Driving the form by hand: form_jump and form_next

Two methods move a navigable form (a list, a Form value, or a graph) under your control:

  • composition.form_jump(name) cuts to a section immediately — the next rebuild cycle lands in it. This is the hard cut, the “go to the chorus now” button.

  • composition.form_next(name) queues a section to play after the current one finishes — the musical, on-the-boundary move. Call it again before the boundary to change your mind.

You’d wire these to hotkeys during a live performance. Because that needs the live keyboard, the binding itself is shown for reference rather than run headlessly:

# A LIVE feature — pair it with play(), not render(). Illustrative only.
composition.hotkeys()
composition.hotkey("c", lambda: composition.form_jump("chorus"))   # jump now
composition.hotkey("b", lambda: composition.form_next("bridge"))   # queue for the boundary
composition.play()

Note

A form jump is heard at the next pattern rebuild cycle, not mid-note — already- queued MIDI is left to finish. That’s the same natural quantization that applies to every live change (composition.data writes, tweak()), so jumps feel musical without you setting a bar boundary by hand. A jump to a name that appears more than once lands on the next occurrence, searching forward and wrapping.

Reacting to a section change: on_section()

To run your own code each time the form turns over — log it, switch a synth patch, re-seed a generator — register a callback with composition.on_section(). It fires on every section change (and once at the start), receiving the new SectionInfo (or None when the form ends):

composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.form([("verse", 4), ("chorus", 4)], loop=True)

composition.on_section(
    lambda info: print("now playing:", info.name if info else "(end)")
)

@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=8, filename="on-section.mid")
now playing: verse
now playing: chorus
now playing: verse

The callback fires once at the start and on every change after — verse, chorus, then verse again as the looping form turns over. It fires a beat early, in time to affect the new section’s first patterns, which is what makes it useful for setup work like a program change ahead of the downbeat.

Capturing a generative form: form_freeze()

A graph form (§10.1) improvises its structure live. When it walks a path you like, composition.form_freeze() captures that walk into an editable Form value — the structural twin of freezing live harmony into a progression (§7.6). You can inspect it, edit it, and rebind it as a fixed arrangement:

composition = subsequence.Composition(bpm=120, key="A", scale="minor", seed=7)

composition.form({
    "intro":  (4, [("verse", 1)]),
    "verse":  (8, [("chorus", 3), ("verse", 1)]),
    "chorus": (8, [("verse", 2), ("outro", 1)]),
    "outro":  (4, None),
}, start="intro")

arrangement = composition.form_freeze()       # the walk, frozen into a Form value
print(arrangement.describe())

# It's an ordinary Form now — edit it, then rebind it as a fixed running order.
composition.form(arrangement.replace(2, bars=16), at_end="stop")
Form — 6 sections over 40 bars
  1. bars 1–4  intro      (4 bars)  energy=0.5
  2. bars 5–12  verse      (8 bars)  energy=0.5
  3. bars 13–20  chorus     (8 bars)  energy=0.5
  4. bars 21–28  verse      (8 bars)  energy=0.5
  5. bars 29–36  chorus     (8 bars)  energy=0.5
  6. bars 37–40  outro      (4 bars)  energy=0.5

form_freeze() walks a clone of the live form state using the same seeded RNG, so the frozen path is exactly the path the graph would have played — the live form is untouched until you rebind. That closes the explore-then-capture loop at the level of structure: jam with a graph form, freeze the take you like, edit it into a finished Form, and you have a fixed arrangement you can render the same way every time.

Note

Under the hood: three clocks, one idea. A playing composition now runs three read-only context clocks in step — the beat clock (where in the bar, Chapter 1), the harmonic clock (which chord, Chapter 6), and the form clock (which section, this chapter). A pattern reads all three — p.bar, the injected chord, p.section — and decides what to play. None of them make sound; each is a specification resolved late, answered fresh every cycle. The form clock is simply the slowest of the three, governing the others: it can swap the harmonic clock’s chords (section_chords) and gate the patterns (energy, min_energy). Same one idea — context resolved late — scaled from the sixteenth-note up to the whole track.


You can now arrange a track: define its form as a fixed list, an editable Form value, or a self-deciding weighted graph; read p.section to shape patterns by name, progress, and lookahead; dial parts in and out with energy and min_energy=; fire fills and mutes at the boundaries; bind a progression and a lead phrase to each section; and steer or freeze the form live. With structure in hand, the next chapter turns to the producer’s workflow — pinning a whole piece to one seed, locking and rerolling parts, and freezing a generative take into something you can ship.