Subsystem

subsequence.composition

The API reference for Composition.

Composition

class Composition(
    output_device: typing.Optional[str] = None,
    bpm: float = 120,
    time_signature: typing.Tuple[int, int] = (4, 4),
    key: typing.Optional[str] = None,
    scale: typing.Optional[str] = None,
    seed: typing.Optional[int] = None,
    record: bool = False,
    record_filename: typing.Optional[str] = None,
    zero_indexed_channels: bool = False,
    latency_ms: float = 0.0,
)

The top-level controller for a musical piece.

The Composition object manages the global clock (Sequencer), the harmonic progression (HarmonicState), the song structure (subsequence.form_state.FormState), and all MIDI patterns. It serves as the main entry point for defining your music.

Typical workflow:

  1. Initialize Composition with BPM and Key.
  2. Define harmony and form (optional).
  3. Register patterns using the @composition.pattern decorator.
  4. Call composition.play() to start the music.

Initialize a new composition.

Parameters

Example

comp = subsequence.Composition(bpm=128, key="Eb", seed=123)

Members: bpm, builder_bar, cc_forward, cc_map, chords, clear_tweak, clock_output, conductor, current_chord, data, display, energy, form, form_freeze, form_jump, form_next, form_state, freeze, get_tweaks, harmonic_state, harmony, hotkey, hotkeys, is_clock_following, is_paused, key, layer, link, live, live_info, load_patterns, lock, midi_input, midi_output, mirror, mute, note_input, on_event, on_section, osc, osc_map, output_device, pattern, pause, phrase_part, pin_chord, play, render, request_cadence, reroll, resume, running_patterns, scale, schedule, section_cadence, section_chords, section_motifs, seed, seed_for, sequencer, set_bpm, target_bpm, time_signature, transition, trigger, tuning, tweak, unlock, unmirror, unmirror_all, unmute, unregister, watch, web_ui

Composition.output_device

Composition.output_device

Composition.bpm

Composition.bpm

Composition.time_signature

Composition.time_signature

Composition.key

Composition.key

Composition.scale

Composition.scale

Composition.data

Composition.data: typing.Dict[str, typing.Any]

Composition.conductor

Composition.conductor

Composition.harmonic_state

property Composition.harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState]

The active HarmonicState, or None if harmony() has not been called.

Composition.current_chord

Composition.current_chord() -> typing.Optional[typing.Any]

The chord sounding at the playhead, or None without harmony.

Reads the harmony window at the current pulse, so it stays accurate under variable harmonic rhythm and clock lookahead (the engine's current_chord flips lookahead beats early — this does not). Falls back to the engine's chord before playback starts. The chord may be a decorated wrapper (Am9, C/G) when the sounding span is spiced; it duck-types the Chord voicing protocol either way.

Composition.form_state

property Composition.form_state: typing.Optional[subsequence.form_state.FormState]

The active subsequence.form_state.FormState, or None if form() has not been called.

Composition.sequencer

property Composition.sequencer: subsequence.sequencer.Sequencer

The underlying Sequencer instance.

Composition.running_patterns

property Composition.running_patterns: typing.Dict[str, typing.Any]

The currently active patterns, keyed by name.

Composition.builder_bar

property Composition.builder_bar: int

Current bar index used by pattern builders.

Composition.harmony

Composition.harmony(
    style: typing.Optional[typing.Union[str, subsequence.chord_graphs.ChordGraph]] = None,
    cycle_beats: int = 4,
    dominant_7th: bool = True,
    gravity: float = 1.0,
    nir_strength: float = 0.5,
    minor_turnaround_weight: float = 0.0,
    root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
    reschedule_lookahead: float = 1,
    progression: typing.Optional[typing.Any] = None,
) -> None

Configure the harmonic logic and chord change intervals.

Two sources, combinable: a bound progression (progression= — a Progression value, an element list like [1, 6, 3, "bVII7"], or chord names) walked span by span on the global clock; and/or a graph style stepping live chords. With only a progression bound, it loops on exhaustion; with a style configured too, exhaustion falls through to live stepping (the frozen-replay bridge). Calling with neither argument keeps today's default live engine (style="functional_major").

Parameters

Example

# A moody minor progression that changes every 8 beats
comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)

# Manual harmony driving everything — loops forever
comp.harmony(progression=subsequence.progression([1, 6, 3, 7]))

Composition.freeze

Composition.freeze(
    bars: int,
    end: typing.Optional[typing.Any] = None,
    pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
    avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
    cadence: typing.Optional[str] = None,
) -> Progression

Capture a chord progression from the live harmony engine.

Runs the harmony engine forward by bars chord changes, records each chord, and returns it as a Progression that can be bound to a form section with section_chords.

The engine state advances — successive freeze() calls produce a continuing compositional journey so section progressions feel like parts of a whole rather than isolated islands.

The hybrid constraints compile into the walk: end= fixes the last bar ("end on V at bar 8"), pins= fix any 1-based bar, avoid= excludes chords throughout. Specs follow the progression-element grammar (ints where diatonic, roman/name strings where chromatic) and resolve against the composition key and scale. A backward feasibility pass guarantees satisfiability before any chord is drawn; the forward walk keeps the engine's real history-dependent weighting. Bar 1 is always the engine's current chord — the journey continues — so pins={1: ...} may only name it redundantly.

