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 p.data["..."] (§2.4–2.5). The conductor’s LFOs/ramps (§12.7) cover most automation.

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 chord parameter, or read p.harmony after composition.harmony() (Chapter 6).

You are running without a Composition at all and must query a HarmonicState you built and stepped yourself.

To react to song position / sections

p.cycle, p.bar, p.bar_cycle (§2.3); p.section and energy (Chapter 10).

You are not using composition.form() and want to schedule structure with your own callbacks.

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 on_reschedule() you can mutate self.steps in place rather than clearing it (§A.3).

Multiple independent clocks / harmonic contexts in one process, or embedding the sequencer inside a larger async application

(no decorator equivalent — one Composition owns one clock)

You really do need two Sequencers, or to own the asyncio loop yourself (§A.5).

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

channel

(required)

The MIDI channel to output on. Note the indexing: here channel is the raw, 0-indexed MIDI channel — drums are 9, not 10. This is the one place the friendly 1-indexed numbering of Chapter 1 does not apply, because you are below the layer that translates it.

length

16

The pattern’s length in beats before it loops and rebuilds (16 = four bars of 4/4; pass 4 for a one-bar loop).

reschedule_lookahead

1

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.

device

0

Output device index; 0 is the primary device.

mirrors

None

Extra (device, channel) destinations to duplicate every event onto (the low-level form of §13.5’s mirroring).

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.

Reference

Pattern, Step

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

on_reschedule()

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

add_note_beats(beat_position, pitch, velocity, duration_beats)

One note, at beat_position beats from the start of the cycle, lasting duration_beats. beat_position must be ≥ 0 and duration_beats > 0.

add_arpeggio_beats(pitches, spacing_beats, velocity=100, duration_beats=None)

An arpeggio: cycles through pitches one at a time at spacing_beats intervals, filling the whole pattern length. When duration_beats is None each note lasts exactly spacing_beats (a legato run).

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.

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 read get_current_chord() from your patterns (as in §A.4).

  • The three module-level helpers in subsequence.composition that wire patterns to the sequencer and run it: schedule_patterns, schedule_harmonic_clock, and run_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 takes output_device_name, time_signature, record, and more — same options Composition forwards for you.)

  • schedule_patterns(seq, [drums, bass]) registers each Pattern for repeating playback from pulse 0. Its signature is schedule_patterns(sequencer, patterns, start_pulse=0).

  • run_until_stopped(seq) is the manual play(): 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 @composition.pattern

A subclass of subsequence.pattern.Pattern

Paradigm

Declarative — describe one bar; it is rebuilt for you

Object-oriented — you own the instance and its self.steps

Lifecycle

Automatic: composition.play() / render()

Manual: build a Sequencer, schedule_patterns, run_until_stopped, asyncio.run

Per-cycle rebuild

Always from a fresh canvas

Your on_reschedule() — rebuild or edit self.steps in place

State across cycles

Module-level values or p.data

Instance attributes on self (one per instance)

Channel numbering

1-indexed (drums = 10), as on your gear

Raw 0-indexed (drums = 9)

Harmony

composition.harmony(); receive the chord by parameter or p.harmony

Build and step a HarmonicState yourself

Ports, clock, panic-on-stop

Handled for you

Your responsibility (await seq.stop())

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.