Subsystem Search

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. Initialise 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.

Initialise a new composition.

Parameters

Example

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

Members: bar_beats, 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

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.bar_beats

property Composition.bar_beats: float

How many beats (quarter notes) one bar lasts: beats × 4 / unit, so 3.0 in 6/8.

Composition.harmony

Composition.harmony(
    style: typing.Optional[typing.Union[str, subsequence.chord_graphs.ChordGraph]] = None,
    cycle_beats: typing.Optional[float] = KEEP,
    dominant_7th: bool = KEEP,
    key_pull: float = KEEP,
    nir_strength: float = KEEP,
    minor_turnaround_weight: float = KEEP,
    root_diversity: float = KEEP,
    reschedule_lookahead: float = KEEP,
    progression: typing.Optional[typing.Any] = KEEP,
    **retired: typing.Any,
) -> 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").

A re-call changes only what it names. Anything left out keeps the value the last call gave it, so harmony(key_pull=0.4) after harmony(style="aeolian_minor", cycle_beats=8, nir_strength=0.9) leaves the style, the harmonic rhythm and the inertia where they were. Pass progression=None to unbind a bound progression - the walk falls back to live stepping at the next chord boundary.

Parameters

Example

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

# 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 generate live chords - when there is a live engine to generate them. With no harmony on the piece there is nothing to generate from, so a section left out holds the chord before it for its whole length, and a line at startup names which sections those are. Give every section its own chords, or call harmony once for the piece.

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.

Pin a chord the style would never reach and the walk carries on from the style's own chord on that root - E7 continues the way Em does. Where the style has nothing on that root at all, the bar after the pin is the tonic. Either way the pin sounds, and one bar later the piece is walking again.

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").

"bar" passes the bar's index from 0. "beat" passes the beat's index within its bar, counted in the time signature's unit: 0–3 in 4/4, 0–5 in 6/8, 0–6 in 7/8. That counter is the one place a beat is not a quarter note.

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 quantisation 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 quantisation. During playback a jump is a section change like any other: on_section callbacks hear the section it lands on, and parts muted by transition() for the boundary it skipped play again. 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(), including a seed given back with seed=. Returns None when the composition is unseeded and the stream was given no seed.

Example

hook_seed = composition.seed_for("hook")

Composition.reroll

Composition.reroll(name: str, seed: typing.Optional[int] = None) -> None

Deal a named stream a fresh deterministic seed to try a new variation, or give it back one you noted.

Prints the stream's new effective seed. A variation you like survives a restart either way round: keep the reroll() calls in your script, which deal the same seeds in the same order on every run, or pass the printed seed back with seed=, which gives the stream exactly that seed until the next plain reroll(name). lock() pins a stream for the session. Refuses on locked names.

Parameters

Example

comp.reroll("lead")                   # prints: reroll('lead') -> effective seed 2994986849 ...
comp.reroll("lead", seed=2994986849)  # that variation again, on any run

Composition.lock

Composition.lock(name: str) -> None

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

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 realises 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), and to every trigger() one-shot and transition fill as it is built. Drum patterns (those registered with a drum_note_map) are excluded by default.

Supply exactly one of the source parameters:

A MIDI note plays the degree it lands on, counting up from reference_note. With twelve degrees each note keeps its name, so ordinary music is retuned note for note: under the just table below, E is 14 cents flat and G 2 cents sharp. With fewer, consecutive notes step through the scale - under a seven-degree table MIDI 60, 61 and 62 play its first three degrees, C, D and E - so write such a part in degrees, one MIDI number to each (#3474).

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).

Only a part whose notes overlap plays through the pool. A part that plays one note at a time keeps its own MIDI channel, and its own pitch wheel. Once a part has played overlapping notes through the pool it stays there, so a bar where it plays a single note sits on the pool's first MIDI channel rather than moving its instrument. Two parts that both need the pool would retune each other, so that is warned about, naming them; give one of them a pool of its own with p.apply_tuning(channels=...).

Parameters

Example

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

# Just intonation in C: each of the twelve notes at its just ratio
comp.tuning(ratios=[16/15, 9/8, 6/5, 5/4, 4/3, 45/32, 3/2, 8/5, 5/3, 9/5, 15/8, 2])

# A seven-degree just scale: consecutive notes step through it
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
comp.tuning("31tet.scl", channels=[1, 2, 3, 4, 5, 6])

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.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 synthesisers, 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: typing.Optional[float] = None) -> 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.

