Chapter 1 · The Step Grid: Writing a Beat by Hand

In Chapter 0 you got a four-on-the-floor loop playing; now we slow down and learn the step grid properly — placing every hit by hand, shaping how hard each one lands, and meeting the one indexing quirk that trips up everyone.

This whole chapter stays with a single, fixed drum loop. Nothing here changes from bar to bar yet — that begins in Chapter 2, and the generators come later still. For now we’re learning to write down exactly the beat in your head, note for note, so that when patterns start to move you already know what they’re standing on.

1.1 The Composition object

Every Subsequence piece begins with one Composition. It’s the conductor: it holds the tempo, owns the connection to your instrument, and runs the clock that every pattern plays against. You make exactly one per script.

import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums

composition = subsequence.Composition(bpm=120)

The only argument we’re using is bpm — beats per minute, the tempo. A few more are worth knowing about now, even though we won’t lean on them until later chapters:

Argument

What it does

bpm=120

Tempo in beats per minute. The default is 120.

output_device="..."

The exact MIDI port name to send to (from §0.5). Omit it and Subsequence uses the only port, or asks you to pick.

time_signature=(4, 4)

Beats per bar and the beat unit. Defaults to 4/4; we stay in 4/4 here.

seed=...

A fixed number that makes every random choice repeatable. We have no randomness to pin down yet, so we leave it off until Chapter 2.

Note

Creating a Composition does not make any sound or open your instrument yet. It just sets up the conductor. Sound (or a MIDI file) only happens when you call composition.play() or composition.render() at the very end of your script.

A Composition on its own is an empty stage. To put music on it, you add patterns — and that’s what the decorator on the next line does.

Reference

Composition

1.2 The @composition.pattern decorator

A pattern is one repeating part — your kick-and-snare loop, a bassline, a hi-hat line. You write a pattern as an ordinary Python function, and you mark that function as a pattern by putting @composition.pattern(...) on the line directly above it. That @-line is called a decorator.

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

composition.render(bars=4, filename="beat.mid")

If you’ve not met decorators before, here’s the whole idea in one sentence: a decorator is a label that hands your function to something else to look after. You are not calling drums() yourself — you’re handing it to the composition and saying “this function describes a pattern; run it for me each loop.” The composition keeps the function and calls it on your behalf, once per cycle, for as long as the piece plays.

The three arguments in the decorator set up the pattern’s world:

  • channel=10 — which MIDI channel this part plays on. Channel 10 is the General MIDI drum channel, so any GM-compatible instrument treats these notes as drums. (Channel numbering has a twist; that’s §1.3.)

  • beats=4 — how long one loop of the pattern is, in beats. Four beats is one bar of 4/4. The pattern repeats every four beats for the life of the piece.

  • drum_note_map=gm_drums.GM_DRUM_MAP — the dictionary that turns drum names into MIDI note numbers. With it in place you can write "kick_1" instead of remembering that the kick is MIDI note 36.

Inside the function, the one argument p is your pattern builder — the palette you paint notes onto. Every drum hit, every bit of velocity shaping, every articulation in this chapter is a method on p. You’ll call p.something(...) a lot; think of p as “the bar I’m currently filling in.”

Tip

Why drum names instead of numbers? Writing "snare_1" says what you mean; 38 makes you look it up. The drum_note_map is just a lookup table — Subsequence ships GM_DRUM_MAP for the General MIDI standard, but if your drum machine lays its sounds out differently you can supply your own map and keep using friendly names. Musical name first, wiring second.

The GM_DRUM_MAP also provides short primary aliases: "kick", "snare", "crash", and "ride" are the same as "kick_1", "snare_1", "crash_1", and "ride_1". Use whichever reads better to you; we’ll mostly spell out the numbered form so it’s unambiguous.

Reference

pattern(), render()

1.3 Channels are 1-indexed, steps are 0-indexed (the gotcha)

Here is the one quirk that catches everybody once. Two different things in that last example were counted two different ways, and you have to keep them straight:

  • MIDI channels are counted from 1. Channel 10 is the drum channel, exactly as it’s labelled on the back of every drum machine and in every DAW. There is no channel 0.

  • Grid steps are counted from 0. The first step of the bar is step 0, not step 1. So a four-beat bar of sixteenth notes has steps numbered 0 through 15, and the four downbeats — beats 1, 2, 3, 4 — live at steps 0, 4, 8, 12.

Important

Channels start at 1; steps start at 0. Channels match the numbers on your hardware (musicians and gear count from 1). Steps follow the programming convention of counting from 0, like most things inside code. Mixing them up is the classic first-week mistake: writing channel=9 and getting silence, or expecting [1, 5, 9, 13] to be your downbeats.

The reason for the split is that each system is borrowed from a different world. Channel numbers come from MIDI hardware, where channel 10 has meant drums since the 1990s, so Subsequence keeps that label so your code matches your gear. Step indices come from Python lists, which start at 0 — and since you’ll soon be generating step lists with code, it pays to count them the way the code does.