Parameters

Returns

Raises

Example:

composition.harmony(style="functional_major", cycle_beats=4)
verse  = composition.freeze(8, end="V")   # the verse sets up the chorus
chorus = composition.freeze(4)            # next 4 chords, continuing on
composition.section_chords("verse",  verse)
composition.section_chords("chorus", chorus)

Composition.section_chords

Composition.section_chords(
    section_name: str,
    progression: typing.Any,
) -> None

Bind a Progression to a named form section.

Every time section_name plays, the harmonic clock walks the progression's spans instead of calling the live engine. Sections without a bound progression continue generating live chords.

Accepts a Progression value (from freeze, the progression() factory, or hand-built) or anything the factory accepts — an element list like [1, 6, 3, "bVII7"] or chord names.

Key-relative content re-keys per occurrence. A progression written in degrees or romans is key-relative content: it resolves late, each time the section plays, against that section's effective key and scale (Section.key > form key > composition key, with mode following the same chain). So a Section(key="A") plays the same numbered progression a tone higher — its chords and its degrees share one tonic. Absolute content — chord names ("Am"), PitchSet, and frozen captures from freeze — names exact chords and is never transposed by a key.

On exhaustion mid-section the progression loops when no graph style is configured (and always when it contains a PitchSet); with a live engine, exhaustion falls through to live stepping in the COMPOSITION key — the live graph engine does not transpose for a section (a stateful walk does not modulate mid-stream), so a re-keyed section that runs out of written chords hands off to composition-key harmony. Bind a full-length progression (or set at_end/loop intent) if you need the whole section in its key.

Parameters

Raises

Example:

composition.section_chords("verse",  verse_progression)
composition.section_chords("chorus", [1, 6, 3, 7])
# "bridge" is not bound — it generates live chords

Composition.pin_chord

Composition.pin_chord(
    bar: int,
    chord: typing.Optional[typing.Any],
) -> None

Force the chord sounding at a bar — fiat over live generation.

Whatever the harmonic source (live walk, bound progression, section progression) produces for bar, the pinned chord overrides it. Pass None to remove a pin.

Parameters

Example:

composition.pin_chord(8, "E7")    # the turnaround lands on E7
composition.pin_chord(8, "V")     # the dominant of bar 8's section
composition.pin_chord(8, None)    # let it walk again

Composition.request_cadence

Composition.request_cadence(
    cadence: str = 'strong',
    bar: typing.Optional[int] = None,
) -> None

Ask the live engine to approach a cadence arriving at a bar.

The request hook: where pin_chord is fiat, this is a steered approach — at the next chord boundary the clock plans the remaining changes up to bar as a constrained walk through the engine's real weights, pinned to the cadence formula at the tail ("strong" arrives V→I, "soft" IV→I, "open" IV→V, "fakeout" V→vi; theory aliases accepted). The chords still commit one boundary at a time, so the journey continues through the close.

One-shot: the request is consumed when planned. Live harmony only — bound/section progressions are data and cannot be steered; a request whose bar passes unserved expires with a warning. If the formula is not walkable from where the harmony stands, the arrival lands by fiat (loudly). Ask at least a pattern-lookahead ahead: patterns may already have rendered against the previously planned chord.

Parameters

Example:

composition.request_cadence("open", bar=16)    # hang on V at bar 16

Composition.section_cadence

Composition.section_cadence(
    section_name: str,
    cadence: typing.Optional[str] = 'strong',
) -> None

Close every pass of a section with a cadence — the standing request.

Each time section_name is entered, the clock registers a request_cadence arriving at the section's final bar, so the harmony approaches the close as the section ends. Live harmony only: a section with bound chords (section_chords) is data and ignores the registration — its closes are written, not steered. Pass None to unregister.

Example:

composition.form([("verse", 8), ("chorus", 8)])
composition.section_cadence("verse", "open")     # every verse hangs on V
composition.section_cadence("chorus", "strong")  # every chorus lands home

Composition.section_motifs

Composition.section_motifs(
    section_name: str,
    value: typing.Any,
    part: typing.Optional[str] = None,
) -> None

Bind a Motif or Phrase to a named form section (per optional part).

Patterns read the binding back with p.section_motif(part) (or use the one-call phrase_part); a section with no binding for the part is silent for that part — bind material or don't, no fallback guessing. Re-binding is idempotent, so the call is safe in a live file: re-executing on save is the desired rebind.

Parameters

Raises

Example:

composition.section_motifs("verse",  verse_line,  part="lead")
composition.section_motifs("chorus", chorus_line, part="lead")

Composition.on_event

Composition.on_event(
    event_name: str,
    callback: typing.Callable[..., typing.Any],
) -> None

Register a callback for a sequencer event (e.g., "bar", "start", "stop").

Composition.hotkeys

Composition.hotkeys(enabled: bool = True) -> None

Enable or disable the global hotkey listener.

Must be called before play to take effect. When enabled, a background thread reads single keystrokes from stdin without requiring Enter. The ? key is always reserved and lists all active bindings.

Hotkeys have zero impact on playback when disabled — the listener thread is never started.

Parameters

Example:

