subsequence.composition¶
The top-level Composition — the object a whole piece is built on.
Composition is the single entry point: it owns the clock, the patterns,
the harmony and form state, and the MIDI output. You create one, decorate
pattern functions onto it with @composition.pattern, and call play().
HarmonyView and ScheduleContext here are the read-only views handed to
pattern and callback functions at run time.
Module Contents¶
- class subsequence.composition.Composition(output_device: str | None = None, bpm: float = 120, time_signature: Tuple[int, int] = (4, 4), key: str | None = None, scale: str | None = None, seed: int | None = None, record: bool = False, record_filename: str | None = None, zero_indexed_channels: bool = False, latency_ms: float = 0.0)[source]¶
The top-level controller for a musical piece.
The
Compositionobject 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 –
The exact name of the MIDI output port to use, as reported by
mido.get_output_names(). Matching is strict — the string must equal an entry in that list verbatim. On Linux/ALSA, names include the client and port IDs (e.g."Scarlett 2i4 USB:Scarlett 2i4 USB MIDI 1 16:0"); the trailing:client:portdigits are assigned in connection order and can change between reboots or when a virtual port is recreated. 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).
key – The root key of the piece (e.g., “C”, “F#”, “Bb”). Required if you plan to use
harmony().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. See
midi_output()for additional devices.
Example
comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
- cc_forward(cc: int, output: str | Callable, *, channel: int | None = None, output_channel: int | None = None, mode: str = 'instant', input_device: subsequence.midi_utils.DeviceId = None, output_device: subsequence.midi_utils.DeviceId = None) None[source]¶
Forward an incoming MIDI CC to the MIDI output in real-time.
Unlike
cc_map()which writes incoming CC values tocomposition.datafor use at pattern rebuild time,cc_forward()routes the signal directly to the MIDI output — bypassing the pattern cycle entirely.Both
cc_map()andcc_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 as
cc_map().Nonematches any channel (default).output_channel – Override the output channel.
Noneuses the incoming channel. Uses the same numbering convention aspattern().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")
- cc_map(cc: int, data_key: str, channel: int | None = None, min_val: float = 0.0, max_val: float = 1.0, input_device: subsequence.midi_utils.DeviceId = None) None[source]¶
Map an incoming MIDI CC to a
composition.datakey.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 – The
composition.datakey to write.channel – If given, only respond to CC messages on this channel. Uses the same numbering convention as
pattern()(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")
- chords(*, channel: int, progression: subsequence.progressions.ProgressionSource, harmonic_rhythm: subsequence.progressions.HarmonicRhythmSpec, bars: float | None = None, beats: float | None = None, voicing: subsequence.progressions.VoicingSpec = (3, 4), velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY, detached: float | None = None, root: int = 60, key: str | None = None, seed: int | None = None, device: subsequence.midi_utils.DeviceId = None, mirrors: Iterable[subsequence.pattern.MirrorSpec] | None = None) subsequence.progressions.Progression[source]¶
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 nocomposition.harmony()call and, with an explicit chord list or akey=, 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, or
between(low, high, step=...). Seep.progression().beats (bars /) – 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.
- clear_tweak(name: str, *param_names: str) None[source]¶
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.
- clock_output(enabled: bool = True) None[source]¶
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 withclock_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
- current_chord() Any | None[source]¶
The chord sounding at the playhead, or
Nonewithout harmony.Reads the harmony window at the current pulse, so it stays accurate under variable harmonic rhythm and clock lookahead (the engine’s
current_chordflips 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 theChordvoicing protocol either way.
- display(enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) None[source]¶
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 (default
1.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.
- energy(energies: Dict[str, float | Tuple[float, float]]) None[source]¶
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 readp.energy(0.5 when nothing is configured) and gate themselves, or declaremin_energy=onpattern()for automatic muting.The dict overrides any energy payload carried by bound
Sectionvalues — 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})
- form(sections: subsequence.forms.Form | List[Any] | Iterator[Tuple[str, int]] | Dict[str, Tuple[int, List[Tuple[str, int]] | None]], loop: bool = False, start: str | None = None, at_end: str = 'stop', key: str | None = None, scale: str | None = None) None[source]¶
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()andform_next()work on them (the jump lands on the next occurrence of the name, wrapping).- Parameters:
sections – The form definition (Form, List, Dict, or Generator).
loop – Sugar for
at_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 with
key.
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")
- form_freeze(sections: int | None = None) subsequence.forms.Form[source]¶
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 withcomposition.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 requires
sections=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
- form_jump(section_name: str) None[source]¶
Jump the form to a named section immediately.
Delegates to
subsequence.form_state.FormState.jump_to(). Only works when the composition uses graph-mode form (a dict passed toform()).The musical effect is heard at the next pattern rebuild cycle — already- queued MIDI notes are unaffected. This natural delay means
form_jumpis effective without needing explicit quantization.- Parameters:
section_name – The section to jump to.
- Raises:
ValueError – If no form is configured, or the form is not in graph mode, or section_name is unknown.
Example:
composition.hotkey("c", lambda: composition.form_jump("chorus"))
- form_next(section_name: str) None[source]¶
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 callingform_nextagain before the boundary.Delegates to
subsequence.form_state.FormState.queue_next(). Only works when the composition uses graph-mode form (a dict passed toform()).- Parameters:
section_name – The section to queue.
- Raises:
ValueError – If no form is configured, or the form is not in graph mode, or section_name is unknown.
Example:
composition.hotkey("c", lambda: composition.form_next("chorus"))
- freeze(bars: int, end: Any | None = None, pins: Dict[int, Any] | None = None, avoid: Sequence[Any] | None = None, cadence: str | None = None) Progression[source]¶
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
Progressionthat can be bound to a form section withsection_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 — sopins={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 – If
harmony()has 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)
- get_tweaks(name: str) Dict[str, Any][source]¶
Return a copy of the current tweaks for a running pattern.
- Parameters:
name – The function name of the pattern.
- harmony(style: str | subsequence.chord_graphs.ChordGraph | None = 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: Any | None = None) None[source]¶
Configure the harmonic logic and chord change intervals.
Two sources, combinable: a bound progression (
progression=— aProgressionvalue, 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.
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]))
- hotkey(key: str, action: Callable[[], None], quantize: int = 0, label: str | None = None) None[source]¶
Register a single-key shortcut that fires during playback.
The listener must be enabled first with
hotkeys().Most actions — form jumps,
composition.datawrites, andtweak()calls — should usequantize=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=Nfor actions where you want an explicit bar-boundary guarantee, such asmute()/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 – If
keyis 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()
- hotkeys(enabled: bool = True) None[source]¶
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()
- layer(*builder_fns: Callable, channel: int, beats: float | None = None, bars: float | None = None, steps: float | None = None, step_duration: float | None = None, drum_note_map: Dict[str, int] | None = None, cc_name_map: Dict[str, int] | None = None, nrpn_name_map: Dict[str, int] | None = None, reschedule_lookahead: float = 1, voice_leading: bool = False, device: subsequence.midi_utils.DeviceId = None, mirrors: Iterable[subsequence.pattern.MirrorSpec] | None = None) None[source]¶
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 ofbeats,bars,steps, andstep_duration.- Parameters:
builder_fns – One or more pattern builder functions.
channel – MIDI channel (1-16, or 0-15 with
zero_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. Requires
step_duration=.step_duration – Duration of one step in beats. Requires
steps=.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.
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.
- link(quantum: float = 4.0) Composition[source]¶
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
linkoptional 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.
- live(port: int = 5555) None[source]¶
Enable the live coding eval server.
This allows you to connect to a running composition using the
subsequence.live_clientREPL 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).
- live_info() Dict[str, Any][source]¶
Return a dictionary containing the current state of the composition.
Includes BPM, key, current bar, active section, current chord, running patterns, and custom data.
- load_patterns(source: str, source_label: str = '<string>') None[source]¶
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 in
SyntaxErrorand__file__- style traceback lines). Default"<string>".
- lock(name: str) None[source]¶
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 untilunlock().- Parameters:
name – The stream name — usually a pattern name.
- midi_input(device: str, clock_follow: bool = False, name: str | None = None) None[source]¶
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 – The name of the MIDI input port.
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 with
cc_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")
- midi_output(device: str, name: str | None = None, latency_ms: float = 0.0) int[source]¶
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 tomidi_output()adds the next device (1, 2, …).- Parameters:
device – The exact name of the MIDI output port, as reported by
mido.get_output_names(). Matching is strict — partial names and substrings are rejected. SeeComposition.__init__for the lookup snippet and a note on ALSA name stability on Linux.name – Optional alias for use with
pattern(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:
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)
- mirror(name: str, device: int, channel: int, drum_note_map: Dict[str, int] | None = None) None[source]¶
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 differentdrum_note_mapre-points it in place.- Parameters:
name – Function name of the pattern to mirror.
device – Output device index (the integer returned from
midi_output(); 0 = primary device).channel – MIDI channel using this composition’s numbering convention (1-16 by default; 0-15 if
zero_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.
- mute(name: str) None[source]¶
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.
- note_input(channel: int | None = None, release_ms: float = 30.0, latch: bool = False, input_device: subsequence.midi_utils.DeviceId = None) None[source]¶
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 top.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, sop.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 as
pattern()(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 when
latchis 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="up") # rests when silent
- on_event(event_name: str, callback: Callable[Ellipsis, Any]) None[source]¶
Register a callback for a sequencer event (e.g., “bar”, “start”, “stop”).
- on_section(callback: Callable[Ellipsis, Any]) None[source]¶
Register a callback fired on every section change.
The callback receives the new
SectionInfo(orNonewhen 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'}"))
- osc(receive_port: int = 9000, send_port: int = 9001, send_host: str = '127.0.0.1', receive_host: str = '0.0.0.0') None[source]¶
Enable bi-directional Open Sound Control (OSC).
Subsequence will listen for commands (like
/bpmor/mute) and broadcast its internal state (like/chordor/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 with
receive_host="127.0.0.1".
- osc_map(address: str, handler: Callable) None[source]¶
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)
- pattern(channel: int, beats: float | None = None, bars: float | None = None, steps: float | None = None, step_duration: float | None = None, drum_note_map: Dict[str, int] | None = None, cc_name_map: Dict[str, int] | None = None, nrpn_name_map: Dict[str, int] | None = None, reschedule_lookahead: float = 1, voice_leading: bool = False, device: subsequence.midi_utils.DeviceId = None, mirrors: Iterable[subsequence.pattern.MirrorSpec] | None = None, min_energy: float | None = None) Callable[source]¶
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). Set
zero_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. Requires
step_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 in
p.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 in
p.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.
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)
- phrase_part(*, channel: int, part: str | None = None, root: int = 60, bars: float | None = None, beats: float | None = None, velocity: int | Tuple[int, int] | None = None, fit: float | None = None, device: subsequence.midi_utils.DeviceId = None, mirrors: Iterable[subsequence.pattern.MirrorSpec] | None = None) None[source]¶
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, viap.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.
beats (bars /) – 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)
- pin_chord(bar: int, chord: Any | None) None[source]¶
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
Noneto 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
- play() None[source]¶
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.
- render(bars: int | None = None, filename: str = 'render.mid', max_minutes: float | None = 60.0) None[source]¶
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, or
Nonefor 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 (default
60.0). PassNoneto disable the time cap — you must then provide an explicit bars value.
- Raises:
ValueError – If both bars and max_minutes are
None, 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")
- request_cadence(cadence: str = 'strong', bar: int | None = None) None[source]¶
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
- reroll(name: str) None[source]¶
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 ...
- schedule(fn: Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) None[source]¶
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 ensures
composition.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 after
play()has started — scheduled tasks register at startup, so a late registration would be silently ignored otherwise.
- section_cadence(section_name: str, cadence: str | None = 'strong') None[source]¶
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. PassNoneto 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
- section_chords(section_name: str, progression: Any) None[source]¶
Bind a
Progressionto 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
Progressionvalue (fromfreeze(), theprogression()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 aSection(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 fromfreeze()— 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 setat_end/loop intent) if you need the whole section in its key.- Parameters:
section_name – Name of the section as defined in
form().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 at
play()/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
- section_motifs(section_name: str, value: Any, part: str | None = None) None[source]¶
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-callphrase_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 in
form().value – A
MotiforPhrase(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")
- seed_for(name: str) int | None[source]¶
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. Reflectsreroll()nonces. Returns None when the composition is unseeded.Example
hook_seed = composition.seed_for("hook")
- set_bpm(bpm: float) None[source]¶
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.
- target_bpm(bpm: float, bars: int, shape: str = 'linear') None[source]¶
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.
- transition(before: str, fill: Any | None = None, channel: int | None = None, beat: float = 0.0, mute: List[str] | None = None, beats: float | None = None, drum_note_map: Dict[str, int] | None = None, device: subsequence.midi_utils.DeviceId = None) None[source]¶
Declare boundary material — the automatic fill or mute, one line.
beforenames 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)
- trigger(fn: Callable, channel: int, beats: float | None = None, bars: float | None = None, steps: float | None = None, step_duration: float | None = None, quantize: float = 0, drum_note_map: Dict[str, int] | None = None, cc_name_map: Dict[str, int] | None = None, nrpn_name_map: Dict[str, int] | None = None, chord: bool = False, device: subsequence.midi_utils.DeviceId = None, mirrors: Iterable[subsequence.pattern.MirrorSpec] | None = None) None[source]¶
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.patterndecorated function and can use all PatternBuilder methods:p.note(),p.euclidean(),p.arpeggio(), and so on.See
pattern()for the full description ofbeats,bars,steps, andstep_duration. Default is 1 beat.- Parameters:
fn – The pattern builder function (same signature as
@comp.pattern).channel – MIDI channel (1-16, or 0-15 with
zero_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. Requires
step_duration=.step_duration – Duration of one step in beats. Requires
steps=.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 – If
True, 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 )
- tuning(source: str | os.PathLike | None = None, *, cents: List[float] | None = None, ratios: List[float] | None = None, equal: int | None = None, bend_range: float = 2.0, channels: List[int] | None = None, reference_note: int = 60, exclude_drums: bool = True) None[source]¶
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
channelspool. Notes are spread across those MIDI channels so each can carry an independent pitch bend. The synth must be configured to matchbend_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 a
drum_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])
- tweak(name: str, **kwargs: Any) None[source]¶
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])
- unmirror(name: str, device: int, channel: int) None[source]¶
Remove a single mirror destination from a running pattern.
Matches on
(device, channel)only — any attacheddrum_note_mapis ignored. Idempotent: silently does nothing if the destination is not currently mirrored. The change applies on the next cycle rebuild.
- unregister(name: str) None[source]¶
Fully remove a running pattern from rotation.
Unlike
mute()(which keeps the pattern alive but silent),unregister()tears the pattern down entirely. It setspattern._removed = Trueso the sequencer’s reschedule loop skips re-adding it on the next pulse; sendsnote_offfor 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_patternsso it no longer appears inlive_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_notespass.Idempotent: silently logs a
debugand 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.
- watch(path: str | pathlib.Path, poll_interval: float = 0.25) None[source]¶
Watch a Python file and reload it into the composition on every save.
The watched file is exec’d into a namespace with
compositionandsubsequenceavailable.@composition.patterndecorators 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
SyntaxErroror 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 between
mtimepolls (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()
- web_ui(http_host: str = '127.0.0.1', ws_host: str = '127.0.0.1') None[source]¶
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.
- property form_state: subsequence.form_state.FormState | None[source]¶
The active
subsequence.form_state.FormState, orNoneifform()has not been called.
- property harmonic_state: subsequence.harmonic_state.HarmonicState | None[source]¶
The active
HarmonicState, orNoneifharmony()has not been called.
- property is_clock_following: bool[source]¶
True if either the primary or any additional device is following external clock.
- property seed: int | None[source]¶
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.)
- property sequencer: subsequence.sequencer.Sequencer[source]¶
The underlying
Sequencerinstance.
- class subsequence.composition.HarmonyView(horizon: _HarmonyHorizon, origin_beat: float)[source]¶
Read-only harmony context for one pattern cycle (
p.harmony).Anchored at the cycle’s start beat, so all beat arguments are cycle-relative —
chord_at(0)is the chord at the cycle’s first beat (what the two-parameterchordconvention injects),chord_at(3.5)the chord sounding under beat 3.5 of this cycle.Under bound/frozen progressions the future is data and any beat answers; in live graph mode the window is the current chord plus one pre-committed step, and
next_chordis planned and revocable.Anchor the view at a cycle-start beat.
- next_chord_at(beat: float) Any | None[source]¶
The chord following the one sounding at beat of THIS cycle, when known.
- class subsequence.composition.HotkeyBinding[source]¶
A registered keyboard shortcut and its associated action.
- class subsequence.composition.ScheduleContext[source]¶
Context object passed to
composition.schedule()callbacks whose signature declares a first parameter (conventionally namedp).
- async subsequence.composition.run_until_stopped(sequencer: subsequence.sequencer.Sequencer) None[source]¶
Run the sequencer until a stop signal is received.
- async subsequence.composition.schedule_form(sequencer: subsequence.sequencer.Sequencer, form_state: subsequence.form_state.FormState, reschedule_lookahead: float = 1, on_bar: Callable[[int, bool], None] | None = None) None[source]¶
Schedule the form state to advance each bar.
Emits a
"section"event on the sequencer’s emitter at play start and on every section change (one lookahead-beat early, like every form decision), carrying the newSectionInfo(Nonewhen the form finishes).on_baris the boundary hook — called once per bar with(boundary_pulse, section_changed)after the form advances; the transition machinery rides it.
- async subsequence.composition.schedule_harmonic_clock(sequencer: subsequence.sequencer.Sequencer, get_harmonic_state: Callable[[], subsequence.harmonic_state.HarmonicState | None], horizon: _HarmonyHorizon, bar_beats: float, cycle_beats: int = 4, get_bound_progression: Callable[[], Progression | None] | None = None, get_section_progression: Callable[[], Tuple[str, int, int, Progression | None] | None] | None = None, get_pinned: Callable[[int], Any | None] | None = None, cadence_requests: Dict[int, str] | None = None, resolve_cadence: Callable[[str], List[subsequence.chords.Chord]] | None = None, get_section_cadence: Callable[[str], str | None] | None = None, reschedule_lookahead: float = 1) None[source]¶
Schedule the harmonic clock — a span walker over the bound harmony sources.
Generalises the old fixed-cycle clock: chords last as long as their spans say, the clock fires at
min(next span boundary, next bar boundary)(so section bookkeeping stays bar-aligned under variable harmonic rhythm), and every realised span is published to horizon (the harmony window patterns read throughp.harmony).Priority chain per chord boundary: section progression > composition-bound progression > live ``step()``. A bound progression loops on exhaustion when no live engine is configured (or when it contains a
PitchSet); with a live engine, exhaustion falls through to live stepping — the frozen-replay bridge. In live mode the engine pre-commits one step so the window always holds[current, next].get_harmonic_state,get_bound_progression, andget_pinnedare evaluated on every tick so mid-playback calls toharmony(), re-binds, and new pins take effect immediately.get_section_progressionreturns(name, index, bars, Progression|None)for the current section (indexincrements on every entry, so verse→verse re-entry resets correctly) orNonewhen no form is active.cadence_requestsis the request-hook seam: a mutable{bar: name}dict (shared withComposition.request_cadence) the live walk steers toward — at the first boundary with a pending request, the remaining changes up to its bar are planned as a constrained walk pinned to the cadence formula (resolved byresolve_cadence) and then committed one boundary at a time.get_section_cadenceturns a section entry into a request arriving at that section’s final bar (live sections only). Requests whose bar passes unserved expire with a warning.The clock fires
reschedule_lookaheadbeats before each boundary — raised by the caller to the maximum pattern lookahead, so the window always covers a pattern’s next cycle before it rebuilds.
- async subsequence.composition.schedule_patterns(sequencer: subsequence.sequencer.Sequencer, patterns: Iterable[subsequence.pattern.Pattern], start_pulse: int = 0) None[source]¶
Schedule a collection of repeating patterns from a shared start pulse.
- async subsequence.composition.schedule_task(sequencer: subsequence.sequencer.Sequencer, fn: Callable, cycle_beats: int, reschedule_lookahead: int = 1, defer: bool = False) None[source]¶
Schedule a non-blocking repeating task on the sequencer’s beat clock.
If
fndeclares a first parameter namedp, it is called with aScheduleContexton every invocation (same behaviour ascomposition.schedule()).When defer is True the backshift fire at pulse 0 is skipped; the first call happens one full cycle_beats later. Direct API users who need the equivalent of
initial=Truecan simplyawait fn()themselves before calling this function.