Mapping the two ways of counting onto one bar:

Musician says

Beat

Step index

In a sixteenth-note bar

“beat 1”

1

0

the downbeat

“beat 2”

2

4

the backbeat (snare)

“beat 3”

3

8

“beat 4”

4

12

the other backbeat (snare)

“the ‘e’ of 1”

1.25

1

first sixteenth after the downbeat

Note

If you’d genuinely rather count channels from 0 to match the raw MIDI protocol, pass zero_indexed_channels=True when you create the Composition — then the drum channel is 9. We keep the default (1-indexed) throughout this guide because it matches what’s printed on instruments. Steps are always 0-indexed; there’s no switch for that.

1.4 Placing hits with hit_steps

p.hit_steps() is the workhorse of hand-written drumming. It places a short hit of one sound on a list of grid steps. The shape is:

p.hit_steps(pitch, steps, velocity=...)
  • pitch — the drum, as a name ("kick_1") or a raw MIDI number.

  • stepswhich steps to hit, as a list of step indices.

  • velocity — how hard, 1–127 (covered in §1.5).

The middle argument is where you actually compose the rhythm. A few ways to write a step list, all doing the same kind of thing:

@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)   # an explicit list
    p.hit_steps("snare_1", [4, 12], velocity=90)          # just the backbeats
    p.hit_steps("hi_hat_closed", range(16), velocity=70)  # every one of 0..15

composition.render(bars=4, filename="beat.mid")

That range(16) is a tidy Python shorthand for “the numbers 0 up to but not including 16” — i.e. 0, 1, 2, 15, all sixteen steps. It saves you typing the full list, and it makes the musical intent (“hit every sixteenth”) obvious. range is the one bit of Python worth memorising for rhythms: range(16) is every sixteenth, range(0, 16, 2) is every eighth (start at 0, step by 2), and range(0, 16, 4) is every quarter.

Tip

A list is a rhythm written down. [0, 4, 8, 12] is “four on the floor.” [4, 12] is “the backbeat.” Reading a step list out loud — “zero, four, eight, twelve” — and tapping along is the fastest way to check you wrote the groove you meant. You can build that list however you like: type it out, use range, or (from Chapter 4) have a generator hand you one.

Each hit_steps call adds to the bar — it doesn’t replace what came before. So the three lines above layer kick, snare, and hi-hat onto the same four beats. The order you write them in doesn’t matter; what matters is which steps each one lands on.

Let’s make the loop unmistakably a beat rather than a metronome — add an open hi-hat to lift the last upbeat, a clap doubling the snare, and a tom fill into the turnaround. Every sound here is a named GM drum:

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
    p.hit_steps("kick_1", [0, 8, 10], velocity=110)        # syncopated kick
    p.hit_steps("snare_1", [4, 12], velocity=95)            # backbeat
    p.hit_steps("hand_clap", [4, 12], velocity=80)          # clap doubling the snare
    p.hit_steps("hi_hat_closed", range(16), velocity=60)    # steady sixteenths
    p.hit_steps("hi_hat_open", [14], velocity=90)           # open hat on the last 'and'
    p.hit_steps("low_tom", [15], velocity=100)              # a single tom pickup

composition.render(bars=4, filename="beat.mid")

Note

The note never sounds until you render (or play). A pattern function only describes a bar; its body runs when the composition asks it to. That’s why every runnable example in this chapter ends with composition.render(...) — that line is what actually executes drums and writes the file. Swapping render for composition.play() performs the same loop live (see §0.8); we use render here because it needs no instrument and finishes instantly.

Reference

hit_steps()

1.5 Velocity and humanisation

Velocity is how hard a note is struck, from 1 (barely there) to 127 (full force). On a drum it’s the difference between a ghost note and an accent; flat velocities are the giveaway of a machine. You set it three ways, from bluntest to most lifelike.

1. A single number — every hit at the same strength. Fine for a foundation like the kick, where you want consistency:

@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)   # rock-solid, every hit at 100

composition.render(bars=2, filename="beat.mid")

2. A (low, high) tuple — a fresh random draw for each note. Write velocity=(60, 90) and every hit independently rolls a value somewhere between 60 and 90. This is the quickest way to take the stiffness out of a hi-hat line:

@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=105)
    p.hit_steps("snare_1", [4, 12], velocity=95)
    p.hit_steps("hi_hat_closed", range(16), velocity=(55, 85))  # each hat its own dynamic

composition.render(bars=4, filename="beat.mid")

Important

The tuple is one independent draw per note, not one value shared across the line. Sixteen hi-hats with velocity=(55, 85) give sixteen different velocities, each landing somewhere in that band — exactly the small, uncorrelated wobble a real hand produces. The two numbers are a range; low must be ≤ high.

