Chapter 3 · Notes, Beats, and Durations

So far every hit has been a drum on a grid step. This chapter is where the sequencer learns to sing: we’ll place individual pitched notes at exact beat positions, give each one a length, and build a bassline by hand that locks to the Chapter 0 drum loop.

3.1 p.note and the required beat

A drum hit is a moment — it has no length worth speaking of. A bass note is different: it starts somewhere, sounds for a while, and then stops. The verb for placing one pitched note is p.note, and its full shape is:

p.note(pitch, beat, velocity, duration)
  • pitch — which note, as a MIDI number (0–127, with 60 = middle C) or a named constant we’ll meet in §3.5.

  • beatwhere in the bar it starts, counted in beats from 0.0. This argument is required. A note with no position is meaningless, so Subsequence insists you say when it sounds.

  • velocity — how hard it plays (1–127), defaulting to 100.

  • duration — how long it rings, in beats, defaulting to 0.25 (a sixteenth note). 1.0 is a full quarter note; 0.5 an eighth; 2.0 a half note.

Here is a complete, runnable example — one bass note per beat, each a quarter note long. It shows the imports in full so you can copy it as a standalone script.

import subsequence
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120)

@composition.pattern(channel=1, beats=4)
def bass(p):
    p.note(notes.C2, beat=0, duration=1.0)   # downbeat
    p.note(notes.C2, beat=1, duration=1.0)
    p.note(notes.G2, beat=2, duration=1.0)
    p.note(notes.C2, beat=3, duration=1.0)

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

Reading the new parts:

  • channel=1 sends to MIDI channel 1 — a melodic instrument, not the drum channel. (Remember from Chapter 0: channels are 1-indexed, so 1 is the first channel and 10 is the General MIDI drum channel.) Point a bass synth or a DAW track at channel 1 and you’ll hear these notes.

  • beats=4 makes the pattern four beats long — one bar of 4/4.

  • notes.C2 is the named constant for the MIDI note C2; we’ll unpack the naming in §3.5. For now read it as “the C two octaves below middle C,” a comfortable bass register.

  • Each p.note(...) places exactly one note. The beat= is spelled out every time because it’s required and because being explicit reads clearly.

Warning

beat is not optional. Writing p.note(notes.C2) with no position raises TypeError: ... missing 1 required positional argument: 'beat'. Older drafts of Subsequence let you drop it; current versions do not. Always give a beat: p.note(pitch, beat=...).

Tip

A negative beat counts back from the end of the pattern. In a 4-beat bar, beat=-0.5 lands on the “and” of beat 4 (i.e. beat 3.5) — handy for placing a note near the end of the bar without doing the subtraction yourself.

Reference

note()

3.2 Beats vs grid steps

Chapters 0 and 1 placed drums on grid steps — whole numbers 0..15. This chapter places notes on beats0.0, 1.0, 2.0 ..., fractions allowed. These are two ways of naming the same timeline, and keeping them straight is the one idea this chapter really turns on.

  • A beat is musical time measured in quarter notes. Beat 0.0 is the downbeat; beat 1.5 is the “and” of beat 2; beat 2.25 is the first sixteenth after beat 3. Beats are what p.note, p.hit, and p.repeat speak.

  • A step is a slot on a fixed grid. A 4-beat pattern is divided into 16 sixteenth-note steps numbered 0..15. Steps are what p.hit_steps and p.sequence speak.

The bridge between them is simple. In a 4-beat bar the grid is 16 steps, so each step is 4 / 16 = 0.25 beats wide, and:

beat = step × 0.25 (in a standard 4-beat, sixteenth-grid pattern)

The same eight moments, named two ways

Step (0–15)

Beat (0.0–)

In words

0

0.0

downbeat (beat 1)

2

0.5

the “and” of beat 1

4

1.0

beat 2

6

1.5

the “and” of beat 2

8

2.0

beat 3

12

3.0

beat 4

14

3.5

the “and” of beat 4

15

3.75

the last sixteenth

So the Chapter 0 kick on steps [0, 4, 8, 12] is exactly a note on beats [0, 1, 2, 3] — four-on-the-floor either way. Use steps when you’re thinking in a fixed grid of equal slots (most drum work); use beats when you want a note at an exact musical position, including off-grid ones like a triplet or a hair-early push.

Note

Why two systems at all? Grid steps make even, repetitive rhythms effortless — range(16) is sixteen hi-hats with no arithmetic. Beats make expressive placement effortless — beat=2.66 lands a note a third of the way through beat 3, which no whole-number step can name. You’ll reach for whichever fits the moment, and they always refer to the same underlying time.

3.3 A hand-written bassline (p.sequence)

Writing one p.note per note is clear but verbose. When you want several notes on a grid — different pitches, different velocities, different lengths — p.sequence lets you lay them out as parallel lists, the same way a hardware step sequencer shows a row of knobs per step.

