Chapter 2 · The Rebuild Loop: Patterns That Remember¶
In Chapter 1 you painted a beat onto the step grid by hand. This chapter explains when that painting actually happens — and uses that one idea to make the loop change from bar to bar, and to let two patterns talk to each other.
So far your drums function has felt like a recipe you wrote once. The surprise —
and the whole engine, really — is that Subsequence runs that function again at
the start of every bar. Once you can see that, a pattern stops being a fixed
loop and becomes something that can react: a fill on the fourth bar, a busier hat
on odd bars, a bass that ducks out of the way of a snare roll. Everything in the
rest of the guide is built on this single mechanism.
2.1 Your function re-runs every cycle¶
When you decorate a function with @composition.pattern(...), you are not handing
Subsequence a finished loop. You are handing it a function — and the engine
calls that function fresh at the start of every cycle. A “cycle” is one pass
of the pattern: for a 4-beat (one-bar) pattern, that’s once per bar.
The clearest way to see it is to print something each time the function runs. Add one line to the Chapter 0 drum loop:
import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
composition = subsequence.Composition(bpm=120)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
print(f"building cycle {p.cycle}") # runs once per bar
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.hit_steps("snare_1", [4, 12], velocity=90)
p.hit_steps("hi_hat_closed", range(16), velocity=70)
composition.render(bars=4, filename="hello.mid")
Rendering four bars prints:
building cycle 0
building cycle 1
building cycle 2
building cycle 3
building cycle 4
The function ran five times — once to build each of the four bars, plus one more to have the next bar ready at the boundary. That last extra build is normal: the engine always builds one cycle ahead so the music never stutters. You can ignore it; just know that the count you see is “builds”, not “bars heard”.
p.cycle is that rebuild counter: a plain integer, starting at 0, that goes
up by one every time your function is called. It’s the engine’s way of telling you
“this is the nth time I’ve run you.”
Note
This is why Subsequence is called a generative sequencer rather than a looper. A looper records sixteen steps and replays the same sixteen for ever. Subsequence re-derives the bar each time from your code, so anything your code can compute — a random choice, a count, the current position — can change what plays. A loop that re-runs is a loop that can evolve.
2.2 p is a fresh canvas (values vs canvas)¶
Here is the part that trips up everyone exactly once. Each time the engine calls
your function, the p it hands you is brand new — a blank canvas with no notes
on it. Nothing you painted last bar carries over. Every p.hit_steps(...) line
you write is starting from an empty bar and filling it in again.
That sounds wasteful, but it’s the source of all the power: because the canvas is empty every time, you decide from scratch what goes on it, and you can decide differently depending on the cycle.
It also means there are two very different kinds of “stuff” in a pattern, and keeping them straight is the key skill of this chapter:
Kind |
Lifetime |
|---|---|
The canvas ( |
Thrown away and rebuilt every cycle. Only good for this bar. |
Values (numbers, lists, strings — your data) |
Last as long as you keep them. Put them where they’ll survive. |
A natural instinct, coming from other tools, is to try to hold on to p — to
stash it in a variable and add more notes to it next time. Don’t. The p from
last cycle is gone; the engine has already discarded it and made a new one. Trying
to reuse it does nothing useful.
Warning
Never hoard the builder. Treat p as borrowed for the length of one function
call — paint this bar and let it go. Anything you want to remember between bars
must be a plain value, stored somewhere that outlives the call: a module-level
variable (§2.4) or the shared p.data dict
(§2.5). Store values, never the canvas.
For now, notice that the drum loop already obeys this rule perfectly — it builds the same fresh bar every cycle and keeps nothing:
composition = subsequence.Composition(bpm=120)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
# p is a blank canvas this cycle; nothing carries over from last bar.
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.hit_steps("hi_hat_closed", range(16), velocity=70)
composition.render(bars=4, filename="hello.mid")
Because the canvas is fresh and your code is in charge of filling it, the obvious next move is to fill it differently depending on where we are in the song. That needs a way to ask “where am I?” — which is exactly what the next section gives you.
2.3 Reacting to position (p.cycle, p.bar, p.bar_cycle)¶
Your function gets three readings of “where am I in time”, all on p:
Reading |
Meaning |
|---|---|
|
How many times this pattern has rebuilt — a counter from 0, +1 each cycle. |
|
The global bar number of the whole composition, from 0. |
|
This bar’s place inside a repeating group of |
For a one-bar pattern, p.cycle and p.bar happen to be the same number. They
differ when a pattern is longer than a bar: a two-bar pattern rebuilds once every
two bars, so its p.cycle ticks 0, 1, 2… while p.bar jumps 0, 2, 4…. The rule
of thumb: p.cycle counts your rebuilds; p.bar counts the song’s bars. Most
of the time you’ll reach for whichever reads more naturally.
Branching on the cycle¶
Since p.cycle is just a number, ordinary Python decides what to paint. To make
odd bars busier than even ones, test whether the cycle is even with % 2 (the
remainder operator: cycle % 2 is 0 on even cycles, 1 on odd):
composition = subsequence.Composition(bpm=120)
@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.cycle % 2 == 0:
p.hit_steps("hi_hat_closed", [0, 4, 8, 12], velocity=70) # even bars: sparse
else:
p.hit_steps("hi_hat_closed", range(16), velocity=70) # odd bars: busy
composition.render(bars=4, filename="hello.mid")
The two branches paint onto the same fresh canvas; only one runs each cycle, so the bar alternates sparse, busy, sparse, busy.
Phrasing with bar_cycle¶
Music is rarely “every other bar” — it’s “the last bar of every four”, “the first
bar of the section”. Writing that with raw remainders (p.bar % 4 == 3) works but
reads poorly. p.bar_cycle(length) says it in plain musical language. It returns a
small object describing this bar’s position within a repeating group of length
bars, with three readable properties:
.first—Trueon the opening bar of the group (bar 0 of the cycle)..last—Trueon the closing bar of the group..progress— how far through the group we are, from0.0rising toward1.0(for a 4-bar group:0.0, 0.25, 0.5, 0.75). Handy for gradual build-ups.
The classic use is a fill on the fourth bar of every four-bar phrase:
composition = subsequence.Composition(bpm=120)
@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("snare_1", [4, 12], velocity=90)
p.hit_steps("hi_hat_closed", range(16), velocity=70)
# On the last bar of every four, add a snare roll into the next phrase.
if p.bar_cycle(4).last:
p.hit_steps("snare_1", [10, 11, 13, 14, 15], velocity=110)
p.hit_steps("hi_hat_open", [14], velocity=100)
composition.render(bars=8, filename="hello.mid")
Render eight bars and you’ll hear the steady groove for three bars, a roll on the fourth, then the cycle resets and it happens again — the loop you wrote in Chapter 1, now phrased.
Tip
p.bar_cycle(4).first and p.bar_cycle(4).last are the two you’ll use constantly:
the downbeat of a phrase and its turnaround. Reach for .progress when you want a
gradual change rather than an on/off one — for example feeding it into a velocity
ramp so the energy lifts across the four bars.
Reference
2.4 Module-level reusable values¶
The branches above carry literal lists like [10, 11, 13, 14, 15] right where
they’re used. That’s fine for one, but once the same step list appears in two
patterns — or you want to tweak the kick pattern in one place — it’s cleaner to
name the value once, outside the function.
A variable written at the top level of your file (not indented inside any function) is a module-level value. It is created once, when the file is first read, and it stays put. Every cycle your pattern function can read it — but, crucially, the engine never resets it, because it isn’t part of the throwaway canvas. It’s a value, so it lasts (§2.2).
composition = subsequence.Composition(bpm=120)
# Named once, at module level — reused by the pattern on every cycle.
FOUR_ON_FLOOR = [0, 4, 8, 12]
BACKBEAT = [4, 12]
SNARE_FILL = [10, 11, 13, 14, 15]
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", FOUR_ON_FLOOR, velocity=100)
p.hit_steps("snare_1", BACKBEAT, velocity=90)
p.hit_steps("hi_hat_closed", range(16), velocity=70)
if p.bar_cycle(4).last:
p.hit_steps("snare_1", SNARE_FILL, velocity=110)
composition.render(bars=8, filename="hello.mid")
This reads almost like a score: the names tell you what each list is, and the
pattern body tells you when it plays. Names in capitals (FOUR_ON_FLOOR) are a
Python convention for “this is a fixed value I set up front and don’t change” —
exactly what a reusable step list is.
Because a module-level value survives between cycles, it can also remember
across them. A running count is the simplest example. To change a module-level
value from inside a function, Python needs one extra word — global — to confirm
you mean the outer variable and not a new local one:
composition = subsequence.Composition(bpm=120)
hit_total = 0 # lives at module level, so it survives every rebuild
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
global hit_total # "I mean the outer hit_total, not a new one"
steps = [0, 4, 8, 12]
hit_total += len(steps) # accumulates across cycles
p.hit_steps("kick_1", steps, velocity=100)
composition.render(bars=4, filename="hello.mid")
print(hit_total)
20
Five cycles built (four bars plus the build-ahead), four kicks each, gives 20.
The counter survived every rebuild precisely because it’s a value living outside
the canvas — the same reason your step lists do.
Important
This is the practical shape of “store values, never hoard p”: data you want to
keep — step lists, counts, a chosen scale, anything — lives at module level (or in
p.data, next). The builder p stays disposable. Get this division right and
patterns that evolve become straightforward; get it wrong and you’ll fight the
engine resetting things you expected to persist.