# Chapter 10 · Form, Sections, and Energy: Arranging a Track In [Chapter 9](09-phrases) 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](07-progressions)): 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. ```{testsetup} ch10 import subsequence from subsequence import Composition, Section, Form, progression, motif, sentence import subsequence.constants.instruments.gm_drums as gm_drums import subsequence.constants.durations as dur ``` (sec-defining-form)= ## 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: ```{testcode} ch10 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](06-harmony-context): 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](#sec-reading-section) 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: ```{list-table} :header-rows: 1 :widths: 22 78 * - `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. ``` ```{testcode} ch10 # 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") ``` (sec-form-value)= ### 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 `+`: ```{testcode} ch10 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()) ``` ```{testoutput} ch10 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 `Section`s, a list of `(name, bars)` tuples, or a finished `Form` value — they're the same form. ``` (sec-graph-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](06-harmony-context). 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: ```{testcode} ch10 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](04-generators-euclidean) and [§7.1](07-progressions), now governing the shape of the whole track. ``` ```{list-table} Which form shape? :header-rows: 1 :widths: 30 70 * - 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](#sec-live-nav)). ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.form`, {py:class}`~subsequence.forms.Section`, {py:class}`~subsequence.forms.Form`, {py:meth}`~subsequence.forms.Form.replace` ``` (sec-reading-section)= ## 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":* ```{testcode} ch10 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](02-rebuild-loop)) 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: ```{list-table} :header-rows: 1 :widths: 26 74 * - 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](#sec-energy). ``` `.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: ```{testcode} ch10 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: ```{testcode} ch10 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](#sec-boundaries) automates the fill so you rarely hand-write this, but it's worth seeing the raw reading once. ``` ```{admonition} Reference :class: seealso {py:class}`~subsequence.form_state.SectionInfo` ``` (sec-energy)= ## 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](#sec-form-value)); or — the performance-level dial — you name the levels in one dict with **`composition.energy(...)`**: ```{testcode} ch10 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: ```{testcode} ch10 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: ```{testcode} ch10 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: ```{testcode} ch10 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") ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.energy` ``` (sec-boundaries)= ## 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](#sec-reading-section) 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](08-motifs)) 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: ```{testcode} ch10 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](08-motifs) — 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: ```{testcode} ch10 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: ```{list-table} :header-rows: 1 :widths: 24 76 * - `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). ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.transition`, {py:meth}`~subsequence.motifs.Motif.hits` ``` (sec-binding)= ## 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](07-progressions)) 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: ```{testcode} ch10 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](06-harmony-context). 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](07-progressions) 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](07-progressions). ``` ### 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(...)`**: ```{testcode} ch10 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. ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.section_chords`, {py:meth}`~subsequence.composition.Composition.section_motifs`, {py:meth}`~subsequence.composition.Composition.phrase_part`, {py:meth}`~subsequence.pattern_builder.PatternBuilder.section_motif` ``` (sec-live-nav)= ## 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: ```python # 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): ```{testcode} ch10 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") ``` ```{testoutput} ch10 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](#sec-graph-form)) 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](07-progressions)). You can inspect it, edit it, and rebind it as a fixed arrangement: ```{testcode} ch10 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") ``` ```{testoutput} ch10 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](01-step-grid)), the **harmonic clock** (which chord, [Chapter 6](06-harmony-context)), 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. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.form_jump`, {py:meth}`~subsequence.composition.Composition.form_next`, {py:meth}`~subsequence.composition.Composition.on_section`, {py:meth}`~subsequence.composition.Composition.form_freeze` ``` --- 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.