p.sequence(steps, pitches, velocities, durations)

You give the steps that fire, then a pitch for each, and optionally a velocity and duration for each. Any of pitches, velocities, durations can be a single value (used for every step) or a list read in step order. Here’s a walking bassline under the Chapter 0 drums — root on the downbeat, a passing tone, the fifth, and a chromatic approach:

import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120)

# The Chapter 0 drum loop, unchanged, on channel 10.
@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)

# A bassline that locks to it, on channel 1.
@composition.pattern(channel=1, beats=4)
def bass(p):
    p.sequence(
        steps      = [0,        4,        8,        10,       12],
        pitches    = [notes.C2, notes.C2, notes.G2, notes.AS2, notes.A2],
        velocities = [110,      85,       100,      80,        95],
        durations  = [1.0,      0.5,      0.75,     0.25,      1.0],
    )

composition.render(bars=4, filename="bass-and-drums.mid")

What each list does, read down the columns:

  • steps — fires on steps 0, 4, 8, 10, 12 → beats 0, 1, 2, 2.5, 3.

  • pitches — C2, C2, G2, A♯2, A2. The root twice, the fifth, then a chromatic A♯ leading into the A.

  • velocities — the downbeat hits hardest (110); the off-beat passing note is softer (80), so the line breathes.

  • durations — the downbeat rings a full beat; the A♯ is a quick sixteenth (0.25) that gets out of the way of the note it leads to.

Because the bass shares the beats=4 length with the drums, step 0 of the bass and step 0 of the kick are the same instant — the line is glued to the four-on-the-floor without any extra effort.

Note

The bass uses its own channel (1), so it goes to a melodic synth while the drums stay on channel 10. Two patterns on two channels play together; render writes both into the one MIDI file, each tagged with its MIDI channel — so your DAW can split them onto separate instruments by channel on import.

Tip

A single value broadcasts to every step. p.sequence([0, 4, 8, 12], notes.C2, durations=1.0) is four quarter-note C2s — one pitch and one duration applied to all four steps. Reach for lists only where the steps actually differ.

Tip

Tile a repeating cell with *. When a per-step list is just a short cell repeated a whole number of times, plain Python list multiplication reads best: a two-step accent across sixteen steps is [100, 70] * 8, and [1, 0, 0] * 4 is [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]. It works on any per-step list — velocities, durations, a 0/1 rhythm cell — and there’s deliberately no helper for it; it’s just the * operator. When the target length isn’t an exact multiple of the cell, reach for subsequence.sequence_utils.tile(cell, length), which cycles and truncates to exactly length (e.g. tile([1, 0, 0], 16)).

Reference

sequence(), tile()

3.4 Durations, repeats, detached and legato

A note’s duration is its gate time — how long it sounds before it’s released. You can set it per-note (the duration= argument we’ve used), or shape every note in a pattern at once with three articulation verbs.

p.repeat fires one pitch at a steady spacing for the whole pattern — the “note repeat” of an MPC or Push. Give it a spacing in beats and it fills the bar:

composition = subsequence.Composition(bpm=120)

@composition.pattern(channel=1, beats=4)
def driving_bass(p):
    p.repeat(notes.C2, spacing=0.5, velocity=90, duration=0.4)  # eighth notes

composition.render(bars=2, filename="repeat-bass.mid")

spacing=0.5 places a note every half-beat — straight eighth notes — from beat 0 to the end of the bar. (spacing=0.25 would give sixteenths; spacing=1.0, quarters.) The duration=0.4 leaves a sliver of silence before each next onset, so the notes don’t slur together.

p.duration overrides the length of every note already placed, to one fixed value. Lay the notes down first, then stamp a global gate over them:

composition = subsequence.Composition(bpm=120)

@composition.pattern(channel=1, beats=4)
def stabby_bass(p):
    p.hit(notes.C2, [0, 1, 2, 3], velocity=95)  # four notes, default short length
    p.duration(0.25)                            # now make them all sixteenth-long stabs

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

Note

p.hit is the pitched cousin of p.hit_steps. Where hit_steps takes grid steps, p.hit(pitch, beats, ...) takes a list of beatsp.hit(notes.C2, [0, 1, 2, 3]) is one C2 on each beat. It’s the quickest way to drop the same pitch at several beat positions.

p.detached trims every note so a guaranteed gap of silence precedes the next onset — clean, separated articulation, and a safety margin on a monophonic synth that would otherwise choke when one note’s tail overlaps the next:

composition = subsequence.Composition(bpm=120)

@composition.pattern(channel=1, beats=4)
def mono_safe_bass(p):
    p.sequence([0, 4, 8, 12], notes.C2, durations=1.0)  # long quarter notes
    p.detached(0.1)  # each ends 0.1 beats before the next begins

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