composition.hotkeys()
composition.hotkey("a", lambda: composition.form_jump("chorus"))
composition.play()

Composition.hotkey

Composition.hotkey(
    key: str,
    action: typing.Callable[[], None],
    quantize: int = 0,
    label: typing.Optional[str] = None,
) -> None

Register a single-key shortcut that fires during playback.

The listener must be enabled first with hotkeys.

Most actions — form jumps, composition.data writes, and tweak calls — should use quantize=0 (the default). Their musical effect is naturally delayed to the next pattern rebuild cycle, which provides automatic musical quantization without extra configuration.

Use quantize=N for actions where you want an explicit bar-boundary guarantee, such as mute / unmute.

The ? key is reserved and cannot be overridden.

Parameters

Raises

Example:

composition.hotkeys()

# Immediate — musical effect happens at next pattern rebuild
composition.hotkey("a", lambda: composition.form_jump("chorus"))
composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))

# Explicit 4-bar phrase boundary
composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)

# Named function — label is derived automatically
def drop_to_breakdown ():
    composition.form_jump("breakdown")
    composition.mute("lead")

composition.hotkey("d", drop_to_breakdown)

composition.play()

Composition.form_jump

Composition.form_jump(section_name: str) -> None

Jump the form to a named section immediately.

Delegates to subsequence.form_state.FormState.jump_to. Works with a graph form (a dict passed to form), a list, or a Form — in list and Form modes the jump lands on the next occurrence of the name, searching forward and wrapping. Only a generator form cannot be navigated.

The musical effect is heard at the next pattern rebuild cycle — already- queued MIDI notes are unaffected. This natural delay means form_jump is effective without needing explicit quantization. A jump part-way through a bar gives the rest of that bar to the new section as its bar 0, so its first full bar is bar 1 — see subsequence.form_state.FormState.jump_to (#2484).

Parameters

Raises

Example:

composition.hotkey("c", lambda: composition.form_jump("chorus"))

Composition.form_next

Composition.form_next(section_name: str) -> None

Queue the next section — takes effect when the current section ends.

Unlike form_jump, this does not interrupt the current section. The queued section replaces the automatically pre-decided next section and takes effect at the natural section boundary. The performer can change their mind by calling form_next again before the boundary.

Delegates to subsequence.form_state.FormState.queue_next. Works with a graph form (a dict passed to form), a list, or a Form — in list and Form modes the queued section lands on the next occurrence of the name, searching forward and wrapping. Only a generator form cannot be navigated.

Parameters

Raises

Example:

composition.hotkey("c", lambda: composition.form_next("chorus"))

Composition.seed

property writable Composition.seed: typing.Optional[int]

The composition's random seed, or None when unseeded.

When set, every random decision derives deterministically from this value through named streams (see seed_for()), so the same script produces the same music on every run. Assign to set it:

comp.seed = 42

(Formerly the method comp.seed(42) — the call form is a hard break per the pre-1.0 rename policy.)

Composition.seed_for

Composition.seed_for(name: str) -> typing.Optional[int]

Surface the effective derived seed for a named stream.

Works for pattern names and equally for any name you invent for a standalone value generator (seed=composition.seed_for("hook")), so its randomness keys off the composition seed without sharing any other consumer's stream. Reflects reroll() nonces. Returns None when the composition is unseeded.

Example

hook_seed = composition.seed_for("hook")

Composition.reroll

Composition.reroll(name: str) -> None

Deal a named stream a fresh deterministic seed — try a new variation.

Bumps the per-name nonce and prints the new effective seed. The nonce lives only in this process, so the printed seed is what lets a variation you like survive a restart: note it down, or lock() the name to pin it for the session. Refuses on locked names.

Parameters

Example

comp.reroll("lead")    # prints: reroll('lead') -> effective seed ...

Composition.lock

Composition.lock(name: str) -> None

Pin a named stream: keep its current effective seed and realization.

Engine-side state, so it survives live reload (it is never a builder swap): a locked pattern re-deals its stream from the same effective seed on every rebuild, so every cycle realizes identically, and reroll() refuses with a message until unlock().

Parameters

Composition.unlock

Composition.unlock(name: str) -> None

Release a lock(): the stream runs free and reroll() works again.

Composition.tuning

Composition.tuning(
    source: typing.Optional[typing.Union[str, os.PathLike]] = None,
    *,
    cents: typing.Optional[typing.List[float]] = None,
    ratios: typing.Optional[typing.List[float]] = None,
    equal: typing.Optional[int] = None,
    bend_range: float = 2.0,
    channels: typing.Optional[typing.List[int]] = None,
    reference_note: int = 60,
    exclude_drums: bool = True,
) -> None

Set a global microtonal tuning for the composition.

The tuning is applied automatically after each pattern rebuild (before the pattern is scheduled). Drum patterns (those registered with a drum_note_map) are excluded by default.

Supply exactly one of the source parameters:

For polyphonic parts, supply a channels pool. Notes are spread across those MIDI channels so each can carry an independent pitch bend. The synth must be configured to match bend_range (its pitch-bend range setting in semitones).

Parameters

Example

# Quarter-comma meantone from a Scala file
comp.tuning("meanquar.scl")

# Just intonation from ratios
comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2])

# 19-TET, monophonic
comp.tuning(equal=19, bend_range=2.0)

