Appendix A · The Direct Pattern API¶
A power-user reference for the low-level, object-oriented path: subclass
Pattern, hold state in your own object, and drive the async sequencer
yourself — and a clear account of when (almost never) you should.
Everything in the main guide goes through the decorator API: you write a
pattern as a function, mark it with @composition.pattern(...), and
Composition looks after the rest — opening MIDI ports, running the async
clock, rebuilding your pattern once per cycle, threading harmony and form
through to you. That is the recommended path for essentially every reader, and
nothing in this appendix changes that.
Underneath the decorator sits a smaller, blunter set of objects: the
Pattern class your function is wrapped around, and the Sequencer that
the clock actually runs on. The Direct Pattern API is those objects, used by
hand. It trades the decorator’s convenience for two things: an instance you can
hang persistent state on, and full control of the event loop. This appendix
documents that trade and shows the exact calls — but read §A.1
first, because for almost all music the decorator API is not just easier, it is
the right tool.
Important
The decorator API can do almost everything the Direct API can. Persistent
state across cycles lives happily in module-level values or p.data (see
Chapter 2); reacting to the chord and the form is what
Chapter 6 and Chapter 10 are about.
Reach for the Direct API only when you hit one of the specific walls in
§A.1 — and expect to manage the things Composition was doing
for you (ports, clock, harmony plumbing) yourself.
A.1 When to drop to the Direct API¶
The decorator API rebuilds your pattern function from scratch every cycle, on a fresh canvas, and hands you the chord and section through parameters. That model covers the whole of this guide. You should consider the Direct Pattern API only when one of the following is genuinely true — and the first three have ordinary decorator-API answers you should rule out first.
You think you need… |
Try this in the decorator API first |
Direct API only if… |
|---|---|---|
State that survives across cycles (an evolving density, a running counter, an LFO phase you advance yourself) |
A module-level value, or |
You want that state to live on the pattern object itself — many instances of the same class, each with its own counter — rather than in shared module scope. |
To follow the current chord |
Declare a |
You are running without a |
To react to song position / sections |
|
You are not using |
Incremental edits — change a few existing notes each cycle instead of rebuilding the whole bar |
(no decorator equivalent — the function always starts from an empty canvas) |
This is a real Direct-API capability: in |
Multiple independent clocks / harmonic contexts in one process, or embedding the sequencer inside a larger async application |
(no decorator equivalent — one |
You really do need two |
Note
The honest summary: most readers will never open this appendix in anger. The decorator API’s “rebuild from a fresh canvas each cycle” model is a feature — it keeps patterns reproducible and easy to reason about. The Direct API hands you back the mutable state and the event loop, and with them the bugs they prevent (a pattern that drifts because its state was never reset, a clock you forgot to stop). Use it deliberately, for the capabilities above, not as a default.
A.2 Subclassing Pattern¶
A pattern, at the bottom, is an instance of subsequence.pattern.Pattern. It
holds a MIDI channel, a length in beats, and a dictionary of steps —
{pulse_position: Step} — where each Step carries the notes that sound at that
pulse. The decorator builds one of these for you behind every @composition.pattern
function. To use the Direct API you subclass it and fill self.steps yourself.
The constructor is:
Pattern(channel, length=16, reschedule_lookahead=1, device=0, mirrors=None)
Argument |
Default |
Meaning |
|---|---|---|
|
(required) |
The MIDI channel to output on. Note the indexing: here |
|
|
The pattern’s length in beats before it loops and rebuilds ( |
|
|
How many beats before the cycle ends the next cycle is built, so events are queued before the clock needs them. One beat is almost always right. |
|
|
Output device index; |
|
|
Extra |
The cleanest way to fill self.steps is to borrow the same PatternBuilder the
decorator uses — the p you have painted notes onto all through this guide. You
construct one around self, call the familiar builder verbs, and the notes land
in self.steps. This is the builder bridge: every high-level method
(hit_steps, euclidean, velocity_shape, …) is available to a Pattern
subclass this way.
>>> import subsequence.pattern
>>> import subsequence.pattern_builder
>>> import subsequence.constants.instruments.gm_drums as gm_drums
>>>
>>> class DrumPattern(subsequence.pattern.Pattern):
... """Kick, snare, and hats — built through the PatternBuilder bridge."""
... def __init__(self):
... super().__init__(channel=9, length=4) # channel 9 = raw drum channel
... self._build()
... def _build(self):
... self.steps = {} # start from an empty canvas
... p = subsequence.pattern_builder.PatternBuilder(
... self, cycle=0, drum_note_map=gm_drums.GM_DRUM_MAP
... )
... p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
... p.hit_steps("snare_1", [4, 12], velocity=100)
... p.velocity_shape(60, 100)
... def on_reschedule(self):
... self._build()
...
>>> drums = DrumPattern()
>>> drums.channel, drums.length
(9, 4)
>>> len(drums.steps) # four kick positions populated
4
Tip
Keep the build in one method. The pattern above defines _build() and calls
it both from __init__ (for the first cycle) and from on_reschedule() (for
every cycle after). That is the idiomatic shape: one place that knows how to fill
the canvas, called whenever the canvas needs filling. §A.3
is about that on_reschedule hook.
The PatternBuilder bridge is the recommended way to author content in a
subclass, because it gives you the entire palette. For the rare case where you
want to place notes without a builder at all, Pattern exposes a couple of
beat-based primitives directly — see §A.4.
A.3 The on_reschedule rebuild hook¶
Pattern.on_reschedule(self) is the Direct API’s equivalent of “your function
re-runs every cycle” from Chapter 2. The sequencer calls it
once per loop, reschedule_lookahead beats before the cycle ends, immediately
before it reads self.steps to queue the next cycle’s MIDI. The base
implementation does nothing; you override it.
There are two ways to use the hook, and the second is the one genuine reason to
be writing a Pattern subclass at all.
1. Rebuild from scratch — clear self.steps and refill it, exactly as the
decorator does. This is the DrumPattern above: on_reschedule just calls
_build(), which resets self.steps = {} and repaints. Stateless and
predictable; if this is all you need, you almost certainly want the decorator API
instead.
2. Carry state and edit incrementally — keep variables on self that
persist between calls, and use them to evolve the pattern. This is the
capability the decorator API does not have: the instance outlives the cycle, so a
counter on self survives. Here a pattern grows denser every loop, and the
density lives on the object:
>>> import subsequence.pattern
>>> import subsequence.pattern_builder
>>>
>>> class EvolvingPattern(subsequence.pattern.Pattern):
... def __init__(self):
... super().__init__(channel=0, length=4)
... self.density = 0.5 # state that survives across cycles
... def on_reschedule(self):
... self.density += 0.05 # advances every cycle — it persists
... self.steps = {} # clear, then repaint at the new density
... p = subsequence.pattern_builder.PatternBuilder(self, cycle=0)
... p.euclidean(36, pulses=int(16 * self.density))
...
>>> ev = EvolvingPattern()
>>> ev.density
0.5
>>> ev.on_reschedule(); round(ev.density, 2)
0.55
>>> ev.on_reschedule(); round(ev.density, 2)
0.6
Note
The same evolution is perfectly possible in the decorator API with a
module-level value advanced inside the function. The Direct-API advantage is only
real when you want many instances of EvolvingPattern, each with its own
density — module scope would force them to share one. If you have a single
evolving part, prefer the decorator API and a module variable.
Because the hook hands you self.steps as it stood at the end of the last cycle,
you can also mutate it in place — nudge a velocity, drop a note, shift one
onset — without clearing and rebuilding. That truly incremental edit has no
decorator equivalent, and is the niche where this hook earns its keep.
Warning
If you do not clear self.steps, notes accumulate. Rebuild-from-scratch
patterns must reset self.steps = {} at the top of the build, or each cycle
adds to the last and the bar fills up with duplicates. Only skip the reset when
you are deliberately doing an incremental edit and know exactly which entries you
are changing.
Reference
A.4 Placing notes: add_note_beats and add_arpeggio_beats¶
The PatternBuilder bridge (§A.2) is the right tool for
most content, but Pattern itself carries a few low-level placement methods that
work in beats and need no builder. The two you will reach for are
add_note_beats and add_arpeggio_beats. Both convert beats to the sequencer’s
internal pulse grid for you (Subsequence runs at 24 pulses per beat), so you
think in musical time, not pulses.
add_note_beats(beat_position, pitch, velocity, duration_beats, pulses_per_beat=24)
add_arpeggio_beats(pitches, spacing_beats, velocity=100, duration_beats=None,
pulses_per_beat=24)
Method |
What it places |
|---|---|
|
One note, at |
|
An arpeggio: cycles through |
A bass that plays the chord root on every beat, and an arpeggio that climbs the
chord tones in sixteenths — both reading from a HarmonicState you built
yourself (more on that engine in §A.5):
>>> import subsequence.pattern
>>> import subsequence.harmonic_state
>>>
>>> state = subsequence.harmonic_state.HarmonicState(
... key_name="E", graph_style="aeolian_minor", key_gravity_blend=0.8
... )
>>> chord = state.get_current_chord()
>>> chord.name()
'Em'
>>>
>>> bass = subsequence.pattern.Pattern(channel=5, length=4)
>>> root = chord.root_note(40) # nearest E to MIDI 40 (E2)
>>> for beat in range(4):
... bass.add_note_beats(beat, pitch=root, velocity=100, duration_beats=0.9)
...
>>> sorted(bass.steps.keys()) # one note per beat, in pulses (×24)
[0, 24, 48, 72]
>>>
>>> arp = subsequence.pattern.Pattern(channel=0, length=4)
>>> arp.add_arpeggio_beats(chord.tones(root=60, count=4), spacing_beats=0.25, velocity=90)
>>> sum(len(step.notes) for step in arp.steps.values()) # 4 beats ÷ 0.25 = 16 notes
16
Note
chord.root_note(midi) returns the chord’s root nearest a reference MIDI note —
here root_note(40) voices the bass low. chord.tones(root=60, count=4) returns
four MIDI pitches for the chord starting near middle C. These are methods on the
Chord value (Appendix D lists the family); the
Direct API simply lets you call them yourself instead of receiving the resolved
chord through a parameter.
Reference
A.5 Running the async sequencer yourself¶
The decorator API ends with composition.play() (or render()), which opens the
MIDI ports, starts the clock, and runs the rebuild loop until you press Ctrl-C.
In the Direct API you do that assembly yourself, on an asyncio event loop. The
moving parts are:
subsequence.sequencer.Sequencer— the engine: a stable clock plus the MIDI event queue and the per-cycle reschedule machinery. Construct it with an initial BPM (and optionally a device name).subsequence.harmonic_state.HarmonicState— the chord engine, built around a key and a chord-transition graph. You step it yourself and readget_current_chord()from your patterns (as in §A.4).The three module-level helpers in
subsequence.compositionthat wire patterns to the sequencer and run it:schedule_patterns,schedule_harmonic_clock, andrun_until_stopped.
Important
This is a live-playback assembly and cannot run under render(). The whole
of §A.5 is therefore shown as non-executed, illustrative
code — clearly marked — rather than as a checked doctest. Every signature
below is verified against the installed v0.6.2 (pattern.py, sequencer.py, and
composition.py); the runnable parts of this appendix are the Pattern
subclassing and note-placement blocks in §A.2–A.4, which do execute.
A complete Direct-API piece — two pattern classes and a hand-driven asyncio
loop — looks like this. It assembles by hand what a decorated Composition does
for you, the long way round (harmony is held fixed here; moving it is the
fiddly part, covered just below):
# ── NON-EXECUTED, ILLUSTRATIVE ──────────────────────────────────────────────
# A live async assembly: it opens ports and runs the wall clock, so it cannot
# run headlessly under render()/doctest. Signatures verified against v0.6.2.
import asyncio
import subsequence.composition
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.harmonic_state
import subsequence.pattern
import subsequence.pattern_builder
import subsequence.sequencer
class DrumPattern(subsequence.pattern.Pattern):
def __init__(self):
super().__init__(channel=9, length=4)
self._build()
def _build(self):
self.steps = {}
p = subsequence.pattern_builder.PatternBuilder(
self, cycle=0, drum_note_map=gm_drums.GM_DRUM_MAP
)
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.hit_steps("snare_1", [4, 12], velocity=100)
p.hit_steps("hi_hat_closed", range(16), velocity=80)
p.velocity_shape(60, 100)
def on_reschedule(self):
self._build()
class BassPattern(subsequence.pattern.Pattern):
def __init__(self, harmonic_state):
super().__init__(channel=5, length=4)
self.harmonic_state = harmonic_state
self._build()
def _build(self):
self.steps = {}
chord = self.harmonic_state.get_current_chord()
root = chord.root_note(40) # bass register
for beat in range(4):
self.add_note_beats(beat, pitch=root, velocity=100, duration_beats=0.9)
def on_reschedule(self):
self._build()
async def main():
seq = subsequence.sequencer.Sequencer(initial_bpm=120)
# The chord engine. Here it is built once and read but never stepped, so the
# harmony stays put — see "Stepping harmony yourself" below for why moving it
# is the awkward part of the Direct API.
harmonic_state = subsequence.harmonic_state.HarmonicState(
key_name="E", graph_style="aeolian_minor", key_gravity_blend=0.8
)
# Schedule the patterns and run until Ctrl-C.
drums = DrumPattern()
bass = BassPattern(harmonic_state)
await subsequence.composition.schedule_patterns(seq, [drums, bass])
await subsequence.composition.run_until_stopped(seq)
if __name__ == "__main__":
asyncio.run(main())
Reading the assembly:
Sequencer(initial_bpm=120)builds the engine. (Its full signature also takesoutput_device_name,time_signature,record, and more — same optionsCompositionforwards for you.)schedule_patterns(seq, [drums, bass])registers eachPatternfor repeating playback from pulse 0. Its signature isschedule_patterns(sequencer, patterns, start_pulse=0).run_until_stopped(seq)is the manualplay(): it starts the sequencer, installs Ctrl-C / SIGTERM handlers, waits, and stops cleanly. Signature:run_until_stopped(sequencer).
Warning
You now own the lifecycle. run_until_stopped stops the sequencer for you on
a signal, but if you assemble the loop differently you are responsible for calling
await seq.stop() — it closes the MIDI ports, sends an all-notes-off panic, and
saves any recording. Skip it and you can leave notes ringing on your synth and a
port held open. This bookkeeping is exactly what Composition does for you.
Stepping harmony yourself¶
To make the bass and arp follow a moving chord, something must advance the
HarmonicState once per chord cycle. The decorator API does this invisibly when
you call composition.harmony(). The Direct API exposes the underlying helper,
schedule_harmonic_clock — but be warned that it is genuine low-level
plumbing whose signature has grown, and is the least stable corner of this
appendix.
Warning
schedule_harmonic_clock is internal plumbing — its signature is not
beginner-facing and has changed. In the installed v0.6.2 it requires a
horizon object and a bar_beats value in addition to the sequencer and a
state-getter:
schedule_harmonic_clock(sequencer, get_harmonic_state, horizon, bar_beats,
cycle_beats=4, ...)
where horizon is a subsequence.composition._HarmonyHorizon instance (note the
leading underscore — it is private). Older examples that call it with three
arguments (schedule_harmonic_clock(seq, lambda: state, cycle_beats=4)) — including
examples/demo_advanced.py and the README’s collapsed Direct-API listing — are
stale against v0.6.2 and will raise a TypeError. Treat the manual harmonic
clock as advanced, churn-prone surface.
For most Direct-API work you do not need the clock helper at all: if your harmony
is fixed, build one HarmonicState and read get_current_chord() from it (as in
§A.4) without stepping it. If your harmony must move, the
honest recommendation is to stay in the decorator API and use
composition.harmony() (§6.1) or a Progression
(Chapter 7), which give you a moving chord without touching
private plumbing. The Direct-API harmonic clock exists, but it is not where a
musician should be living.
Note
If you genuinely need Pattern subclasses and moving harmony, the simplest
robust route is to run the whole piece through the Direct API — building your state
manually, as the README’s Direct-API guidance suggests — and step a single
HarmonicState from your own repeating callback, rather than reaching for the
private schedule_harmonic_clock seam. But weigh that against just using the
decorator API: it was built to make exactly this easy.
A.6 Decorator API vs Direct API¶
The two APIs are not rivals — the decorator API is a thin, musician-friendly skin
over the same Pattern and Sequencer objects the Direct API exposes raw. The
choice is almost always the decorator API; this table is mostly here to show why.
Decorator API (recommended) |
Direct Pattern API |
|
|---|---|---|
Your code is… |
A function with |
A subclass of |
Paradigm |
Declarative — describe one bar; it is rebuilt for you |
Object-oriented — you own the instance and its |
Lifecycle |
Automatic: |
Manual: build a |
Per-cycle rebuild |
Always from a fresh canvas |
Your |
State across cycles |
Module-level values or |
Instance attributes on |
Channel numbering |
1-indexed (drums = |
Raw 0-indexed (drums = |
Harmony |
|
Build and step a |
Ports, clock, panic-on-stop |
Handled for you |
Your responsibility ( |
Best for |
Essentially all music — prototyping to finished generative pieces |
Per-instance persistent state, truly incremental edits, multiple sequencers, or embedding the engine in a larger async app |
Important
Default to the decorator API; treat the Direct API as an escape hatch. If you are unsure which you need, you need the decorator API — the entire main guide is built on it, and it reaches every musical destination this book describes. Drop to the Direct Pattern API only for the specific capabilities in §A.1, and only once you are comfortable owning the event loop and the cleanup that comes with it.
The Direct Pattern API is the floor the whole library stands on: subclass
Pattern, fill self.steps (by hand or through the PatternBuilder bridge),
override on_reschedule, and run a Sequencer on your own asyncio loop. It
buys you per-instance state and full control of the clock — and asks, in return,
that you manage what Composition was quietly handling. For the analysis and
set-theory tools that complement hand-built patterns, see
Appendix B; for the full method catalogue, see
Appendix D.