Its opposite, p.legato, stretches each note to fill the gap up to the next onset, so the line plays smoothly with no holes — the bound, connected feel:

composition = subsequence.Composition(bpm=120)

@composition.pattern(channel=1, beats=4)
def smooth_bass(p):
    p.hit(notes.C2, [0, 2], velocity=90)  # two short notes
    p.legato()                            # each grows to meet the next

composition.render(bars=2, filename="legato.mid")
The three articulation verbs

Verb

What it does to every placed note

p.duration(beats)

Sets each note to one fixed length, ignoring what comes next.

p.detached(gap)

Shrinks each note to leave at least gap beats of silence before the next.

p.legato(ratio)

Resizes each note to fill the gap to the next (ratio=1.0 = fully connected; less = a fraction of the gap).

Important

These three verbs act on notes already placed, so call them after you’ve written the line. They reshape durations only — they never move a note’s start. Note length is about articulation; pitch is set with snap_to_scale, and timing feel with swing — we’ll meet both in later chapters. Don’t confuse length with either.

3.5 Note and duration constants

Bass notes written as bare MIDI numbers (36, 43, 45) work, but they read like a phone number. Subsequence ships named constants so you can write the music, not the arithmetic.

Pitches live in subsequence.constants.midi_notes. Each constant is named <Letter><Octave> for naturals and <Letter>S<Octave> for sharps, with C4 = 60 (middle C), the convention Ableton, Logic, and Reaper use:

import subsequence.constants.midi_notes as notes

print(notes.C4)    # middle C
print(notes.C2)    # two octaves down — bass register
print(notes.AS2)   # A-sharp 2 (the 'S' is sharp)
print(notes.G2)
60
36
46
43

So the walking line from §3.3C2, C2, G2, AS2, A2 — is just 36, 36, 43, 46, 45 spelled in a way you can read at a glance.

Note

Sharps use the S spelling: C♯4 is notes.CS4. There are no flat names — a flat is the enharmonic sharp a step lower (D♭4 is notes.CS4). When in doubt, reach for the sharp.

Durations live in subsequence.constants.durations, all measured in beats where QUARTER is 1.0:

import subsequence.constants.durations as dur

print(dur.SIXTEENTH, dur.EIGHTH, dur.QUARTER, dur.HALF)
print(dur.DOTTED_EIGHTH)        # 0.75 — an eighth and a half
print(4 * dur.SIXTEENTH)        # four sixteenths = one beat
0.25 0.5 1.0 2.0
0.75
1.0

Now the bassline reads in plain musical language — register in note names, lengths in note values:

import subsequence
import subsequence.constants.midi_notes as notes
import subsequence.constants.durations as dur

composition = subsequence.Composition(bpm=120)

@composition.pattern(channel=1, beats=4)
def named_bass(p):
    p.note(notes.C2, beat=0, duration=dur.QUARTER)
    p.note(notes.G2, beat=2, duration=dur.EIGHTH)
    p.note(notes.A2, beat=3, duration=dur.DOTTED_EIGHTH)

composition.render(bars=4, filename="named-bass.mid")

Tip

Multiplying a constant by a count expresses odd lengths clearly: 9 * dur.SIXTEENTH is “nine sixteenths” (2.25 beats) without you ever computing the decimal. The constants are ordinary numbers, so you can do arithmetic on them freely.

3.6 Legacy aside: mini-notation

Note

This section documents an older shorthand you may meet in other people’s code. It is not the path this guide teaches. Skip it on a first read — nothing later depends on it.

Some sequencers describe rhythm with a compact mini-notation string, and Subsequence keeps one for familiarity: p.seq. A space-separated string is spread evenly across the bar, ~ or . is a rest, [a b] subdivides a slot, and _ extends the previous note:

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 mini_drums(p):
    p.seq("kick_1 . snare_1 .")   # kick on 1, snare on 3, rests between

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

The string "kick_1 . snare_1 ." reads as four equal slots: kick, rest, snare, rest. It’s terse, and for a quick rhythmic sketch it can feel fast.

Warning

Why we don’t build on it. A string is opaque to Python — you can’t loop over it, vary one value, pull a pitch from the current chord, or feed it a generated list. The standard form in this guide is plain Python lists (p.sequence, p.hit, p.note), because the API is the instrument: lists are values you can compute, transform, and reuse. Everything from here on uses them, and so should you. p.seq is shown once, here, and never again.

The same two-bar idea as a list — the form the rest of the guide uses — is simply:

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 list_drums(p):
    p.hit_steps("kick_1", [0], velocity=100)
    p.hit_steps("snare_1", [8], velocity=90)

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

You can now place pitched notes at any beat, shape their length and articulation, and write a bassline in plain musical names alongside the drums. Next we hand the rhythm-making over to generators — single numbers that bloom into whole Euclidean and spread patterns — so you describe the feel and let Subsequence fill in the grid.