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 (p and everything you call on it)

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

p.cycle

How many times this pattern has rebuilt — a counter from 0, +1 each cycle.

p.bar

The global bar number of the whole composition, from 0.

p.bar_cycle(length)

This bar’s place inside a repeating group of length bars (see below).

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:

  • .firstTrue on the opening bar of the group (bar 0 of the cycle).

  • .lastTrue on the closing bar of the group.

  • .progress — how far through the group we are, from 0.0 rising toward 1.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

bar_cycle(), BarCycle

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.

2.5 Sharing state with p.data

Module-level values let one pattern remember across cycles. But a composition usually has several patterns, and often one should react to another: a bass that gets out of the way when the drums play a fill, a hat that follows the kick’s intensity. They need a shared place to leave notes for each other — and that place is p.data.

p.data is a plain Python dictionary that every pattern shares. It is the very same object as composition.data, so a value one pattern writes is visible to the others. You write to it like any dict (p.data["name"] = value) and read from it with p.data.get("name", default) (the default is what you get back if no one has written that key yet).

The timing that makes this work: patterns rebuild top to bottom, in the order you defined them. So a pattern defined earlier in your file runs before one defined later in the same cycle — which means an earlier writer’s value is ready for a later reader to pick up, that same bar.

Here the drums pattern decides each bar whether it’s a fill bar and leaves that decision in p.data; the bass, defined below it, reads the flag and drops out on fill bars to leave room:

composition = subsequence.Composition(bpm=120)

# Defined FIRST — so it writes p.data before the bass reads it this cycle.
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
    fill_bar = p.bar_cycle(4).last        # True on the 4th bar of each phrase
    p.data["fill_bar"] = fill_bar         # leave the decision for later patterns

    p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
    p.hit_steps("snare_1", [4, 12], velocity=90)
    if fill_bar:
        p.hit_steps("snare_1", [10, 11, 13, 14, 15], velocity=110)

# Defined SECOND — reads the flag the drums just wrote this cycle.
@composition.pattern(channel=1, beats=4)
def bass(p):
    if p.data.get("fill_bar", False):
        return                            # silent this bar — let the fill breathe
    p.note(36, beat=0, velocity=100, duration=1)
    p.note(36, beat=2, velocity=90, duration=1)

composition.render(bars=8, filename="hello.mid")

Render it and the bass plays its two notes for three bars, then sits out the fourth bar while the snare roll lands — every four bars. The two patterns never call each other; they coordinate purely through one shared value.

Note

Note p.note(36, beat=0, ...) always names a beat. Unlike a drum hit on the grid, a melodic note needs to know where in the bar it lands, so beat= is required — beat 0 is the downbeat, 2 is the third quarter. We’ll dig into beats and durations properly in Chapter 3; here the bass is just enough to hear two patterns react to each other.

Warning

The writer-before-reader guarantee depends on definition order: when two patterns rebuild together — same beats, so they share a cycle, as drums and bass do here — the one defined above runs first, and its fresh value is ready for the one below. If you instead read a key the writer hasn’t set yet this cycle, you get the previous cycle’s value (or the default). So when order matters, put the writer first — and always pass a sensible default to .get() for the very first cycle, before any key exists.

Tip

p.data isn’t only for pattern-to-pattern talk. Because it’s the same dict as composition.data, it’s also where outside information lands — a value mapped from a hardware knob, or data you set before playback. The patterns don’t care where a value came from; they just read the dict. That makes p.data the seam between your music and the world, a thread we pick up much later when we sonify external data.


You now have the engine’s heartbeat: a pattern is a function the engine re-runs every cycle onto a fresh canvas, while your values — at module level or in p.data — persist and let bars differ and patterns coordinate. In Chapter 3 we leave the drum grid behind and write actual pitched material: notes at chosen beats, with real durations, including the hand-written bassline this chapter only sketched.