# 31-TET with channel rotation for polyphony (channels 1-6)
comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])

Composition.display

Composition.display(
    enabled: bool = True,
    grid: bool = False,
    grid_scale: float = 1.0,
) -> None

Enable or disable the live terminal dashboard.

When enabled, Subsequence uses a safe logging handler that allows a persistent status line (BPM, Key, Bar, Section, Chord) to stay at the bottom of the terminal while logs scroll above it.

Parameters

Composition.web_ui

Composition.web_ui(
    http_host: str = '127.0.0.1',
    ws_host: str = '127.0.0.1',
) -> None

Enable the realtime Web UI Dashboard.

When enabled, Subsequence instantiates a WebSocket server that broadcasts the current state, signals, and active patterns (with high-res timing and note data) to any connected browser clients.

Both servers bind to localhost by default. Pass http_host / ws_host (e.g. "0.0.0.0") to opt into LAN exposure — the dashboard is read-only but broadcasts full composition state, so only do so on a trusted network.

Composition.midi_input

Composition.midi_input(
    device: str,
    clock_follow: bool = False,
    name: typing.Optional[str] = None,
) -> None

Configure a MIDI input device for external sync and MIDI messages.

May be called multiple times to register additional input devices. The first call sets the primary input (device 0). Subsequent calls add additional input devices (device 1, 2, …). Only one device may have clock_follow=True.

Parameters

Example

# Single controller (unchanged usage)
comp.midi_input("Scarlett 2i4", clock_follow=True)

# Multiple controllers
comp.midi_input("Arturia KeyStep", name="keys")
comp.midi_input("Faderfox EC4", name="faders")

Composition.midi_output

Composition.midi_output(
    device: str,
    name: typing.Optional[str] = None,
    latency_ms: float = 0.0,
) -> int

Register an additional MIDI output device.

The first output device is always the one passed to Composition(output_device=…) — that is device 0. Each call to midi_output() adds the next device (1, 2, …).

Parameters

Returns

Example

comp = subsequence.Composition(bpm=120, output_device="MOTU Express")

# Returns 1 — use as device=1 or device="integra"
comp.midi_output("Roland Integra", name="integra")

# A software sampler that sounds 20ms late
comp.midi_output("Subsample", name="sampler", latency_ms=20)

@comp.pattern(channel=1, beats=4, device="integra")
def strings (p):
        p.note(60, beat=0)

Composition.clock_output

Composition.clock_output(enabled: bool = True) -> None

Send MIDI timing clock to connected hardware.

When enabled, Subsequence acts as a MIDI clock master and sends standard clock messages on the output port: a Start message (0xFA) when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN), and a Stop message (0xFC) when playback ends.

This allows hardware synthesizers, drum machines, and effect units to slave their tempo to Subsequence automatically.

Note: Clock output is automatically disabled when midi_input() is called with clock_follow=True, to prevent a clock feedback loop.

Parameters

Example

comp = subsequence.Composition(bpm=120, output_device="...")
comp.clock_output()   # hardware will follow Subsequence tempo
Composition.link(quantum: float = 4.0) -> Composition

Enable Ableton Link tempo and phase synchronisation.

When enabled, Subsequence joins the local Link session and slaves its clock to the shared network tempo and beat phase. All other Link-enabled apps on the same LAN — Ableton Live, iOS synths, other Subsequence instances — will automatically stay in time.

Playback starts on the next bar boundary aligned to the Link quantum, so downbeats stay in sync across all participants.

Requires the link optional extra:

pip install subsequence[link]

Parameters

Example:

comp = subsequence.Composition(bpm=120, key="C")
comp.link()          # join the Link session
comp.play()

# On another machine / instance:
comp2 = subsequence.Composition(bpm=120)
comp2.link()         # tempo and phase will lock to comp
comp2.play()

Note

set_bpm() proposes the new tempo to the Link network when Link is active. The network-authoritative tempo is applied on the next pulse, so there may be a brief lag before the change is visible.

Composition.cc_map

Composition.cc_map(
    cc: int,
    data_key: str,
    channel: typing.Optional[int] = None,
    min_val: float = 0.0,
    max_val: float = 1.0,
    input_device: subsequence.midi_utils.DeviceId = None,
) -> None

Map an incoming MIDI CC to a composition.data key.

When the composition receives a CC message on the configured MIDI input port, the value is scaled from the CC range (0–127) to [min_val, max_val] and stored in composition.data[data_key].

This lets hardware knobs, faders, and expression pedals control live parameters without writing any callback code.

Requires midi_input() to be called first to open an input port.

Parameters

Example

comp.midi_input("Arturia KeyStep")
comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader

# Multi-device: only listen to CC 74 from the "faders" controller
comp.cc_map(74, "filter", input_device="faders")

Composition.note_input

Composition.note_input(
    channel: typing.Optional[int] = None,
    release_ms: float = 30.0,
    latch: bool = False,
    input_device: subsequence.midi_utils.DeviceId = None,
) -> None

Track notes held on a MIDI keyboard for live arpeggiation.