3. p.velocity_shape(low, high) — shape the dynamics of everything already placed in the pattern, spreading them evenly across the range instead of rolling each one independently. Where the tuple is per-note randomness, velocity_shape is a deliberate, even spread that often sounds more “played” than pure chance. Call it bare (p.velocity_shape()) and it uses a sensible default range of 64–127:

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
    p.hit_steps("hi_hat_closed", range(16), velocity=70)  # placed flat first…
    p.velocity_shape(50, 100)                              # …then spread 50–100

composition.render(bars=4, filename="beat.mid")

There’s a real difference between the last two, worth feeling out:

Tool

Use it when

velocity=(low, high) on a hit_steps call

You want each hit of that one sound to vary independently — humanising a single line as you place it.

p.velocity_shape(low, high)

You want to re-shape the dynamics of the whole pattern after placing it, as an even, organic spread across a range.

Warning

p.velocity_shape() rewrites the velocity of every note already in the pattern — kick, snare, hats, all of them — to fit the new range. Call it after you’ve placed your hits, and know that it overrides the per-hit velocities you set. If you only want to humanise the hi-hats, prefer the (low, high) tuple on that one hit_steps line.

Reference

velocity_shape()

1.6 Note length: legato and detached

A drum hit is usually short, but length still matters — a clipped closed hat versus an open hat that rings into the next beat, or a synth-drum tom you let bloom. Three tools control how long each note holds.

duration= on the hit sets a fixed length, in beats, for those hits. 1.0 is a quarter-note’s worth; 0.25 is a sixteenth; the default for hit_steps is very short (a tight 0.1), which is what you want for most percussion:

@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)         # short, default-ish
    p.hit_steps("hi_hat_open", [2, 6, 10, 14], velocity=80, duration=0.5)  # let it ring

composition.render(bars=2, filename="beat.mid")

p.legato() stretches every note so it lasts right up to the next one — no gap. On sustaining sounds this glues the line together; on an open-hat line it makes each hat ring until the following hit cuts it off:

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
    p.hit_steps("hi_hat_open", [0, 4, 8, 12], velocity=75)
    p.legato()   # each hat now holds until the next one starts

composition.render(bars=2, filename="beat.mid")

p.detached() is the opposite: it shortens every note so there’s always a guaranteed sliver of silence before the next onset. This is the classic crisp, staccato feel — and it’s also a safety margin on monophonic gear, making sure one note fully releases before the next retriggers:

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
    p.hit_steps("snare_1", [4, 12], velocity=95, duration=0.5)
    p.detached(0.1)   # trim each note so 0.1 beat of silence precedes the next

composition.render(bars=2, filename="beat.mid")

Tip

Think of the pair as a single dial. legato() fills the gap to the next note (fully connected); detached() guarantees a gap before the next note (cleanly separated). For one fixed length regardless of spacing, set duration= on the hit instead. Like velocity_shape, both legato() and detached() act on everything already placed, so call them after your hit_steps lines.

Reference

legato(), detached()

1.7 Reading the terminal display

When you perform a piece live with composition.play(), Subsequence can paint a small dashboard at the bottom of your terminal so you can see what’s happening without leaving the keyboard. You turn it on with composition.display(), just before play():

# A LIVE feature — pair it with play(), not render(). Illustrative only.
composition.display(grid=True)   # status line + a grid of the running patterns
composition.play()               # Ctrl-C to stop

Note

The display is a live-playback feature, so the snippet above is shown for reference rather than run as one of our checked examples — there’s no terminal to draw into during a headless render(). Everything it shows, though, comes from the exact same patterns you’ve been writing in this chapter.

With just composition.display(), you get a status line pinned to the bottom that updates as the music moves — tempo, key, the bar you’re on, and (once you add harmony, in Part IV) the current chord:

120.00 BPM  Bar: 3.1

Add grid=True and Subsequence draws an ASCII grid above that line, one row per drum sound, updated each bar. It’s a live readout of the very loop you hand-wrote — a quick way to confirm your kick really is on the downbeats and your hats really are every sixteenth:

kick_1          |█ · · · █ · · · █ · · · █ · · ·|
snare_1         |· · · · █ · · · · · · · █ · · ·|
hi_hat_closed   |▓ ▒ ▓ ▒ ▓ ▒ ▓ ▒ ▓ ▒ ▓ ▒ ▓ ▒ ▓ ▒|

Reading the grid:

  • Each row is one sound; each column is one grid step, left to right across the bar — so the columns are exactly your 0-indexed steps from §1.4.

  • A · is an empty step; a filled block is a hit, and the shade shows velocity — a light is soft, a solid is hard. You can see the humanised hi-hats from §1.5 as the shades flicker between and from step to step.

Tip

The grid is the fastest sanity check there is. If a row is empty when you expected hits, you probably wrote the wrong channel or drum name; if the hits are in the wrong columns, re-read your step list remembering that the first column is step 0.

Reference

display()


You can now write any fixed beat by hand — placing named hits on explicit steps, humanising velocity, and shaping note length — with the channel/step indexing firmly straight. In Chapter 2 we take the lid off the loop: your pattern function actually re-runs every cycle, which is the doorway to patterns that change over time.