# 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](#sec-appA-when) 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](02-rebuild-loop)); reacting to the chord and the form is what [Chapter 6](06-harmony-context) and [Chapter 10](10-form-sections) are about. Reach for the Direct API only when you hit one of the specific walls in [§A.1](#sec-appA-when) — and expect to manage the things `Composition` was doing for you (ports, clock, harmony plumbing) yourself. ``` (sec-appA-when)= ## 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. ```{list-table} :header-rows: 1 :widths: 34 33 33 * - 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](02-rebuild-loop)). The conductor's LFOs/ramps ([§12.7](12-deep-generative)) 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](06-harmony-context)). - 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](02-rebuild-loop)); `p.section` and energy ([Chapter 10](10-form-sections)). - 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](#sec-appA-reschedule)). * - **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 `Sequencer`s, or to own the `asyncio` loop yourself ([§A.5](#sec-appA-sequencer)). ``` ```{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. ``` (sec-appA-subclass)= ## 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: ```python Pattern(channel, length=16, reschedule_lookahead=1, device=0, mirrors=None) ``` ```{list-table} :header-rows: 1 :widths: 24 18 58 * - 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](01-step-grid) 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](13-expression-hardware)'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. ```{doctest} appA >>> 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](#sec-appA-reschedule) 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](#sec-appA-notes). ```{admonition} Reference :class: seealso {py:class}`~subsequence.pattern.Pattern`, {py:class}`~subsequence.pattern.Step` ``` (sec-appA-reschedule)= ## 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](02-rebuild-loop). 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: ```{doctest} appA >>> 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. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.pattern.Pattern.on_reschedule` ``` (sec-appA-notes)= ## A.4 Placing notes: `add_note_beats` and `add_arpeggio_beats` The `PatternBuilder` bridge ([§A.2](#sec-appA-subclass)) 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. ```python 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) ``` ```{list-table} :header-rows: 1 :widths: 30 70 * - 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](#sec-appA-sequencer)): ```{doctest} appA >>> 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](appendix-d-api-reference) lists the family); the Direct API simply lets you call them yourself instead of receiving the resolved chord through a parameter. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.pattern.Pattern.add_note_beats`, {py:meth}`~subsequence.pattern.Pattern.add_arpeggio_beats` ``` (sec-appA-sequencer)= ## 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](#sec-appA-notes)). - 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](#sec-appA-sequencer) 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): ```python # ── 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](#sec-appA-notes)) without stepping it. If your harmony must *move*, the honest recommendation is to stay in the decorator API and use `composition.harmony()` ([§6.1](06-harmony-context)) or a `Progression` ([Chapter 7](07-progressions)), 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. ``` ```{admonition} Reference :class: seealso {py:class}`~subsequence.sequencer.Sequencer`, {py:func}`~subsequence.composition.schedule_patterns`, {py:func}`~subsequence.composition.schedule_harmonic_clock`, {py:func}`~subsequence.composition.run_until_stopped` ``` (sec-appA-comparison)= ## 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*. ```{list-table} :header-rows: 1 :widths: 26 37 37 * - - 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](#sec-appA-when), 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](appendix-b-analysis-toolkit); for the full method catalogue, see [Appendix D](appendix-d-api-reference).