Incoming note-on/note-off messages build a live "currently held" set that any pattern reads via p.held_notes() — typically fed straight to p.arpeggio(). The composition still authors the rhythm and motion; the player's hands supply the pitch set. This is a live performance layer over the deterministic, seeded composition: when rendering headlessly there is no input, so p.held_notes() is empty and seeded output is unchanged.

Requires midi_input() to be called first to open an input port.

Parameters

Example

comp.midi_input("Arturia KeyStep")
comp.note_input(channel=1, release_ms=30)

@comp.pattern(channel=6, beats=4)
def arp (p):
    p.arpeggio(p.held_notes(), direction="forward")  # rests when silent

Composition.cc_forward

Composition.cc_forward(
    cc: int,
    output: typing.Union[str, typing.Callable],
    *,
    channel: typing.Optional[int] = None,
    output_channel: typing.Optional[int] = None,
    mode: str = 'instant',
    input_device: subsequence.midi_utils.DeviceId = None,
    output_device: subsequence.midi_utils.DeviceId = None,
) -> None

Forward an incoming MIDI CC to the MIDI output in real-time.

Unlike cc_map() which writes incoming CC values to composition.data for use at pattern rebuild time, cc_forward() routes the signal directly to the MIDI output — bypassing the pattern cycle entirely.

Both cc_map() and cc_forward() may be registered for the same CC number; they operate independently.

Parameters

Example

comp.midi_input("Arturia KeyStep")

# CC 1 → CC 1 (identity, instant)
comp.cc_forward(1, "cc")

# CC 1 → pitch bend on channel 1, queued (recordable)
comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued")

# CC 1 → CC 74, custom channel
comp.cc_forward(1, "cc:74", output_channel=2)

# Custom transform — remap CC range 0–127 to CC 74 range 40–100
import subsequence.midi as midi
comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch))

# Forward AND map to data simultaneously — both active on the same CC
comp.cc_map(1, "mod_wheel")
comp.cc_forward(1, "cc:74")

Composition.live

Composition.live(port: int = 5555) -> None

Enable the live coding eval server.

This allows you to connect to a running composition using the subsequence.live_client REPL and hot-swap pattern code or modify variables in real-time.

Security

The server executes arbitrary Python in this process — it is not a sandbox. It binds to localhost only and is opt-in, but any process on the same machine that can reach the port gains full code execution here. Do not enable it on shared or multi-user hosts, and never expose the port to a network.

Parameters

Composition.watch

Composition.watch(
    path: typing.Union[str, pathlib.Path],
    poll_interval: float = 0.25,
) -> None

Watch a Python file and reload it into the composition on every save.

The watched file is exec'd into a namespace with composition and subsequence available. @composition.pattern decorators inside the file hot-swap their corresponding running patterns in place; patterns whose function bodies have been deleted from the file are unregistered automatically on the next reload (notes stopped, removed from the running-pattern set).

An initial synchronous load happens here — if the file has a SyntaxError or doesn't exist at this moment, the exception propagates so the user knows immediately. Subsequent reloads happen on the composition's event loop and tolerate transient errors (logged, skipped).

Call BEFORE composition.play(). Reloads happen on the composition's event loop, so all mutations are thread-safe.

See the "Live coding via file watching" section of the README for the recommended wrapper-script + live-file split.

Parameters

Example:

# live_init.py — runs once
composition = subsequence.Composition(bpm=120, key="E")
composition.harmony(style="aeolian_minor")
composition.watch("live_patterns.py")
composition.play()

Composition.load_patterns

Composition.load_patterns(
    source: str,
    source_label: str = '<string>',
) -> None

Compile and apply a pattern-source string to the composition.

Equivalent to one watch() reload triggered by save, but with the source presented in-memory rather than on disk. Useful for web / REST handlers that accept pattern uploads from a trusted contributor, or for one-shot session loads with no file backing.

Behaviour mirrors watch():

Errors are raised so the caller can act on them:

In either failure case, existing composition state is preserved — the diff-and-unregister phase is skipped if exec raised, so a half-broken upload cannot tear down working patterns.

Threading

Designed to be called from a thread DIFFERENT from the composition's event loop — typically a web-handler worker. Cannot be called from inside the loop itself (a pattern callback, an asyncio task spawned by the composition). From there, await composition._apply_source_async(...) directly.

SECURITY WARNING: exec() is not sandboxed. The source has full Python access in this process. Only pass source from trusted senders. The built-in blocklist (help, input, breakpoint, exit, quit) prevents calls that would stall the event loop; it is not a security boundary.

Parameters

Composition.osc

Composition.osc(
    receive_port: int = 9000,
    send_port: int = 9001,
    send_host: str = '127.0.0.1',
    receive_host: str = '0.0.0.0',
) -> None

Enable bi-directional Open Sound Control (OSC).

Subsequence will listen for commands (like /bpm or /mute) and broadcast its internal state (like /chord or /bar) over UDP.

Parameters

Composition.osc_map

Composition.osc_map(address: str, handler: typing.Callable) -> None

Register a custom OSC handler.

Must be called after osc has been configured.

Parameters

Example:

composition.osc()

def on_intensity (address, value):
        composition.data["intensity"] = float(value)

composition.osc_map("/intensity", on_intensity)

Composition.set_bpm

Composition.set_bpm(bpm: float) -> None

Instantly change the tempo.

Parameters

