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 |
|---|---|
|
Tempo in beats per minute. The default is 120. |
|
The exact MIDI port name to send to (from §0.5). Omit it and Subsequence uses the only port, or asks you to pick. |
|
Beats per bar and the beat unit. Defaults to 4/4; we stay in 4/4 here. |
|
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
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.
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 step1. 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 steps0, 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 |
|
the downbeat |
“beat 2” |
2 |
|
the backbeat (snare) |
“beat 3” |
3 |
|
|
“beat 4” |
4 |
|
the other backbeat (snare) |
“the ‘e’ of 1” |
1.25 |
|
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.steps— which 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
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 |
|---|---|
|
You want each hit of that one sound to vary independently — humanising a single line as you place it. |
|
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
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
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
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.