What you type is a declaration, and the piece takes it as one. A new @composition.pattern comes in at the next whole multiple of its own length, so a one-bar part lands on a bar line and a four-bar part on a four-bar line, however the typing fell. Re-declaring a part that is already playing swaps its body in without disturbing its cycle count, its tweaks or its mirrors. What you add this way is yours: a save of a watched file removes only what that file stopped declaring, and unregister() is how you take a typed part out again.

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.

The watched file runs in a fresh namespace on every save, holding only composition and subsequence: a name defined in the script that calls watch() cannot be seen there, and whatever the file creates at its top level is created again. State that must outlive a save goes on composition.data, set up once before watch() and read back in the watched file. One-time setup (devices, harmony(), form()) belongs in that calling script too, or every save runs it again.

A save replaces a running pattern's body, and any decorator argument the save changed (channel, beats/bars, reschedule_lookahead, min_energy, mirrors, the maps), all heard from its next cycle. An argument the save left alone keeps what the performance did to it, such as a mirror(). device is the exception, because opening a port while the clock runs would be heard: a pattern moves to a new device when the composition restarts.

A part the save adds comes in on the next whole multiple of its own length - a one-bar part on the next bar, a four-bar part on the next four-bar line - so it sits on the grid however the timing of the save fell, and stays there.

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 = '127.0.0.1',
) -> 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.

A drone it is holding is released at the start of its first silent cycle, on every destination it plays to. It has to be: its builder is what would have turned the drone off, and a muted builder does not run, so the note used to ring until the performance stopped. Unmuting does not strike it again - what sounds is the builder's decision, and it will place a fresh one if it wants one. The same goes for a part the energy gate closes or a transition holds quiet.

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 this 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.

Another pattern sharing the MIDI channel keeps playing. Its notes are its own to end, and cutting them is what used to happen - a pad's four-beat note stopped 0.08 of a beat in when an arp beside it was unregistered (#2996). A note with no pattern behind it, from trigger() or sent straight to a port, is still released: nothing else is coming to end it.

Its own note-ons still waiting in the queue are dropped, so a drone struck inside the reschedule lookahead does not play after the release pass and ring for the rest of the piece. Everything else queued plays out - note_offs are paired with their note_ons at queue time, so ordinary notes end at their natural duration.

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

Trade-offs: each mirror adds another full copy of the pattern's events, which can crowd a slow DIN-MIDI link. A tuned part's rotation across a MIDI channel pool collapses onto the mirror's one MIDI channel, so the mirror cannot play it in tune. OSC events are not mirrored.

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. While the piece plays, it shortens Python's thread switch interval (sys.setswitchinterval()) to 0.2 ms, from a default of 5 ms, so a function busy computing does not make the notes late. Async functions are run directly on the event loop. In render() each call finishes before the render moves on, plain or async, so a function that feeds the patterns renders the same file on every run with the same seed. A plain function still running when the piece stops is given two seconds to finish, then left behind, so one that never returns cannot keep the program from ending.

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 - once at play start for the opening section, and when form_jump() moves to a section.

Because it fires from the clock, it must be an ordinary def: an async def is refused here, when you write it. To start async work from a section change, hand it to the running loop:

def on_section (info):
    asyncio.get_running_loop().create_task(tell_the_lighting_desk(info))

A callback that raises is logged and the others still run - one broken listener never stops the music.

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 MIDI channel 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 MIDI channel 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 quantised 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 boundary quantize names.

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, on the next bar in any metre - 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=composition.bar_beats
)

# 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 initialises the MIDI hardware, launches the background sequencer, and begins playback.

A Composition runs once: the performance closes its ports and takes its patterns with it, so a second play() or render() raises. Build a new Composition per take - see render for the shape.

Raises

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: it opens with the composition's time signature and starting tempo, and a tempo change is written where it happens, so the DAW's bar lines fall where the music's do. The file lasts exactly as long as the render, and a note still sounding at its end is released there.

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 differences are that time is simulated rather than wall-clock driven, and that each call of a function given to schedule() finishes before the render moves on, so a seeded render is the same file every time.

A render reaches no MIDI device. It sends nothing and opens no input, and it keeps its own clock whatever clock_follow or link() asked for. So a piece written for a controller renders without it, as though no control had moved.

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")

A Composition renders once. For several takes, build one each time - a function that returns a fresh Composition is the whole trick, and it keeps each take's seed honest:

def take (seed):
        composition = subsequence.Composition(bpm=120, key="A", scale="minor", seed=seed)
        # … patterns, harmony, form …
        return composition

for seed in (1, 2, 3):
        take(seed).render(bars=32, filename=f"take_{seed}.mid")

Raises