When Ableton Link is active, this proposes the new tempo to the Link network instead of applying it locally. The network-authoritative tempo is picked up on the next pulse.

Composition.target_bpm

Composition.target_bpm(
    bpm: float,
    bars: int,
    shape: str = 'linear',
) -> None

Smoothly ramp the tempo to a target value over a number of bars.

Parameters

Example

# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
comp.target_bpm(140, bars=8, shape="ease_in_out")

Note

Ignored while Ableton Link is active — the shared session tempo is authoritative. Use set_bpm() to propose a tempo to the Link network.

Composition.live_info

Composition.live_info() -> typing.Dict[str, typing.Any]

Return a dictionary containing the current state of the composition.

Includes BPM, key, current bar, active section, current chord, running patterns, and custom data.

Composition.pause

Composition.pause() -> None

Hold playback where it is, keeping the composition's place.

The clock stops advancing, sounding notes are released, and MIDI Stop is sent to any hardware following the clock output. resume continues from the same pulse, beat and bar — where stopping and playing again would start the piece over.

Bar and cycle counters hold too, so patterns resume mid-phrase rather than jumping. A note cut short by the pause is not re-struck on resume; it returns on its pattern's next cycle.

Idempotent and safe to call from any thread. Ignored, with a log line, when the transport is not ours to hold — under clock_follow=True or an active Ableton Link session.

Composition.resume

Composition.resume() -> None

Continue playback from where pause held it.

Sends MIDI Continue rather than Start, so downstream hardware picks up where it left off instead of resetting to the top of its own pattern. Idempotent.

Composition.is_paused

property Composition.is_paused: bool

True while playback is held by pause.

Composition.mute

Composition.mute(name: str) -> None

Mute a running pattern by name.

The pattern continues to 'run' and increment its cycle count in the background, but it will not produce any MIDI notes until unmuted.

Parameters

Composition.unmute

Composition.unmute(name: str) -> None

Unmute a previously muted pattern.

Composition.unregister

Composition.unregister(name: str) -> None

Fully remove a running pattern from rotation.

Unlike mute() (which keeps the pattern alive but silent), unregister() tears the pattern down entirely. It sets pattern._removed = True so the sequencer's reschedule loop skips re-adding it on the next pulse; sends note_off for any of the pattern's currently-sounding notes on the primary destination AND on every mirror destination (so drones and sustaining notes stop immediately); and removes the entry from _running_patterns so it no longer appears in live_info(), the terminal grid, or any other consumer that enumerates running patterns.

Already-queued events in the sequencer's event queue play out — note_offs are paired with their note_ons at queue time, so notes end at their natural duration; only drones rely on the targeted _stop_pattern_notes pass.

Idempotent: silently logs a debug and returns if the pattern is already absent. Useful from both the live REPL (composition.live()) and the file watcher (composition.watch()), which calls this for any pattern removed from the watched file between reloads.

Parameters

Composition.mirror

Composition.mirror(
    name: str,
    device: int,
    channel: int,
    drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
) -> None

Add a mirror destination to a running pattern.

Every note, CC, pitch bend, NRPN/RPN, program change, SysEx, and drone event the pattern emits will also be sent to (device, channel), starting from the next cycle rebuild. Idempotent on (device, channel) — calling with the same destination twice does not double-fan; calling again with a different drum_note_map re-points it in place.

Parameters

Bandwidth: each mirror adds another full copy of the pattern's events. See the README "MIDI mirroring" section for the tradeoffs.

Composition.unmirror

Composition.unmirror(name: str, device: int, channel: int) -> None

Remove a single mirror destination from a running pattern.

Matches on (device, channel) only — any attached drum_note_map is ignored. Idempotent: silently does nothing if the destination is not currently mirrored. The change applies on the next cycle rebuild.

Composition.unmirror_all

Composition.unmirror_all(name: str) -> None

Remove every mirror destination from a running pattern.

Composition.tweak

Composition.tweak(name: str, **kwargs: typing.Any) -> None

Override parameters for a running pattern.

Values set here are available inside the pattern's builder function via p.param(). They persist across rebuilds until explicitly changed or cleared. Changes take effect on the next rebuild cycle.

Parameters

Example (from the live REPL):

composition.tweak("bass", pitches=[48, 52, 55, 60])

Composition.clear_tweak

Composition.clear_tweak(name: str, *param_names: str) -> None

Remove tweaked parameters from a running pattern.

If no parameter names are given, all tweaks for the pattern are cleared and every p.param() call reverts to its default.

Parameters

Composition.get_tweaks

Composition.get_tweaks(name: str) -> typing.Dict[str, typing.Any]

Return a copy of the current tweaks for a running pattern.

Parameters

Composition.schedule

Composition.schedule(
    fn: typing.Callable,
    cycle_beats: int,
    reschedule_lookahead: int = 1,
    wait_for_initial: bool = False,
    defer: bool = False,
) -> None

Register a custom function to run on a repeating beat-based cycle.

Subsequence automatically runs synchronous functions in a thread pool so they don't block the timing-critical MIDI clock. Async functions are run directly on the event loop.

Parameters

Raises

Composition.form

