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:
- Initialize
Compositionwith BPM and Key. - Define harmony and form (optional).
- Register patterns using the
@composition.patterndecorator. - Call
composition.play()to start the music.
Initialize a new composition.
Parameters
-
output_device: Which MIDI output port to use, matched againstmido.get_output_names(). The name is treated as a pattern:*stands for any run of characters and?for exactly one, matching is case-insensitive, and a name with no wildcards is simply a substring — so a plain"Scarlett"finds the port without typing the rest. An exact name always wins outright.Wildcards matter on Linux/ALSA, where names carry the client and port ids (e.g.
"Scarlett 2i4 USB:Scarlett 2i4 USB MIDI 1 16:0"). The client id —16here — is handed out in connection order and moves between reboots or when a virtual port is recreated, while the port index after it (0) stays put. Wildcard the one that moves and keep the one that does not:"*Scarlett 2i4 USB *:0"Keep that trailing port index. A multi-port interface reports one name per port, so
"*U6MIDI Pro*"matches all three ports of a 3-port unit and asks which you meant at every launch, while"*U6MIDI Pro *:0"names one for good. Prefer*to?—?matches a single character, so a pattern written for16:0quietly stops matching once ids reach three digits. To look up the current names:import mido for n in mido.get_output_names(): print(n)If
None, Subsequence auto-discovers — uses the only available device, or prompts to choose if several exist. -
bpm: Initial tempo in beats per minute (default 120). -
time_signature: The metre as(beats, unit), default(4, 4). Sets the bar length everywhere bars matter:bars=pattern lengths,p.bar/p.signal(), form advancement and transitions, and pinned-chord bar numbers. -
key: The root key of the piece (e.g., "C", "F#", "Bb"). Required if you plan to useharmony(). -
scale: The scale/mode of the piece (e.g. "minor", "dorian", or any registered scale name). Used to resolve scale degrees in motifs; defaults to major (ionian) when unset. -
seed: An optional integer for deterministic randomness. When set, every random decision (chord choices, drum probability, etc.) will be identical on every run. -
record: When True, record all MIDI events to a file. -
record_filename: Optional filename for the recording (defaults to timestamp). -
zero_indexed_channels: When False (default), MIDI channels use 1-based numbering (1-16) matching instrument labelling. Channel 10 is drums, the way musicians and hardware panels show it. When True, channels use 0-based numbering (0-15) matching the raw MIDI protocol. -
latency_ms: Physical output latency of the primary device in milliseconds, for delay compensation (default 0.0, must be non-negative). Set this when the primary output sounds late (e.g. a software sampler) so Subsequence delays faster devices to line everything up. Seemidi_output()for additional devices.
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
style: The harmonic style to use. Built-in: "functional_major" (alias "diatonic_major"), "hooktheory_major" (alias "pop_major"), "turnaround", "aeolian_minor", "phrygian_minor", "lydian_major", "dorian_minor", "chromatic_mediant", "suspended", "mixolydian", "whole_tone", "diminished". See README for full descriptions.cycle_beats: How many beats each live chord lasts (default 4). Bound progressions carry their own harmonic rhythm in their spans, so this applies to live stepping only. A re-call during playback takes effect from the next chord boundary; a FIRST harmony() call mid-playback starts the clock itself.dominant_7th: Whether to include V7 chords (default True).gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement expectations.minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.root_diversity: Root-repetition damping (0.0 to 1.0). Each recent chord sharing a candidate's root reduces the weight to 40% at the default (0.4). Set to 1.0 to disable.reschedule_lookahead: How many beats in advance to calculate the next chord.progression: A progression to bind to the global clock. Key- relative content resolves now, against the composition key and scale (binding freezes one realisation).
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
bars: Number of chords to capture (one per harmony cycle).end: The chord at the final bar —end="V"is the cadential major dominant in minor.pins:{bar: chord}— 1-based fiat positions.avoid: Chords excluded from the walk.cadence: A cadence name ("strong"/"soft"/"open"/"fakeout", theory aliases accepted) — its formula pins the final bars, so the walk approaches the close. Conflicts withend=or pins on those bars.
Returns
- A
Progressionwith the captured chords and trailing history for NIR continuity.
Raises
ValueError: Ifharmonyhas not been called first, or the constraints are contradictory or unsatisfiable.
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
section_name: Name of the section as defined inform.progression: The progression to bind.
Raises
ValueError: If a graph-based form has been configured and section_name is not one of its sections. List and generator forms yield names lazily, so they cannot be validated here. (A key-relative progression with no resolvable key for the section is caught atplay/render, once the form's keys are known.)
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
bar: 1-based bar number (the musician count).chord: A chord name, int degree, roman string,Chord,PitchSet, orNoneto unpin. A key-relative spec (int degree, roman) re-keys like section harmony: it resolves late, against the effective key of the section sounding at that bar (sopin_chord(8, "V")is the dominant of wherever bar 8 lands). A concrete spec (name,Chord,PitchSet) is absolute and never moves.
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
cadence: The cadence name.bar: The 1-based bar the cadence's final chord arrives at (required; in practice ≥ 2 — bar 1 cannot be approached).
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
section_name: Name of the section as defined inform.value: AMotiforPhrase(anything exposing.length/.sliceplaces).part: Optional part label, so one section can carry several bindings ("lead","bass", ...).
Raises
ValueError: If a graph-based form has been configured and section_name is not one of its sections.
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
enabled:True(default) to enable hotkeys;Falseto disable.
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
key: A single character trigger (e.g."a","1"," ").action: Zero-argument callable to execute.quantize:0= execute immediately (default).N= execute on the next global bar number divisible by N.label: Display name for the?help listing. Auto-derived from the function name or lambda body if omitted.
Raises
ValueError: Ifkeyis the reserved?character, or ifkeyis not exactly one character.
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
section_name: The section to jump to.
Raises
ValueError: If no form is configured, or the form is a generator, or section_name is unknown.
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
section_name: The section to queue.
Raises
ValueError: If no form is configured, or the form is a generator, or section_name is unknown.
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
name: The stream name — usually a pattern name.
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
name: The stream name — usually a pattern name.
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:
source: path to a Scala.sclfile.cents: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).ratios: list of frequency ratios (e.g.,[9/8, 5/4, 4/3, 3/2, 2]).equal: integer for N-tone equal temperament (e.g.,equal=19).
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
source: Path to a.sclfile.cents: Cent offsets for scale degrees 1..N.ratios: Frequency ratios for scale degrees 1..N.equal: Number of equal divisions of the period.bend_range: Synth pitch-bend range in semitones (default ±2).channels: Channel pool for polyphonic rotation.reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).exclude_drums: When True (default), skip patterns that have adrum_note_map(they use fixed GM pitches, not tuned ones).
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
enabled: Whether to show the display (default True).grid: When True, render an ASCII grid visualisation of all running patterns above the status line. The grid updates once per bar, showing which steps have notes and at what velocity.grid_scale: Horizontal zoom factor for the grid (default1.0). Higher values add visual columns between grid steps, revealing micro-timing from swing and groove. Snapped to the nearest integer internally for uniform marker spacing.
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
device: Which MIDI input port to use, matched againstmido.get_input_names(). Treated as a pattern —*and?are wildcards, matching is case-insensitive, and a name without wildcards is a substring. SeeComposition.__init__for why a pattern like"*Launchpad *:0"survives an ALSA client id changing between runs. Because a wrong input would desynchronise or mis-record a performance, a pattern matching nothing raises, and one matching several asks which you meant rather than guessing.clock_follow: If True, Subsequence will slave its clock to incoming MIDI Ticks. It will also follow MIDI Start/Stop/Continue commands. Only one device can have this enabled at a time.name: Optional alias for use withcc_map(input_device=…)andcc_forward(input_device=…). When omitted, the raw device name is used.
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
device: Which MIDI output port to add, matched againstmido.get_output_names(). Treated as a pattern —*and?are wildcards, matching is case-insensitive, and a name without wildcards is a substring. SeeComposition.__init__for the lookup snippet and why a pattern like"*U6MIDI Pro *:0"survives an ALSA client id changing between runs.name: Optional alias for use withpattern(device=…),cc_forward(output_device=…), etc. When omitted, the raw device name is used.latency_ms: Physical output latency of this device in milliseconds, for delay compensation (default 0.0, must be non-negative). Set this when the device sounds late (e.g. a software sampler) so Subsequence delays faster devices to line everything up.
Returns
int: The integer device index assigned (1, 2, 3, …).
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
enabled: Whether to send MIDI clock (default True).
Example
comp = subsequence.Composition(bpm=120, output_device="...")
comp.clock_output() # hardware will follow Subsequence tempo
Composition.link
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
quantum: Beat cycle length.4.0(default) = one bar in 4/4 time. Change this if your composition uses a different meter.
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
cc: MIDI Control Change number (0–127).data_key: Thecomposition.datakey to write.channel: If given, only respond to CC messages on this channel. Uses the same numbering convention aspattern()(1-16 by default, or 0-15 withzero_indexed_channels=True).Nonematches any channel (default).min_val: Scaled minimum — written when CC value is 0 (default 0.0).max_val: Scaled maximum — written when CC value is 127 (default 1.0).input_device: Only respond to CC messages from this input device (index or name).Noneresponds to any input device (default).
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
channel: If given, only track notes on this channel. Uses the same numbering convention aspattern()(1-16 by default, or 0-15 withzero_indexed_channels=True).Nonetracks any channel (default).release_ms: How long (milliseconds) a released note keeps counting as held. This smooths the momentary all-keys-up gap during a hand-position change so the arp does not drop to silence. Default 30.0; set 0.0 to release instantly. Ignored whenlatchis True.latch: When True, the held set persists after you lift your hands until you play a new chord (the first key after every key is up replaces it) — like a hardware arp's latch.input_device: Only track notes from this input device (index or name).Nonetracks any input device (default).
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
-
cc: Incoming CC number to listen for (0–127). -
output: What to send. Either a preset string:"cc"— identity forward, same CC number and value."cc:N"— forward as CC number N (e.g."cc:74")."pitchwheel"— scale 0–127 to -8192..8191 and send as pitch bend.
Or a callable with signature
(value: int, channel: int) -> Optional[mido.Message]. Return a fully formedmido.Messageto send, orNoneto suppress.channelis 0-indexed (the incoming channel). -
channel: If given, only respond to CC messages on this channel. Uses the same numbering convention ascc_map().Nonematches any channel (default). -
output_channel: Override the output channel.Noneuses the incoming channel. Uses the same numbering convention aspattern(). -
input_device: Only respond to CC from this input device — an index, a registered name, orNonefor any input (default), the same convention ascc_map(). -
output_device: Send to this output device — an index, a registered name, orNonefor the primary output (default). -
mode: Dispatch mode:"instant"(default) — send immediately on the MIDI input callback thread. Lowest latency (~1–5 ms). Instant forwards are not recorded when recording is enabled."queued"— inject into the sequencer event queue and send at the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards are recorded when recording is enabled.
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
port: The TCP port to listen on (default 5555).
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
path: Path to the Python file to watch.poll_interval: Seconds betweenmtimepolls (default 0.25 s).
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():
- The source is exec'd into a fresh namespace with
compositionandsubsequencein scope. @composition.patterndecorators in the source hot-swap their corresponding running patterns in place.- Patterns currently running but not declared in the source are unregistered — the source is treated as the full new truth.
- If the composition is already playing, the swap happens on the event loop thread; the call blocks until it completes.
- If the composition has not yet called
play(), the source runs on the caller's thread; decorators populate_pending_patternsandplay()picks them up in the usual way.
Errors are raised so the caller can act on them:
SyntaxErrorifsourcefails to compile.- The exception raised inside
exec()for any runtime error. RuntimeErrorif called from inside the composition's own event loop thread (would deadlock — see Threading below).
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
source: Python source declaring@composition.patternfunctions.source_label: Identifier used in compile errors and tracebacks (appears as the filename inSyntaxErrorand__file__- style traceback lines). Default"<string>".
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
receive_port: Port to listen for incoming OSC messages (default 9000).send_port: Port to send state updates to (default 9001).send_host: The IP address to send updates to (default "127.0.0.1").receive_host: Interface to listen on (default "0.0.0.0" — all interfaces, so external OSC controllers on the LAN can reach it). The listener can change tempo, mute patterns, and write data, so on an untrusted network restrict it withreceive_host="127.0.0.1".
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
address: OSC address pattern to match (e.g."/my/param").handler: Callable invoked with(address, *args)when a matching message arrives.
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
bpm: The new tempo in beats per minute.
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
bpm: Target tempo in beats per minute.bars: Duration of the transition in bars.shape: Easing curve name. Defaults to"linear"."ease_in_out"or"s_curve"are recommended for natural- sounding tempo changes. Seesubsequence.easingfor all available shapes.
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
name: The function name of the pattern to mute.
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
name: Function name of the pattern to remove.
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
name: Function name of the pattern to mirror.device: Output device index (the integer returned frommidi_output(); 0 = primary device).channel: MIDI channel using this composition's numbering convention (1-16 by default; 0-15 ifzero_indexed_channels=True).drum_note_map: Optional per-destination drum map. When set, mirrored drum hits are re-resolved by name through it, so a named voice lands on this device's own note number — see the README "MIDI mirroring" section.
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
name: The function name of the pattern.**kwargs: Parameter names and their new values.
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
name: The function name of the pattern.*param_names: Specific parameter names to clear. If omitted, all tweaks are removed.
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
name: The function name of the pattern.
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
fn: The function to call.cycle_beats: How often to call it (e.g., 4 = every bar).reschedule_lookahead: How far in advance to schedule the next call.wait_for_initial: If True, run the function once during startup and wait for it to complete before playback begins. This ensurescomposition.datais populated before patterns first build. Impliesdefer=Truefor the repeating schedule.defer: If True, skip the pulse-0 fire and defer the first repeating call to just before the second cycle boundary.
Raises
RuntimeError: If called afterplay()has started — scheduled tasks register at startup, so a late registration would be silently ignored otherwise.
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:
- Form value: a frozen
FormofSectionvalues — the payload home (energy, key per section); editable, navigable. - Sequence (List): a fixed order of
(name, bars)tuples or Sections (lists coerce — they are the same form). - Graph (Dict): dynamic transitions based on weights.
- 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
sections: The form definition (Form, List, Dict, or Generator).loop: Sugar forat_end="loop".start: The section to start with (Graph mode only).at_end: What happens when a sequence form runs out —"stop"(the form finishes and patterns see no section; default),"hold"(the final section repeats until navigated away from), or"loop"(start over). Graphs end via their terminal sections instead.key: A form-level key — the form tier of the key-source chain (Section.keyoverrides it; it overrides the composition key). Re-anchors key-relative content for the whole form. When sections is aFormvalue carrying its ownkey, that value is used unless this argument overrides.scale: A form-level scale/mode, paired withkey.
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
sections: Number of sections to freeze. Without it, the walk runs until a terminal section; a graph with no terminal sections requiressections=explicitly.
Raises
ValueError: If no graph form is bound (a list form is already a frozen sequence), the form has already finished, or the walk cannot terminate.
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:
fill=(+channel=,beat=): a Motif played in the last bar before the boundary, starting atbeatof that bar. Drum names resolve throughdrum_note_map=if given, otherwise the map is borrowed from a registered pattern on the same channel.mute=(+beats=): pattern names muted over the approach and unmuted at the boundary. Muting is bar-granular (the existing rule), sobeatsrounds up to whole bars. Performer mutes win: a pattern you muted yourself stays muted.
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:
- Duration mode (default): use
beats=orbars=. The grid defaults to sixteenth-note resolution. - Step mode: use
steps=paired withstep_duration=. The grid equals the step count, sop.hit_steps()indices map directly to steps.
Parameters
channel: MIDI channel. By default uses 1-based numbering (1-16). Setzero_indexed_channels=Trueon theCompositionto use 0-based numbering (0-15), matching the raw MIDI protocol, instead.beats: Duration in beats (quarter notes).beats=4= 1 bar.bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).bars=2= 8 beats.steps: Step count for step mode. Requiresstep_duration=.step_duration: Duration of one step in beats (e.g.dur.SIXTEENTH). Requiressteps=.drum_note_map: Optional mapping for drum instruments.cc_name_map: Optional mapping of CC names to MIDI CC numbers. Enables string-based CC names inp.cc()andp.cc_ramp().nrpn_name_map: Optional mapping of NRPN parameter names (strings) to 14-bit parameter numbers (0–16383). Enables string-based names inp.nrpn()andp.nrpn_ramp()— typically a device-specific dictionary (e.g. Sequential Take 5'sOsc1FreqFine→ 9).reschedule_lookahead: Beats in advance to compute the next cycle. At0each cycle is built on its own first beat, so it hears the very latest state (a held chord, a just-moved control) and its downbeat sounds late by however long that build takes.voice_leading: If True, chords in this pattern will automatically use inversions that minimize voice movement.mirrors: Optional list of additional(device, channel)destinations to duplicate every event from this pattern onto. Notes, CCs, pitch bend, NRPN/RPN bursts, program changes, SysEx, and drone events are all mirrored; OSC events are not (OSC is not bound to a MIDI port).deviceis the integer index returned bymidi_output()(0 = primary).channelfollows this composition's channel-numbering convention. See alsomirror()/unmirror()for live toggling.min_energy: Automatic energy gating — the pattern is silent while the current section's energy (composition.energy()dict, or the bound Section payload) is below this threshold. Composes withmute(): a performer mute always wins.
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
builder_fns: One or more pattern builder functions.channel: MIDI channel (1-16, or 0-15 withzero_indexed_channels=True).beats: Duration in beats (quarter notes).bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).steps: Step count for step mode. Requiresstep_duration=.step_duration: Duration of one step in beats. Requiressteps=.drum_note_map: Optional mapping for drum instruments.cc_name_map: Optional mapping of CC names to MIDI CC numbers.nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit parameter numbers.reschedule_lookahead: Beats in advance to compute the next cycle. At0each cycle is built on its own first beat, so it hears the very latest state (a held chord, a just-moved control) and its downbeat sounds late by however long that build takes.voice_leading: If True, chords use smooth voice leading.mirrors: Optional list of additional(device, channel)destinations to duplicate every event onto. Seepattern()for details.
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
channel: MIDI channel for the chord part.progression: A chord-graph style name to generate from, or an explicit list of chords (Chordobjects or names like["Cm7", "Dbmaj7"]).harmonic_rhythm: How long each chord lasts — a number, a list of lengths, orbetween(low, high, step=...). Seep.progression().bars / beats: Length of the part (defaults to 4 beats if neither is given).barsuses the composition's time signature.voicing: Notes per chord — an int, or a(low, high)range (e.g.(3, 4)).velocity: MIDI velocity, or a(low, high)tuple for per-voice humanisation.detached: Beats of silence before each next chord (duration = length - detached).root: MIDI root the voicings are centred on (e.g. 48 = C3).key: Key for a generated progression; defaults to the composition key.seed: Seed for the (otherwise fixed) realisation; defaults to the composition seed, so the part is reproducible.device: Optional output-device override.mirrors: Optional additional(device, channel)destinations.
Returns
- The realised
Progression.
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
channel: MIDI channel for the part.part: The part label to read from the registry (None= the unlabelled binding).root: Register anchor for degree resolution.bars / beats: Cycle length of the part (defaults to 4 beats); the phrase is sliced one cycle window at a time.velocity: Optional override applied to every note.fit: Passed through (active with the melody engine stage).device: Optional output-device override.mirrors: Optional additional(device, channel)destinations.
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
fn: The pattern builder function (same signature as@comp.pattern).channel: MIDI channel (1-16, or 0-15 withzero_indexed_channels=True).beats: Duration in beats (quarter notes, default 1).bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).steps: Step count for step mode. Requiresstep_duration=.step_duration: Duration of one step in beats. Requiressteps=.quantize: Snap the trigger to a beat boundary:0= immediate (default),1= next beat (quarter note),4= next bar. Usedur.*constants fromsubsequence.constants.durations.drum_note_map: Optional drum name mapping for this pattern.cc_name_map: Optional mapping of CC names to MIDI CC numbers.nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit parameter numbers.chord: IfTrue, the builder function receives the current chord as a second parameter (same as@composition.pattern).mirrors: Optional list of additional(device, channel)destinations to fire this one-shot onto in parallel with the primary destination.
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
bars: Number of bars to render, orNonefor no bar limit (defaultNone). When both bars and max_minutes are active, playback stops at whichever limit is reached first.filename: Output MIDI filename (default"render.mid").max_minutes: Safety cap on the length of rendered MIDI in minutes (default60.0). PassNoneto disable the time cap — you must then provide an explicit bars value.
Raises
ValueError: If both bars and max_minutes areNone, which would produce an infinite render.
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")