Composition.form(
    sections: typing.Union[subsequence.forms.Form, typing.List[typing.Any], typing.Iterator[typing.Tuple[str, int]], typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]],
    loop: bool = False,
    start: typing.Optional[str] = None,
    at_end: str = 'stop',
    key: typing.Optional[str] = None,
    scale: typing.Optional[str] = None,
) -> None

Define the structure (sections) of the composition.

You can define form in four ways:

  1. Form value: a frozen Form of Section values — the payload home (energy, key per section); editable, navigable.
  2. Sequence (List): a fixed order of (name, bars) tuples or Sections (lists coerce — they are the same form).
  3. Graph (Dict): dynamic transitions based on weights.
  4. Generator: a Python generator that yields (name, bars) pairs.

Form-value and list forms are navigable: form_jump() and form_next() work on them (the jump lands on the next occurrence of the name, wrapping).

Re-binding form() during playback takes effect at the next bar — the clock reads the current form state on every bar, so the new form advances from there (its first section plays from its first bar).

Parameters

Example

# A simple pop structure
comp.form([
        ("verse", 8),
        ("chorus", 8),
        ("verse", 8),
        ("chorus", 16)
])

# The same structure with payloads, held open at the end
S = subsequence.Section
comp.form(subsequence.Form([
        S("verse", 8, energy=0.5), S("chorus", 8, energy=0.9),
]), at_end="hold")

Composition.form_freeze

Composition.form_freeze(
    sections: typing.Optional[int] = None,
) -> subsequence.forms.Form

Freeze the graph form's walk into an editable Form.

Walks a clone of the live form state — the same RNG state, so the frozen path is exactly the path the live graph would have played — and returns it as a Form value: inspect it, edit it (path.replace(3, bars=16)), and rebind it with composition.form(path, at_end=...). The live form state is untouched (rebinding replaces it).

Parameters

Raises

Example:

composition.form({...}, start="intro")
path = composition.form_freeze()          # the walk, frozen
composition.form(path, at_end="stop")     # rebind the editable value

Composition.energy

Composition.energy(
    energies: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]],
) -> None

Set per-section energy — the arranging dial, as one plain dict.

{"verse": 0.5, "chorus": 0.9, "build": (0.3, 1.0)} — a float is the section's level; a (start, end) tuple interpolates across the section (a build). Patterns read p.energy (0.5 when nothing is configured) and gate themselves, or declare min_energy= on pattern() for automatic muting.

The dict overrides any energy payload carried by bound Section values — it is the later, performance-level dial. Re-calling replaces the whole mapping (idempotent, live-reload friendly).

Example:

composition.energy({"intro": 0.2, "verse": 0.55, "drop": 0.95})

Composition.on_section

Composition.on_section(
    callback: typing.Callable[..., typing.Any],
) -> None

Register a callback fired on every section change.

The callback receives the new SectionInfo (or None when the form finishes). It fires from the form clock, one lookahead-beat early — in time to affect the new section's first patterns — and once at play start for the opening section.

Example:

composition.on_section(lambda info: print(f"now: {info.name if info else 'end'}"))

Composition.transition

Composition.transition(
    before: str,
    fill: typing.Optional[typing.Any] = None,
    channel: typing.Optional[int] = None,
    beat: float = 0.0,
    mute: typing.Optional[typing.List[str]] = None,
    beats: typing.Optional[float] = None,
    drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
    device: subsequence.midi_utils.DeviceId = None,
) -> None

Declare boundary material — the automatic fill or mute, one line.

before names the incoming section ("chorus"), or "*" for any different section (repeats don't fire it). Two actions, combinable:

Transitions stack — call once per rule. Registration is additive and idempotent per identical rule.

Example:

composition.transition(before="*", fill=FILL, channel=10, beat=2.0)
composition.transition(before="drop", mute=["pads"], beats=4)

Composition.pattern

Composition.pattern(
    channel: int,
    beats: typing.Optional[float] = None,
    bars: typing.Optional[float] = None,
    steps: typing.Optional[float] = None,
    step_duration: typing.Optional[float] = None,
    drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
    cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
    nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
    reschedule_lookahead: float = 1,
    voice_leading: bool = False,
    device: subsequence.midi_utils.DeviceId = None,
    mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
    min_energy: typing.Optional[float] = None,
) -> typing.Callable

Register a function as a repeating MIDI pattern.

The decorated function will be called once per cycle to 'rebuild' its content. This allows for generative logic that evolves over time.

Two ways to specify pattern length:

Parameters

Example

@comp.pattern(channel=1, beats=4)
def chords (p):
        p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)

@comp.pattern(channel=1, bars=2)
def long_phrase (p):
        ...

@comp.pattern(channel=1, steps=6, step_duration=dur.SIXTEENTH)
def riff (p):
        p.sequence(steps=[0, 1, 3, 5], pitches=60)

Composition.layer

Composition.layer(
    *builder_fns: typing.Callable,
    channel: int,
    beats: typing.Optional[float] = None,
    bars: typing.Optional[float] = None,
    steps: typing.Optional[float] = None,
    step_duration: typing.Optional[float] = None,
    drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
    cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
    nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
    reschedule_lookahead: float = 1,
    voice_leading: bool = False,
    device: subsequence.midi_utils.DeviceId = None,
    mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
) -> None

Combine multiple functions into a single MIDI pattern.

This is useful for composing complex patterns out of reusable building blocks (e.g., a 'kick' function and a 'snare' function).

See pattern() for the full description of beats, bars, steps, and step_duration.

Parameters

Composition.chords

Composition.chords(
    *,
    channel: int,
    progression: subsequence.progressions.ProgressionSource,
    harmonic_rhythm: subsequence.progressions.HarmonicRhythmSpec,
    bars: typing.Optional[float] = None,
    beats: typing.Optional[float] = None,
    voicing: subsequence.progressions.VoicingSpec = (3, 4),
    velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY,
    detached: typing.Optional[float] = None,
    root: int = 60,
    key: typing.Optional[str] = None,
    seed: typing.Optional[int] = None,
    device: subsequence.midi_utils.DeviceId = None,
    mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
) -> subsequence.progressions.Progression

Declare a self-contained chord part: a progression at a chosen harmonic rhythm.

The one-call form of p.progression() — it registers a pattern on channel that plays progression across bars (or beats), each chord lasting a length drawn from harmonic_rhythm (the musical term for how often the chords change). It needs no composition.harmony() call and, with an explicit chord list or a key=, no composition key either — so a drums-plus-one-chord-part sketch stays simple.

The progression is realised once, up front, and the same timeline plays every cycle (a stable phrase). That timeline is returned so you can see exactly what was chosen — print(comp.chords(...)).

Parameters

Returns

Composition.phrase_part

Composition.phrase_part(
    *,
    channel: int,
    part: typing.Optional[str] = None,
    root: int = 60,
    bars: typing.Optional[float] = None,
    beats: typing.Optional[float] = None,
    velocity: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
    fit: typing.Optional[float] = None,
    device: subsequence.midi_utils.DeviceId = None,
    mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
) -> None

Declare a part that plays each section's bound Motif/Phrase.

The one-call consumer for section_motifs — it registers a pattern on channel that walks whatever value is bound to the current section for part (stateless position from the cycle counter, via p.phrase()). A section with no binding for the part is silent for that part — bind material or don't; no fallback guessing.

Parameters

Example:

composition.section_motifs("verse",  verse_line,  part="lead")
composition.section_motifs("chorus", chorus_line, part="lead")
composition.phrase_part(channel=4, part="lead", root=72, bars=2)

Composition.trigger

Composition.trigger(
    fn: typing.Callable,
    channel: int,
    beats: typing.Optional[float] = None,
    bars: typing.Optional[float] = None,
    steps: typing.Optional[float] = None,
    step_duration: typing.Optional[float] = None,
    quantize: float = 0,
    drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
    cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
    nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
    chord: bool = False,
    device: subsequence.midi_utils.DeviceId = None,
    mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
) -> None

Trigger a one-shot pattern immediately or on a quantized boundary.

This is useful for real-time response to sensors, OSC messages, or other external events. The builder function is called immediately with a fresh PatternBuilder, and the generated events are injected into the queue at the specified quantize boundary.

The builder function has the same API as a @composition.pattern decorated function and can use all PatternBuilder methods: p.note(), p.euclidean(), p.arpeggio(), and so on.

See pattern() for the full description of beats, bars, steps, and step_duration. Default is 1 beat.

Parameters

Example

# Immediate single note (channels are 1-16 by default)
composition.trigger(
        lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
        channel=1
)

# Quantized fill (next bar) — channel 10 is the GM drum channel
import subsequence.constants.durations as dur
composition.trigger(
        lambda p: p.euclidean("snare", pulses=7, velocity=90),
        channel=10,
        drum_note_map=gm_drums.GM_DRUM_MAP,
        quantize=dur.WHOLE
)

# With chord context — the builder receives the chord as a second
# argument when chord=True.
composition.trigger(
        lambda p, chord: p.arpeggio(chord.tones(root=60), spacing=dur.SIXTEENTH),
        channel=1,
        quantize=dur.QUARTER,
        chord=True
)

Composition.is_clock_following

property Composition.is_clock_following: bool

True if either the primary or any additional device is following external clock.

Composition.play

Composition.play() -> None

Start the composition.

This call blocks until the program is interrupted (e.g., via Ctrl+C). It initializes the MIDI hardware, launches the background sequencer, and begins playback.

Composition.render

Composition.render(
    bars: typing.Optional[int] = None,
    filename: str = 'render.mid',
    max_minutes: typing.Optional[float] = 60.0,
) -> None

Render the composition to a MIDI file without real-time playback.

Runs the sequencer as fast as possible (no timing delays) and stops when the first active limit is reached. The result is saved as a standard MIDI file that can be imported into any DAW.

All patterns, scheduled callbacks, and harmony logic run exactly as they would during live playback — BPM transitions, generative fills, and probabilistic gates all work in render mode. The only difference is that time is simulated rather than wall-clock driven.

Parameters

Raises

Examples

# Default: renders up to 60 minutes of MIDI content.
composition.render()

# Render exactly 64 bars (time cap still active as backstop).
composition.render(bars=64, filename="demo.mid")

# Render up to 5 minutes of an infinite generative composition.
composition.render(max_minutes=5, filename="five_min.mid")

# Remove the time cap — must supply bars instead.
composition.render(bars=128, max_minutes=None, filename="long.mid")