subsequence

Subsequence - an algorithmic composition framework for Python.

Subsequence gives you a palette of mathematical building blocks - Euclidean rhythms, cellular automata, L-systems, Markov chains, cognitive melody generation - and a stateful engine that lets them interact and evolve over time. Unlike tools that loop a fixed pattern forever, Subsequence rebuilds every pattern fresh before each cycle with full context, so algorithms feed into each other and compositions emerge that no single technique could produce alone. It generates pure MIDI (no audio engine) to control hardware synths, modular systems, drum machines, or software VSTs/DAWs.

What makes it different:

  • A rich algorithmic palette. Euclidean and Bresenham rhythm generators, cellular automata (1D and 2D), L-system string rewriting, Markov chains, cognitive melody via the Narmour model, probability- weighted ghost notes, position-aware thinning, drones and continuous notes, Perlin and pink noise, logistic chaos maps - plus groove templates, velocity shaping, and pitch-bend automation to shape how they sound.
  • Stateful patterns that evolve. Each pattern is a Python function rebuilt fresh every cycle with full context - current chord, section, cycle count, shared data from other patterns. A Euclidean rhythm can thin itself as tension builds, a cellular automaton can seed from the harmony, and a Markov chain can shift behaviour between sections.
  • Optional chord graph. Define weighted chord and key transitions via probability graphs, with gravity and automatic voice leading. Eleven built-in palettes and frozen progressions to lock some sections while others evolve freely. Layer on cognitive harmony for Narmour-based melodic inertia.
  • Sub-microsecond clock. Hybrid sleep+spin timing achieves typical pulse jitter of < 5 us on Linux, with zero long-term drift.
  • Turn anything into music. composition.schedule() runs any Python function on a beat cycle - APIs, sensors, files. Anything Python can reach becomes a musical parameter.
  • Pure MIDI, zero sound engine. No audio synthesis, no heavyweight dependencies. Route to hardware synths, drum machines, Eurorack, or software instruments.

Composition tools:

  • Rhythm and feel. Euclidean and Bresenham generators, multi-voice weighted Bresenham distribution (bresenham_poly()), ghost note layers (ghost_fill()), position-aware note removal (thin() - the musical inverse of ghost_fill), evolving cellular-automaton rhythms (cellular_1d(), cellular_2d()), smooth Perlin noise (perlin_1d(), perlin_2d(), perlin_1d_sequence(), perlin_2d_grid()), deterministic chaos sequences (logistic_map()), pink 1/f noise (pink_noise()), L-system string rewriting (p.lsystem()), Markov-chain generation (p.markov()), aperiodic binary rhythms (p.thue_morse()), golden-ratio beat placement (p.fibonacci()), Gray-Scott reaction-diffusion patterns (p.reaction_diffusion()), Lorenz strange-attractor generation (p.lorenz()), exhaustive pitch-subsequence melodies (p.de_bruijn()), step-wise melodies with guaranteed pitch diversity (p.self_avoiding_walk()), drones and explicit note on/off events (p.drone(), p.drone_off(), p.silence()), groove templates (Groove.swing(), Groove.from_agr()), swing via p.swing() (a shortcut for Groove.swing()), randomize, velocity shaping and ramps (p.velocity_ramp()), dropout, per-step probability, and polyrhythms via independent pattern lengths.
  • Melody generation. p.melody() with MelodicState applies the Narmour Implication-Realization model to single-note lines: continuation after small steps, reversal after large leaps, chord-tone weighting, range gravity, and pitch-diversity penalty. History persists across bar rebuilds for natural phrase continuity.
  • Expression. CC messages/ramps, pitch bend, note-correlated bend/portamento/slide, program changes, SysEx, and OSC output - all from within patterns.
  • Form and structure. Musical form as a weighted graph, ordered list, or generator. Patterns read p.section to adapt. Conductor signals (LFOs, ramps) shape intensity over time.
  • Mini-notation. p.seq("x x [x x] x", pitch="kick") - concise string syntax for rhythms, subdivisions, and per-step probability.
  • Scales and quantization. p.quantize() snaps notes to any scale. scale_notes() generates a list of MIDI note numbers from a key, mode, and range or note count - useful for arpeggios, Markov chains, and melodic walks. Built-in western and non-western modes, plus register_scale() for your own.
  • Microtonal tuning. composition.tuning() applies a tuning system globally; p.apply_tuning() overrides per-pattern. Supports Scala .scl files, explicit cent lists, frequency ratios, and N-TET equal temperaments. Polyphonic parts use explicit channel rotation so simultaneous notes can carry independent pitch bends without MPE. Compatible with any standard MIDI synthesiser.
  • Randomness tools. Weighted choice, no-repeat shuffle, random walk, probability gates. Deterministic seeding (seed=42) makes every decision repeatable.
  • Pattern transforms. Legato, staccato, reverse, double/half-time, shift, transpose, invert, randomize, and conditional p.every().

Integration:

  • MIDI clock. Master (clock_output()) or follower (clock_follow=True). When multiple inputs are connected, only one may be designated as the master clock source; messages from other inputs are filtered to prevent sync interference. Sync to a DAW or drive hardware.
  • Hardware control. CC input mapping from knobs/faders to composition.data; patterns read and write the same dict via p.data for both external data access and cross-pattern communication. OSC for bidirectional communication with mixers, lighting, visuals.
  • Live coding. Hot-swap patterns, change tempo, mute/unmute, and tweak parameters during playback via a built-in TCP eval server.
  • Hotkeys. Single keystrokes to jump sections, toggle mutes, or fire any action - with optional bar-boundary quantization.
  • Real-time pattern triggering. composition.trigger() generates one-shot patterns in response to sensors, OSC, or any event.
  • Terminal display. Live status line (BPM, bar, section, chord). Add grid=True for an ASCII pattern grid showing velocity and sustain - makes legato and staccato visually distinct at a glance. Add grid_scale=2 to zoom in horizontally, revealing swing and groove micro-timing.
  • Web UI Dashboard (Beta). Enable with composition.web_ui() to broadcast live composition metadata and visualize piano-roll pattern grids in a reactive HTTP/WebSocket browser dashboard.
  • Ableton Link. Industry-standard wireless tempo/phase sync (comp.link(); requires pip install subsequence[link]). Any Link-enabled app on the same LAN — Ableton Live, iOS synths, other Subsequence instances — stays in time automatically.
  • Recording. Record to standard MIDI file. Render to file without waiting for real-time playback.
Minimal example:
import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums

comp = subsequence.Composition(bpm=120)

@comp.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums (p):
    (p.hit_steps("kick_1",        [0, 4, 8, 12], velocity=100)
      .hit_steps("snare_1",       [4, 12],        velocity=90)
      .hit_steps("hi_hat_closed", range(16),      velocity=70))

comp.play()

Community and Feedback:

Package-level exports: Composition, Groove, MelodicState, Tuning, register_scale, scale_notes, bank_select.

  1"""
  2Subsequence - an algorithmic composition framework for Python.
  3
  4Subsequence gives you a palette of mathematical building blocks -
  5Euclidean rhythms, cellular automata, L-systems, Markov chains,
  6cognitive melody generation - and a stateful engine that lets them
  7interact and evolve over time. Unlike tools that loop a fixed pattern
  8forever, Subsequence rebuilds every pattern fresh before each cycle
  9with full context, so algorithms feed into each other and compositions
 10emerge that no single technique could produce alone. It generates pure
 11MIDI (no audio engine) to control hardware synths, modular systems,
 12drum machines, or software VSTs/DAWs.
 13
 14What makes it different:
 15
 16- **A rich algorithmic palette.** Euclidean and Bresenham rhythm
 17  generators, cellular automata (1D and 2D), L-system string rewriting,
 18  Markov chains, cognitive melody via the Narmour model, probability-
 19  weighted ghost notes, position-aware thinning, drones and continuous
 20  notes, Perlin and pink noise, logistic chaos maps - plus groove
 21  templates, velocity shaping, and pitch-bend automation to shape
 22  how they sound.
 23- **Stateful patterns that evolve.** Each pattern is a Python function
 24  rebuilt fresh every cycle with full context - current chord, section,
 25  cycle count, shared data from other patterns. A Euclidean rhythm can
 26  thin itself as tension builds, a cellular automaton can seed from the
 27  harmony, and a Markov chain can shift behaviour between sections.
 28- **Optional chord graph.** Define weighted chord and key transitions
 29  via probability graphs, with gravity and automatic voice leading.
 30  Eleven built-in palettes and frozen progressions to lock some sections
 31  while others evolve freely. Layer on cognitive harmony for
 32  Narmour-based melodic inertia.
 33- **Sub-microsecond clock.** Hybrid sleep+spin timing achieves typical
 34  pulse jitter of < 5 us on Linux, with zero long-term drift.
 35- **Turn anything into music.** ``composition.schedule()`` runs any
 36  Python function on a beat cycle - APIs, sensors, files. Anything
 37  Python can reach becomes a musical parameter.
 38- **Pure MIDI, zero sound engine.** No audio synthesis, no heavyweight
 39  dependencies. Route to hardware synths, drum machines, Eurorack, or
 40  software instruments.
 41
 42Composition tools:
 43
 44- **Rhythm and feel.** Euclidean and Bresenham generators, multi-voice
 45  weighted Bresenham distribution (``bresenham_poly()``), ghost note
 46  layers (``ghost_fill()``), position-aware note removal (``thin()`` -
 47  the musical inverse of ``ghost_fill``), evolving cellular-automaton
 48  rhythms (``cellular_1d()``, ``cellular_2d()``), smooth Perlin noise (``perlin_1d()``,
 49  ``perlin_2d()``, ``perlin_1d_sequence()``, ``perlin_2d_grid()``),
 50  deterministic chaos sequences (``logistic_map()``), pink 1/f noise
 51  (``pink_noise()``), L-system string rewriting (``p.lsystem()``),
 52  Markov-chain generation (``p.markov()``), aperiodic binary rhythms
 53  (``p.thue_morse()``), golden-ratio beat placement (``p.fibonacci()``),
 54  Gray-Scott reaction-diffusion patterns (``p.reaction_diffusion()``),
 55  Lorenz strange-attractor generation (``p.lorenz()``), exhaustive
 56  pitch-subsequence melodies (``p.de_bruijn()``), step-wise melodies
 57  with guaranteed pitch diversity (``p.self_avoiding_walk()``), drones
 58  and explicit note on/off events (``p.drone()``, ``p.drone_off()``,
 59  ``p.silence()``),
 60  groove templates (``Groove.swing()``, ``Groove.from_agr()``), swing via
 61  ``p.swing()`` (a shortcut for ``Groove.swing()``), randomize,
 62  velocity shaping and ramps (``p.velocity_ramp()``), dropout, per-step
 63  probability, and polyrhythms via independent pattern lengths.
 64- **Melody generation.** ``p.melody()`` with ``MelodicState`` applies
 65  the Narmour Implication-Realization model to single-note lines:
 66  continuation after small steps, reversal after large leaps, chord-tone
 67  weighting, range gravity, and pitch-diversity penalty.  History persists
 68  across bar rebuilds for natural phrase continuity.
 69- **Expression.** CC messages/ramps, pitch bend, note-correlated
 70  bend/portamento/slide, program changes, SysEx, and OSC output - all
 71  from within patterns.
 72- **Form and structure.** Musical form as a weighted graph, ordered list,
 73  or generator. Patterns read ``p.section`` to adapt. Conductor signals
 74  (LFOs, ramps) shape intensity over time.
 75- **Mini-notation.** ``p.seq("x x [x x] x", pitch="kick")`` - concise
 76  string syntax for rhythms, subdivisions, and per-step probability.
 77- **Scales and quantization.** ``p.quantize()`` snaps notes to any
 78  scale. ``scale_notes()`` generates a list of MIDI note numbers from
 79  a key, mode, and range or note count - useful for arpeggios, Markov
 80  chains, and melodic walks. Built-in western and non-western modes,
 81  plus ``register_scale()`` for your own.
 82- **Microtonal tuning.** ``composition.tuning()`` applies a tuning
 83  system globally; ``p.apply_tuning()`` overrides per-pattern.
 84  Supports Scala ``.scl`` files, explicit cent lists, frequency ratios,
 85  and N-TET equal temperaments. Polyphonic parts use explicit channel
 86  rotation so simultaneous notes can carry independent pitch bends
 87  without MPE. Compatible with any standard MIDI synthesiser.
 88- **Randomness tools.** Weighted choice, no-repeat shuffle, random
 89  walk, probability gates. Deterministic seeding (``seed=42``) makes
 90  every decision repeatable.
 91- **Pattern transforms.** Legato, staccato, reverse, double/half-time,
 92  shift, transpose, invert, randomize, and conditional ``p.every()``.
 93
 94Integration:
 95
 96- **MIDI clock.** Master (``clock_output()``) or follower
 97  (``clock_follow=True``). When multiple inputs are connected, only
 98  one may be designated as the master clock source; messages from
 99  other inputs are filtered to prevent sync interference. Sync to a
100  DAW or drive hardware.
101- **Hardware control.** CC input mapping from knobs/faders to
102  ``composition.data``; patterns read and write the same dict via
103  ``p.data`` for both external data access and cross-pattern
104  communication. OSC for bidirectional communication with mixers,
105  lighting, visuals.
106- **Live coding.** Hot-swap patterns, change tempo, mute/unmute, and
107  tweak parameters during playback via a built-in TCP eval server.
108- **Hotkeys.** Single keystrokes to jump sections, toggle mutes, or
109  fire any action - with optional bar-boundary quantization.
110- **Real-time pattern triggering.** ``composition.trigger()`` generates
111  one-shot patterns in response to sensors, OSC, or any event.
112- **Terminal display.** Live status line (BPM, bar, section, chord).
113  Add ``grid=True`` for an ASCII pattern grid showing velocity and
114  sustain - makes legato and staccato visually distinct at a glance.
115  Add ``grid_scale=2`` to zoom in horizontally, revealing swing and
116  groove micro-timing.
117- **Web UI Dashboard (Beta).** Enable with ``composition.web_ui()`` to 
118  broadcast live composition metadata and visualize piano-roll pattern 
119  grids in a reactive HTTP/WebSocket browser dashboard.
120- **Ableton Link.** Industry-standard wireless tempo/phase sync
121  (``comp.link()``; requires ``pip install subsequence[link]``).
122  Any Link-enabled app on the same LAN — Ableton Live, iOS synths,
123  other Subsequence instances — stays in time automatically.
124- **Recording.** Record to standard MIDI file. Render to file without
125  waiting for real-time playback.
126
127Minimal example:
128
129    ```python
130    import subsequence
131    import subsequence.constants.instruments.gm_drums as gm_drums
132
133    comp = subsequence.Composition(bpm=120)
134
135    @comp.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
136    def drums (p):
137        (p.hit_steps("kick_1",        [0, 4, 8, 12], velocity=100)
138          .hit_steps("snare_1",       [4, 12],        velocity=90)
139          .hit_steps("hi_hat_closed", range(16),      velocity=70))
140
141    comp.play()
142    ```
143
144Community and Feedback:
145
146- **Discussions:** Chat and ask questions at https://github.com/simonholliday/subsequence/discussions
147- **Issues:** Report bugs and request features at https://github.com/simonholliday/subsequence/issues
148
149Package-level exports: ``Composition``, ``Groove``, ``MelodicState``, ``Tuning``, ``register_scale``, ``scale_notes``, ``bank_select``.
150"""
151
152import subsequence.composition
153import subsequence.groove
154import subsequence.intervals
155import subsequence.melodic_state
156import subsequence.midi_utils
157import subsequence.tuning
158
159
160Composition = subsequence.composition.Composition
161Groove = subsequence.groove.Groove
162MelodicState = subsequence.melodic_state.MelodicState
163Tuning = subsequence.tuning.Tuning
164register_scale = subsequence.intervals.register_scale
165scale_notes = subsequence.intervals.scale_notes
166bank_select = subsequence.midi_utils.bank_select
class Composition:
 565class Composition:
 566
 567	"""
 568	The top-level controller for a musical piece.
 569	
 570	The `Composition` object manages the global clock (Sequencer), the harmonic
 571	progression (HarmonicState), the song structure (subsequence.form_state.FormState), and all MIDI patterns.
 572	It serves as the main entry point for defining your music.
 573	
 574	Typical workflow:
 575	1. Initialize `Composition` with BPM and Key.
 576	2. Define harmony and form (optional).
 577	3. Register patterns using the `@composition.pattern` decorator.
 578	4. Call `composition.play()` to start the music.
 579	"""
 580
 581	def __init__ (
 582		self,
 583		output_device: typing.Optional[str] = None,
 584		bpm: float = 120,
 585		time_signature: typing.Tuple[int, int] = (4, 4),
 586		key: typing.Optional[str] = None,
 587		seed: typing.Optional[int] = None,
 588		record: bool = False,
 589		record_filename: typing.Optional[str] = None,
 590		zero_indexed_channels: bool = False
 591	) -> None:
 592
 593		"""
 594		Initialize a new composition.
 595
 596		Parameters:
 597			output_device: The name of the MIDI output port to use. If `None`,
 598				Subsequence will attempt to find a device, prompting if necessary.
 599			bpm: Initial tempo in beats per minute (default 120).
 600			key: The root key of the piece (e.g., "C", "F#", "Bb").
 601				Required if you plan to use `harmony()`.
 602			seed: An optional integer for deterministic randomness. When set,
 603				every random decision (chord choices, drum probability, etc.)
 604				will be identical on every run.
 605			record: When True, record all MIDI events to a file.
 606			record_filename: Optional filename for the recording (defaults to timestamp).
 607			zero_indexed_channels: When False (default), MIDI channels use
 608				1-based numbering (1-16) matching instrument labelling.
 609				Channel 10 is drums, the way musicians and hardware panels
 610				show it. When True, channels use 0-based numbering (0-15)
 611				matching the raw MIDI protocol.
 612
 613		Example:
 614			```python
 615			comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
 616			```
 617		"""
 618
 619		self.output_device = output_device
 620		self.bpm = bpm
 621		self.time_signature = time_signature
 622		self.key = key
 623		self._seed: typing.Optional[int] = seed
 624		self._zero_indexed_channels: bool = zero_indexed_channels
 625
 626		self._sequencer = subsequence.sequencer.Sequencer(
 627			output_device_name = output_device,
 628			initial_bpm = bpm,
 629			time_signature = time_signature,
 630			record = record,
 631			record_filename = record_filename
 632		)
 633
 634		self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None
 635		self._harmony_cycle_beats: typing.Optional[int] = None
 636		self._harmony_reschedule_lookahead: float = 1
 637		self._section_progressions: typing.Dict[str, Progression] = {}
 638		self._pending_patterns: typing.List[_PendingPattern] = []
 639		self._pending_scheduled: typing.List[_PendingScheduled] = []
 640		self._form_state: typing.Optional[subsequence.form_state.FormState] = None
 641		self._builder_bar: int = 0
 642		self._display: typing.Optional[subsequence.display.Display] = None
 643		self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None
 644		self._is_live: bool = False
 645		self._running_patterns: typing.Dict[str, typing.Any] = {}
 646		self._input_device: typing.Optional[str] = None
 647		self._input_device_alias: typing.Optional[str] = None
 648		self._clock_follow: bool = False
 649		self._clock_output: bool = False
 650		self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = []
 651		self._cc_forwards: typing.List[typing.Dict[str, typing.Any]] = []
 652		# Additional output devices registered with midi_output() after construction.
 653		# Each entry: (device_name: str, alias: Optional[str])
 654		self._additional_outputs: typing.List[typing.Tuple[str, typing.Optional[str]]] = []
 655		# Additional input devices: (device_name: str, alias: Optional[str], clock_follow: bool)
 656		self._additional_inputs: typing.List[typing.Tuple[str, typing.Optional[str], bool]] = []
 657		# Maps alias/name → output device index (populated in _run after all devices are opened).
 658		self._output_device_names: typing.Dict[str, int] = {}
 659		# Maps alias/name → input device index (populated in _run after all input devices are opened).
 660		self._input_device_names: typing.Dict[str, int] = {}
 661		self.data: typing.Dict[str, typing.Any] = {}
 662		self._osc_server: typing.Optional[subsequence.osc.OscServer] = None
 663		self.conductor = subsequence.conductor.Conductor()
 664		self._web_ui_enabled: bool = False
 665		self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None
 666		self._link_quantum: typing.Optional[float] = None
 667
 668		# Hotkey state — populated by hotkeys() and hotkey().
 669		self._hotkeys_enabled: bool = False
 670		self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {}
 671		self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = []
 672		self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None
 673
 674		# Tuning state — populated by tuning().
 675		self._tuning: typing.Optional[typing.Any] = None       # subsequence.tuning.Tuning
 676		self._tuning_bend_range: float = 2.0
 677		self._tuning_channels: typing.Optional[typing.List[int]] = None
 678		self._tuning_reference_note: int = 60
 679		self._tuning_exclude_drums: bool = True
 680
 681	def _resolve_device_id (self, device: subsequence.midi_utils.DeviceId) -> int:
 682		"""Resolve an output device id (None/int/str) to an integer index.
 683
 684		``None`` → 0 (primary device).  ``int`` → returned as-is.
 685		``str`` → looked up in ``_output_device_names``; logs a warning and
 686		returns 0 if the name is unknown (called after all devices are opened
 687		in ``_run()``).
 688		"""
 689		if device is None:
 690			return 0
 691		if isinstance(device, int):
 692			return device
 693		idx = self._output_device_names.get(device)
 694		if idx is None:
 695			logger.warning(
 696				f"Unknown output device name '{device}' — routing to device 0. "
 697				f"Available names: {list(self._output_device_names.keys())}"
 698			)
 699			return 0
 700		return idx
 701
 702	def _resolve_input_device_id (self, device: subsequence.midi_utils.DeviceId) -> typing.Optional[int]:
 703		"""Resolve an input device id (None/int/str) to an integer index.
 704
 705		``None`` → ``None`` (matches any input device — existing behaviour).
 706		``int`` → returned as-is.  ``str`` → looked up in ``_input_device_names``;
 707		logs a warning and returns ``None`` if the name is unknown.
 708		Called after all input devices are opened in ``_run()``.
 709		"""
 710		if device is None:
 711			return None
 712		if isinstance(device, int):
 713			return device
 714		idx = self._input_device_names.get(device)
 715		if idx is None:
 716			logger.warning(
 717				f"Unknown input device name '{device}' — mapping will be ignored. "
 718				f"Available names: {list(self._input_device_names.keys())}"
 719			)
 720			return None
 721		return idx
 722
 723	def _resolve_pending_devices (self) -> None:
 724		"""Resolve name-based device ids on pending patterns now that all output devices are open."""
 725		for pending in self._pending_patterns:
 726			if isinstance(pending.raw_device, str):
 727				pending.device = self._resolve_device_id(pending.raw_device)
 728
 729	def _resolve_channel (self, channel: int) -> int:
 730
 731		"""
 732		Convert a user-supplied MIDI channel to the 0-indexed value used internally.
 733
 734		When ``zero_indexed_channels`` is False (default), the channel is
 735		validated as 1-16 and decremented by one. When True (0-indexed), the
 736		channel is validated as 0-15 and returned unchanged.
 737		"""
 738
 739		if self._zero_indexed_channels:
 740			if not 0 <= channel <= 15:
 741				raise ValueError(f"MIDI channel must be 0-15 (zero_indexed_channels=True), got {channel}")
 742			return channel
 743		else:
 744			if not 1 <= channel <= 16:
 745				raise ValueError(f"MIDI channel must be 1-16, got {channel}")
 746			return channel - 1
 747
 748	@property
 749	def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]:
 750		"""The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called."""
 751		return self._harmonic_state
 752
 753	@property
 754	def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]:
 755		"""The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called."""
 756		return self._form_state
 757
 758	@property
 759	def sequencer (self) -> subsequence.sequencer.Sequencer:
 760		"""The underlying ``Sequencer`` instance."""
 761		return self._sequencer
 762
 763	@property
 764	def running_patterns (self) -> typing.Dict[str, typing.Any]:
 765		"""The currently active patterns, keyed by name."""
 766		return self._running_patterns
 767
 768	@property
 769	def builder_bar (self) -> int:
 770		"""Current bar index used by pattern builders."""
 771		return self._builder_bar
 772
 773	def _require_harmonic_state (self) -> subsequence.harmonic_state.HarmonicState:
 774		"""Return the active HarmonicState, raising ValueError if none is configured."""
 775		if self._harmonic_state is None:
 776			raise ValueError(
 777				"harmony() must be called before this action — "
 778				"no harmonic state has been configured."
 779			)
 780		return self._harmonic_state
 781
 782	def harmony (
 783		self,
 784		style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major",
 785		cycle_beats: int = 4,
 786		dominant_7th: bool = True,
 787		gravity: float = 1.0,
 788		nir_strength: float = 0.5,
 789		minor_turnaround_weight: float = 0.0,
 790		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
 791		reschedule_lookahead: float = 1
 792	) -> None:
 793
 794		"""
 795		Configure the harmonic logic and chord change intervals.
 796
 797		Subsequence uses a weighted transition graph to choose the next chord.
 798		You can influence these choices using 'gravity' (favoring the tonic) and
 799		'NIR strength' (melodic inertia based on Narmour's model).
 800
 801		Parameters:
 802			style: The harmonic style to use. Built-in: "functional_major"
 803				(alias "diatonic_major"), "turnaround", "aeolian_minor",
 804				"phrygian_minor", "lydian_major", "dorian_minor",
 805				"chromatic_mediant", "suspended", "mixolydian", "whole_tone",
 806				"diminished". See README for full descriptions.
 807			cycle_beats: How many beats each chord lasts (default 4).
 808			dominant_7th: Whether to include V7 chords (default True).
 809			gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
 810			nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement
 811				expectations.
 812			minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
 813			root_diversity: Root-repetition damping (0.0 to 1.0). Each recent
 814				chord sharing a candidate's root reduces the weight to 40% at
 815				the default (0.4). Set to 1.0 to disable.
 816			reschedule_lookahead: How many beats in advance to calculate the
 817				next chord.
 818
 819		Example:
 820			```python
 821			# A moody minor progression that changes every 8 beats
 822			comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
 823			```
 824		"""
 825
 826		if self.key is None:
 827			raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor")
 828
 829		preserved_history: typing.List[subsequence.chords.Chord] = []
 830		preserved_current: typing.Optional[subsequence.chords.Chord] = None
 831
 832		if self._harmonic_state is not None:
 833			preserved_history = self._harmonic_state.history.copy()
 834			preserved_current = self._harmonic_state.current_chord
 835
 836		self._harmonic_state = subsequence.harmonic_state.HarmonicState(
 837			key_name = self.key,
 838			graph_style = style,
 839			include_dominant_7th = dominant_7th,
 840			key_gravity_blend = gravity,
 841			nir_strength = nir_strength,
 842			minor_turnaround_weight = minor_turnaround_weight,
 843			root_diversity = root_diversity
 844		)
 845
 846		if preserved_history:
 847			self._harmonic_state.history = preserved_history
 848		if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current):
 849			self._harmonic_state.current_chord = preserved_current
 850
 851		self._harmony_cycle_beats = cycle_beats
 852		self._harmony_reschedule_lookahead = reschedule_lookahead
 853
 854	def freeze (self, bars: int) -> "Progression":
 855
 856		"""Capture a chord progression from the live harmony engine.
 857
 858		Runs the harmony engine forward by *bars* chord changes, records each
 859		chord, and returns it as a :class:`Progression` that can be bound to a
 860		form section with :meth:`section_chords`.
 861
 862		The engine state **advances** — successive ``freeze()`` calls produce a
 863		continuing compositional journey so section progressions feel like parts
 864		of a whole rather than isolated islands.
 865
 866		Parameters:
 867			bars: Number of chords to capture (one per harmony cycle).
 868
 869		Returns:
 870			A :class:`Progression` with the captured chords and trailing
 871			history for NIR continuity.
 872
 873		Raises:
 874			ValueError: If :meth:`harmony` has not been called first.
 875
 876		Example::
 877
 878			composition.harmony(style="functional_major", cycle_beats=4)
 879			verse  = composition.freeze(8)   # 8 chords, engine advances
 880			chorus = composition.freeze(4)   # next 4 chords, continuing on
 881			composition.section_chords("verse",  verse)
 882			composition.section_chords("chorus", chorus)
 883		"""
 884
 885		hs = self._require_harmonic_state()
 886
 887		if bars < 1:
 888			raise ValueError("bars must be at least 1")
 889		collected: typing.List[subsequence.chords.Chord] = [hs.current_chord]
 890
 891		for _ in range(bars - 1):
 892			hs.step()
 893			collected.append(hs.current_chord)
 894
 895		# Advance past the last captured chord so the next freeze() call or
 896		# live playback does not duplicate it.
 897		hs.step()
 898
 899		return Progression(
 900			chords = tuple(collected),
 901			trailing_history = tuple(hs.history),
 902		)
 903
 904	def section_chords (self, section_name: str, progression: "Progression") -> None:
 905
 906		"""Bind a frozen :class:`Progression` to a named form section.
 907
 908		Every time *section_name* plays, the harmonic clock replays the
 909		progression's chords in order instead of calling the live engine.
 910		Sections without a bound progression continue generating live chords.
 911
 912		Parameters:
 913			section_name: Name of the section as defined in :meth:`form`.
 914			progression: The :class:`Progression` returned by :meth:`freeze`.
 915
 916		Raises:
 917			ValueError: If the form has been configured and *section_name* is
 918				not a known section name.
 919
 920		Example::
 921
 922			composition.section_chords("verse",  verse_progression)
 923			composition.section_chords("chorus", chorus_progression)
 924			# "bridge" is not bound — it generates live chords
 925		"""
 926
 927		if (
 928			self._form_state is not None
 929			and self._form_state._section_bars is not None
 930			and section_name not in self._form_state._section_bars
 931		):
 932			known = ", ".join(sorted(self._form_state._section_bars))
 933			raise ValueError(
 934				f"Section '{section_name}' not found in form. "
 935				f"Known sections: {known}"
 936			)
 937
 938		self._section_progressions[section_name] = progression
 939
 940	def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None:
 941
 942		"""
 943		Register a callback for a sequencer event (e.g., "bar", "start", "stop").
 944		"""
 945
 946		self._sequencer.on_event(event_name, callback)
 947
 948
 949	# -----------------------------------------------------------------------
 950	# Hotkey API
 951	# -----------------------------------------------------------------------
 952
 953	def hotkeys (self, enabled: bool = True) -> None:
 954
 955		"""Enable or disable the global hotkey listener.
 956
 957		Must be called **before** :meth:`play` to take effect.  When enabled, a
 958		background thread reads single keystrokes from stdin without requiring
 959		Enter.  The ``?`` key is always reserved and lists all active bindings.
 960
 961		Hotkeys have zero impact on playback when disabled — the listener
 962		thread is never started.
 963
 964		Args:
 965		    enabled: ``True`` (default) to enable hotkeys; ``False`` to disable.
 966
 967		Example::
 968
 969		    composition.hotkeys()
 970		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
 971		    composition.play()
 972		"""
 973
 974		self._hotkeys_enabled = enabled
 975
 976
 977	def hotkey (
 978		self,
 979		key:      str,
 980		action:   typing.Callable[[], None],
 981		quantize: int = 0,
 982		label:    typing.Optional[str] = None,
 983	) -> None:
 984
 985		"""Register a single-key shortcut that fires during playback.
 986
 987		The listener must be enabled first with :meth:`hotkeys`.
 988
 989		Most actions — form jumps, ``composition.data`` writes, and
 990		:meth:`tweak` calls — should use ``quantize=0`` (the default).  Their
 991		musical effect is naturally delayed to the next pattern rebuild cycle,
 992		which provides automatic musical quantization without extra configuration.
 993
 994		Use ``quantize=N`` for actions where you want an explicit bar-boundary
 995		guarantee, such as :meth:`mute` / :meth:`unmute`.
 996
 997		The ``?`` key is reserved and cannot be overridden.
 998
 999		Args:
1000		    key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``).
1001		    action: Zero-argument callable to execute.
1002		    quantize: ``0`` = execute immediately (default).  ``N`` = execute
1003		        on the next global bar number divisible by *N*.
1004		    label: Display name for the ``?`` help listing.  Auto-derived from
1005		        the function name or lambda body if omitted.
1006
1007		Raises:
1008		    ValueError: If ``key`` is the reserved ``?`` character, or if
1009		        ``key`` is not exactly one character.
1010
1011		Example::
1012
1013		    composition.hotkeys()
1014
1015		    # Immediate — musical effect happens at next pattern rebuild
1016		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
1017		    composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
1018
1019		    # Explicit 4-bar phrase boundary
1020		    composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
1021
1022		    # Named function — label is derived automatically
1023		    def drop_to_breakdown ():
1024		        composition.form_jump("breakdown")
1025		        composition.mute("lead")
1026
1027		    composition.hotkey("d", drop_to_breakdown)
1028
1029		    composition.play()
1030		"""
1031
1032		if len(key) != 1:
1033			raise ValueError(f"hotkey key must be a single character, got {key!r}")
1034
1035		if key == _HOTKEY_RESERVED:
1036			raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.")
1037
1038		derived = label if label is not None else _derive_label(action)
1039
1040		self._hotkey_bindings[key] = HotkeyBinding(
1041			key      = key,
1042			action   = action,
1043			quantize = quantize,
1044			label    = derived,
1045		)
1046
1047
1048	def form_jump (self, section_name: str) -> None:
1049
1050		"""Jump the form to a named section immediately.
1051
1052		Delegates to :meth:`subsequence.form_state.FormState.jump_to`.  Only works when the
1053		composition uses graph-mode form (a dict passed to :meth:`form`).
1054
1055		The musical effect is heard at the *next pattern rebuild cycle* — already-
1056		queued MIDI notes are unaffected.  This natural delay means ``form_jump``
1057		is effective without needing explicit quantization.
1058
1059		Args:
1060		    section_name: The section to jump to.
1061
1062		Raises:
1063		    ValueError: If no form is configured, or the form is not in graph
1064		        mode, or *section_name* is unknown.
1065
1066		Example::
1067
1068		    composition.hotkey("c", lambda: composition.form_jump("chorus"))
1069		"""
1070
1071		if self._form_state is None:
1072			raise ValueError("form_jump() requires a form to be configured via composition.form().")
1073
1074		self._form_state.jump_to(section_name)
1075
1076
1077	def form_next (self, section_name: str) -> None:
1078
1079		"""Queue the next section — takes effect when the current section ends.
1080
1081		Unlike :meth:`form_jump`, this does not interrupt the current section.
1082		The queued section replaces the automatically pre-decided next section
1083		and takes effect at the natural section boundary.  The performer can
1084		change their mind by calling ``form_next`` again before the boundary.
1085
1086		Delegates to :meth:`subsequence.form_state.FormState.queue_next`.  Only works when the
1087		composition uses graph-mode form (a dict passed to :meth:`form`).
1088
1089		Args:
1090		    section_name: The section to queue.
1091
1092		Raises:
1093		    ValueError: If no form is configured, or the form is not in graph
1094		        mode, or *section_name* is unknown.
1095
1096		Example::
1097
1098		    composition.hotkey("c", lambda: composition.form_next("chorus"))
1099		"""
1100
1101		if self._form_state is None:
1102			raise ValueError("form_next() requires a form to be configured via composition.form().")
1103
1104		self._form_state.queue_next(section_name)
1105
1106
1107	def _list_hotkeys (self) -> None:
1108
1109		"""Log all active hotkey bindings (triggered by the ``?`` key).
1110
1111		Output appears via the standard logger so it scrolls cleanly above
1112		the :class:`~subsequence.display.Display` status line.
1113		"""
1114
1115		lines = ["Active hotkeys:"]
1116		for key in sorted(self._hotkey_bindings):
1117			b = self._hotkey_bindings[key]
1118			quant_str = "immediate" if b.quantize == 0 else f"quantize={b.quantize}"
1119			lines.append(f"  {key}  \u2192  {b.label}  ({quant_str})")
1120		lines.append(f"  ?  \u2192  list hotkeys")
1121		logger.info("\n".join(lines))
1122
1123
1124	def _process_hotkeys (self, bar: int) -> None:
1125
1126		"""Drain pending keystrokes and execute due actions.
1127
1128		Called on every ``"bar"`` event by the sequencer when hotkeys are
1129		enabled.  Handles both immediate (``quantize=0``) and quantized actions.
1130
1131		Immediate actions are executed directly from the keystroke listener
1132		thread (not here).  This method only processes quantized actions that
1133		were deferred to a bar boundary.
1134
1135		Args:
1136		    bar: The current global bar number from the sequencer.
1137		"""
1138
1139		if self._keystroke_listener is None:
1140			return
1141
1142		# Process newly arrived keys.
1143		for key in self._keystroke_listener.drain():
1144
1145			if key == _HOTKEY_RESERVED:
1146				self._list_hotkeys()
1147				continue
1148
1149			binding = self._hotkey_bindings.get(key)
1150			if binding is None:
1151				continue
1152
1153			if binding.quantize == 0:
1154				# Immediate — execute now (we're on the bar-event callback,
1155				# which is safe for all mutation methods).
1156				try:
1157					binding.action()
1158					logger.info(f"Hotkey '{key}' \u2192 {binding.label}")
1159				except Exception as exc:
1160					logger.warning(f"Hotkey '{key}' action raised: {exc}")
1161			else:
1162				# Defer until the next quantize boundary.
1163				self._pending_hotkey_actions.append(
1164					_PendingHotkeyAction(binding=binding, requested_bar=bar)
1165				)
1166
1167		# Fire any pending actions whose bar boundary has arrived.
1168		still_pending: typing.List[_PendingHotkeyAction] = []
1169
1170		for pending in self._pending_hotkey_actions:
1171			if bar % pending.binding.quantize == 0:
1172				try:
1173					pending.binding.action()
1174					logger.info(
1175						f"Hotkey '{pending.binding.key}' \u2192 {pending.binding.label} "
1176						f"(bar {bar})"
1177					)
1178				except Exception as exc:
1179					logger.warning(
1180						f"Hotkey '{pending.binding.key}' action raised: {exc}"
1181					)
1182			else:
1183				still_pending.append(pending)
1184
1185		self._pending_hotkey_actions = still_pending
1186
1187	def seed (self, value: int) -> None:
1188
1189		"""
1190		Set a random seed for deterministic, repeatable playback.
1191
1192		If a seed is set, Subsequence will produce the exact same sequence 
1193		every time you run the script. This is vital for finishing tracks or 
1194		reproducing a specific 'performance'.
1195
1196		Parameters:
1197			value: An integer seed.
1198
1199		Example:
1200			```python
1201			# Fix the randomness
1202			comp.seed(42)
1203			```
1204		"""
1205
1206		self._seed = value
1207
1208	def tuning (
1209		self,
1210		source: typing.Optional[typing.Union[str, "os.PathLike"]] = None,
1211		*,
1212		cents: typing.Optional[typing.List[float]] = None,
1213		ratios: typing.Optional[typing.List[float]] = None,
1214		equal: typing.Optional[int] = None,
1215		bend_range: float = 2.0,
1216		channels: typing.Optional[typing.List[int]] = None,
1217		reference_note: int = 60,
1218		exclude_drums: bool = True,
1219	) -> None:
1220
1221		"""Set a global microtonal tuning for the composition.
1222
1223		The tuning is applied automatically after each pattern rebuild (before
1224		the pattern is scheduled).  Drum patterns (those registered with a
1225		``drum_note_map``) are excluded by default.
1226
1227		Supply exactly one of the source parameters:
1228
1229		- ``source``: path to a Scala ``.scl`` file.
1230		- ``cents``: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).
1231		- ``ratios``: list of frequency ratios (e.g., ``[9/8, 5/4, 4/3, 3/2, 2]``).
1232		- ``equal``: integer for N-tone equal temperament (e.g., ``equal=19``).
1233
1234		For polyphonic parts, supply a ``channels`` pool.  Notes are spread
1235		across those MIDI channels so each can carry an independent pitch bend.
1236		The synth must be configured to match ``bend_range`` (its pitch-bend range
1237		setting in semitones).
1238
1239		Parameters:
1240			source: Path to a ``.scl`` file.
1241			cents: Cent offsets for scale degrees 1..N.
1242			ratios: Frequency ratios for scale degrees 1..N.
1243			equal: Number of equal divisions of the period.
1244			bend_range: Synth pitch-bend range in semitones (default ±2).
1245			channels: Channel pool for polyphonic rotation.
1246			reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).
1247			exclude_drums: When True (default), skip patterns that have a
1248			    ``drum_note_map`` (they use fixed GM pitches, not tuned ones).
1249
1250		Example:
1251			```python
1252			# Quarter-comma meantone from a Scala file
1253			comp.tuning("meanquar.scl")
1254
1255			# Just intonation from ratios
1256			comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2])
1257
1258			# 19-TET, monophonic
1259			comp.tuning(equal=19, bend_range=2.0)
1260
1261			# 31-TET with channel rotation for polyphony (channels 1-6)
1262			comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])
1263			```
1264		"""
1265		import subsequence.tuning as _tuning_mod
1266
1267		given = sum(x is not None for x in [source, cents, ratios, equal])
1268		if given == 0:
1269			raise ValueError("composition.tuning() requires one of: source, cents, ratios, or equal")
1270		if given > 1:
1271			raise ValueError("composition.tuning() accepts only one source parameter")
1272
1273		if source is not None:
1274			t = _tuning_mod.Tuning.from_scl(source)
1275		elif cents is not None:
1276			t = _tuning_mod.Tuning.from_cents(cents)
1277		elif ratios is not None:
1278			t = _tuning_mod.Tuning.from_ratios(ratios)
1279		else:
1280			t = _tuning_mod.Tuning.equal(equal)  # type: ignore[arg-type]
1281
1282		self._tuning = t
1283		self._tuning_bend_range = bend_range
1284		self._tuning_channels = channels
1285		self._tuning_reference_note = reference_note
1286		self._tuning_exclude_drums = exclude_drums
1287
1288	def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
1289
1290		"""
1291		Enable or disable the live terminal dashboard.
1292
1293		When enabled, Subsequence uses a safe logging handler that allows a
1294		persistent status line (BPM, Key, Bar, Section, Chord) to stay at
1295		the bottom of the terminal while logs scroll above it.
1296
1297		Parameters:
1298			enabled: Whether to show the display (default True).
1299			grid: When True, render an ASCII grid visualisation of all
1300				running patterns above the status line. The grid updates
1301				once per bar, showing which steps have notes and at what
1302				velocity.
1303			grid_scale: Horizontal zoom factor for the grid (default
1304				``1.0``).  Higher values add visual columns between
1305				grid steps, revealing micro-timing from swing and groove.
1306				Snapped to the nearest integer internally for uniform
1307				marker spacing.
1308		"""
1309
1310		if enabled:
1311			self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale)
1312		else:
1313			self._display = None
1314
1315	def web_ui (self) -> None:
1316
1317		"""
1318		Enable the realtime Web UI Dashboard.
1319
1320		When enabled, Subsequence instantiates a WebSocket server that broadcasts 
1321		the current state, signals, and active patterns (with high-res timing and note data) 
1322		to any connected browser clients.
1323		"""
1324
1325		self._web_ui_enabled = True
1326
1327	def midi_input (self, device: str, clock_follow: bool = False, name: typing.Optional[str] = None) -> None:
1328
1329		"""
1330		Configure a MIDI input device for external sync and MIDI messages.
1331
1332		May be called multiple times to register additional input devices.
1333		The first call sets the primary input (device 0).  Subsequent calls
1334		add additional input devices (device 1, 2, …).  Only one device may
1335		have ``clock_follow=True``.
1336
1337		Parameters:
1338			device: The name of the MIDI input port.
1339			clock_follow: If True, Subsequence will slave its clock to incoming
1340				MIDI Ticks. It will also follow MIDI Start/Stop/Continue
1341				commands. Only one device can have this enabled at a time.
1342			name: Optional alias for use with ``cc_map(input_device=…)`` and
1343				``cc_forward(input_device=…)``.  When omitted, the raw device
1344				name is used.
1345
1346		Example:
1347			```python
1348			# Single controller (unchanged usage)
1349			comp.midi_input("Scarlett 2i4", clock_follow=True)
1350
1351			# Multiple controllers
1352			comp.midi_input("Arturia KeyStep", name="keys")
1353			comp.midi_input("Faderfox EC4", name="faders")
1354			```
1355		"""
1356
1357		if clock_follow:
1358			if self.is_clock_following:
1359				raise ValueError("Only one input device can be configured to follow external clock (clock_follow=True)")
1360
1361		if self._input_device is None:
1362			# First call: set primary input device (device 0)
1363			self._input_device = device
1364			self._input_device_alias = name
1365			self._clock_follow = clock_follow
1366		else:
1367			# Subsequent calls: register additional input devices
1368			self._additional_inputs.append((device, name, clock_follow))
1369
1370	def midi_output (self, device: str, name: typing.Optional[str] = None) -> int:
1371
1372		"""
1373		Register an additional MIDI output device.
1374
1375		The first output device is always the one passed to
1376		``Composition(output_device=…)`` — that is device 0.
1377		Each call to ``midi_output()`` adds the next device (1, 2, …).
1378
1379		Parameters:
1380			device: The name of the MIDI output port.
1381			name: Optional alias for use with ``pattern(device=…)``,
1382				``cc_forward(output_device=…)``, etc.  When omitted, the raw
1383				device name is used.
1384
1385		Returns:
1386			The integer device index assigned (1, 2, 3, …).
1387
1388		Example:
1389			```python
1390			comp = subsequence.Composition(bpm=120, output_device="MOTU Express")
1391
1392			# Returns 1 — use as device=1 or device="integra"
1393			comp.midi_output("Roland Integra", name="integra")
1394
1395			@comp.pattern(channel=1, beats=4, device="integra")
1396			def strings (p):
1397				p.note(60, beat=0)
1398			```
1399		"""
1400
1401		idx = 1 + len(self._additional_outputs)  # device 0 is always the primary
1402		self._additional_outputs.append((device, name))
1403		return idx
1404
1405	def clock_output (self, enabled: bool = True) -> None:
1406
1407		"""
1408		Send MIDI timing clock to connected hardware.
1409
1410		When enabled, Subsequence acts as a MIDI clock master and sends
1411		standard clock messages on the output port: a Start message (0xFA)
1412		when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN),
1413		and a Stop message (0xFC) when playback ends.
1414
1415		This allows hardware synthesizers, drum machines, and effect units to
1416		slave their tempo to Subsequence automatically.
1417
1418		**Note:** Clock output is automatically disabled when ``midi_input()``
1419		is called with ``clock_follow=True``, to prevent a clock feedback loop.
1420
1421		Parameters:
1422			enabled: Whether to send MIDI clock (default True).
1423
1424		Example:
1425			```python
1426			comp = subsequence.Composition(bpm=120, output_device="...")
1427			comp.clock_output()   # hardware will follow Subsequence tempo
1428			```
1429		"""
1430
1431		self._clock_output = enabled
1432
1433
1434	def link (self, quantum: float = 4.0) -> "Composition":
1435
1436		"""
1437		Enable Ableton Link tempo and phase synchronisation.
1438
1439		When enabled, Subsequence joins the local Link session and slaves its
1440		clock to the shared network tempo and beat phase.  All other Link-enabled
1441		apps on the same LAN — Ableton Live, iOS synths, other Subsequence
1442		instances — will automatically stay in time.
1443
1444		Playback starts on the next bar boundary aligned to the Link quantum,
1445		so downbeats stay in sync across all participants.
1446
1447		Requires the ``link`` optional extra::
1448
1449		    pip install subsequence[link]
1450
1451		Parameters:
1452			quantum: Beat cycle length.  ``4.0`` (default) = one bar in 4/4 time.
1453			         Change this if your composition uses a different meter.
1454
1455		Example::
1456
1457		    comp = subsequence.Composition(bpm=120, key="C")
1458		    comp.link()          # join the Link session
1459		    comp.play()
1460
1461		    # On another machine / instance:
1462		    comp2 = subsequence.Composition(bpm=120)
1463		    comp2.link()         # tempo and phase will lock to comp
1464		    comp2.play()
1465
1466		Note:
1467		    ``set_bpm()`` proposes the new tempo to the Link network when Link
1468		    is active.  The network-authoritative tempo is applied on the next
1469		    pulse, so there may be a brief lag before the change is visible.
1470		"""
1471
1472		# Eagerly check that aalink is installed — fail early with a clear message.
1473		subsequence.link_clock._require_aalink()
1474
1475		self._link_quantum = quantum
1476		return self
1477
1478
1479	def cc_map (
1480		self,
1481		cc: int,
1482		key: str,
1483		channel: typing.Optional[int] = None,
1484		min_val: float = 0.0,
1485		max_val: float = 1.0,
1486		input_device: subsequence.midi_utils.DeviceId = None,
1487	) -> None:
1488
1489		"""
1490		Map an incoming MIDI CC to a ``composition.data`` key.
1491
1492		When the composition receives a CC message on the configured MIDI
1493		input port, the value is scaled from the CC range (0–127) to
1494		*[min_val, max_val]* and stored in ``composition.data[key]``.
1495
1496		This lets hardware knobs, faders, and expression pedals control live
1497		parameters without writing any callback code.
1498
1499		**Requires** ``midi_input()`` to be called first to open an input port.
1500
1501		Parameters:
1502			cc: MIDI Control Change number (0–127).
1503			key: The ``composition.data`` key to write.
1504			channel: If given, only respond to CC messages on this channel.
1505				Uses the same numbering convention as ``pattern()`` (0-15
1506				by default, or 1-16 with ``zero_indexed_channels=False``).
1507				``None`` matches any channel (default).
1508			min_val: Scaled minimum — written when CC value is 0 (default 0.0).
1509			max_val: Scaled maximum — written when CC value is 127 (default 1.0).
1510			input_device: Only respond to CC messages from this input device
1511				(index or name).  ``None`` responds to any input device (default).
1512
1513		Example:
1514			```python
1515			comp.midi_input("Arturia KeyStep")
1516			comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
1517			comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader
1518
1519			# Multi-device: only listen to CC 74 from the "faders" controller
1520			comp.cc_map(74, "filter", input_device="faders")
1521			```
1522		"""
1523
1524		resolved_channel = self._resolve_channel(channel) if channel is not None else None
1525
1526		self._cc_mappings.append({
1527			'cc': cc,
1528			'key': key,
1529			'channel': resolved_channel,
1530			'min_val': min_val,
1531			'max_val': max_val,
1532			'input_device': input_device,  # resolved to int index in _run()
1533		})
1534
1535
1536	@staticmethod
1537	def _make_cc_forward_transform (
1538		output: typing.Union[str, typing.Callable],
1539		cc: int,
1540		output_channel: typing.Optional[int],
1541	) -> typing.Callable:
1542
1543		"""Build a transform callable from a preset string or user-supplied callable.
1544
1545		The returned callable has signature ``(value: int, channel: int) -> Optional[mido.Message]``
1546		where ``channel`` is the 0-indexed incoming channel.
1547		"""
1548
1549		import mido as _mido
1550
1551		def _out_ch (incoming: int) -> int:
1552			return output_channel if output_channel is not None else incoming
1553
1554		if callable(output):
1555			if output_channel is None:
1556				return output
1557			def _wrapped (value: int, channel: int) -> typing.Optional[typing.Any]:
1558				msg = output(value, channel)
1559				if msg is not None and output_channel is not None:
1560					# Rebuild message with overridden channel
1561					return _mido.Message(msg.type, channel=output_channel, **{
1562						k: v for k, v in msg.__dict__.items() if k != 'channel'
1563					})
1564				return msg
1565			return _wrapped
1566
1567		if output == 'cc':
1568			def _cc_identity (value: int, channel: int) -> typing.Any:
1569				return _mido.Message('control_change', channel=_out_ch(channel), control=cc, value=value)
1570			return _cc_identity
1571
1572		if output.startswith('cc:'):
1573			try:
1574				target_cc = int(output[3:])
1575			except ValueError:
1576				raise ValueError(f"cc_forward(): invalid preset '{output}' — expected 'cc:N' where N is 0–127")
1577			if not 0 <= target_cc <= 127:
1578				raise ValueError(f"cc_forward(): CC number {target_cc} out of range 0–127")
1579			def _cc_remap (value: int, channel: int) -> typing.Any:
1580				return _mido.Message('control_change', channel=_out_ch(channel), control=target_cc, value=value)
1581			return _cc_remap
1582
1583		if output == 'pitchwheel':
1584			def _pitchwheel (value: int, channel: int) -> typing.Any:
1585				pitch = int(value / 127 * 16383) - 8192
1586				return _mido.Message('pitchwheel', channel=_out_ch(channel), pitch=pitch)
1587			return _pitchwheel
1588
1589		raise ValueError(
1590			f"cc_forward(): unknown preset '{output}'. "
1591			"Use 'cc', 'cc:N' (e.g. 'cc:74'), 'pitchwheel', or a callable."
1592		)
1593
1594
1595	def cc_forward (
1596		self,
1597		cc: int,
1598		output: typing.Union[str, typing.Callable],
1599		*,
1600		channel: typing.Optional[int] = None,
1601		output_channel: typing.Optional[int] = None,
1602		mode: str = "instant",
1603		input_device: subsequence.midi_utils.DeviceId = None,
1604		output_device: subsequence.midi_utils.DeviceId = None,
1605	) -> None:
1606
1607		"""
1608		Forward an incoming MIDI CC to the MIDI output in real-time.
1609
1610		Unlike ``cc_map()`` which writes incoming CC values to ``composition.data``
1611		for use at pattern rebuild time, ``cc_forward()`` routes the signal
1612		directly to the MIDI output — bypassing the pattern cycle entirely.
1613
1614		Both ``cc_map()`` and ``cc_forward()`` may be registered for the same CC
1615		number; they operate independently.
1616
1617		Parameters:
1618			cc: Incoming CC number to listen for (0–127).
1619			output: What to send. Either a **preset string**:
1620
1621				- ``"cc"`` — identity forward, same CC number and value.
1622				- ``"cc:N"`` — forward as CC number N (e.g. ``"cc:74"``).
1623				- ``"pitchwheel"`` — scale 0–127 to -8192..8191 and send as pitch bend.
1624
1625				Or a **callable** with signature
1626				``(value: int, channel: int) -> Optional[mido.Message]``.
1627				Return a fully formed ``mido.Message`` to send, or ``None`` to suppress.
1628				``channel`` is 0-indexed (the incoming channel).
1629			channel: If given, only respond to CC messages on this channel.
1630				Uses the same numbering convention as ``cc_map()``.
1631				``None`` matches any channel (default).
1632			output_channel: Override the output channel. ``None`` uses the
1633				incoming channel. Uses the same numbering convention as ``pattern()``.
1634			mode: Dispatch mode:
1635
1636				- ``"instant"`` *(default)* — send immediately on the MIDI input
1637				  callback thread. Lowest latency (~1–5 ms). Instant forwards are
1638				  **not** recorded when recording is enabled.
1639				- ``"queued"`` — inject into the sequencer event queue and send at
1640				  the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards
1641				  **are** recorded when recording is enabled.
1642
1643		Example:
1644			```python
1645			comp.midi_input("Arturia KeyStep")
1646
1647			# CC 1 → CC 1 (identity, instant)
1648			comp.cc_forward(1, "cc")
1649
1650			# CC 1 → pitch bend on channel 1, queued (recordable)
1651			comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued")
1652
1653			# CC 1 → CC 74, custom channel
1654			comp.cc_forward(1, "cc:74", output_channel=2)
1655
1656			# Custom transform — remap CC range 0–127 to CC 74 range 40–100
1657			import subsequence.midi as midi
1658			comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch))
1659
1660			# Forward AND map to data simultaneously — both active on the same CC
1661			comp.cc_map(1, "mod_wheel")
1662			comp.cc_forward(1, "cc:74")
1663			```
1664		"""
1665
1666		if not 0 <= cc <= 127:
1667			raise ValueError(f"cc_forward(): cc {cc} out of range 0–127")
1668
1669		if mode not in ('instant', 'queued'):
1670			raise ValueError(f"cc_forward(): mode must be 'instant' or 'queued', got '{mode}'")
1671
1672		resolved_in_channel = self._resolve_channel(channel) if channel is not None else None
1673		resolved_out_channel = self._resolve_channel(output_channel) if output_channel is not None else None
1674
1675		transform = self._make_cc_forward_transform(output, cc, resolved_out_channel)
1676
1677		self._cc_forwards.append({
1678			'cc': cc,
1679			'channel': resolved_in_channel,
1680			'output_channel': resolved_out_channel,
1681			'mode': mode,
1682			'transform': transform,
1683			'input_device': input_device,   # resolved to int index in _run()
1684			'output_device': output_device, # resolved to int index in _run()
1685		})
1686
1687
1688	def live (self, port: int = 5555) -> None:
1689
1690		"""
1691		Enable the live coding eval server.
1692
1693		This allows you to connect to a running composition using the 
1694		`subsequence.live_client` REPL and hot-swap pattern code or 
1695		modify variables in real-time.
1696
1697		Parameters:
1698			port: The TCP port to listen on (default 5555).
1699		"""
1700
1701		self._live_server = subsequence.live_server.LiveServer(self, port=port)
1702		self._is_live = True
1703
1704	def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None:
1705
1706		"""
1707		Enable bi-directional Open Sound Control (OSC).
1708
1709		Subsequence will listen for commands (like `/bpm` or `/mute`) and 
1710		broadcast its internal state (like `/chord` or `/bar`) over UDP.
1711
1712		Parameters:
1713			receive_port: Port to listen for incoming OSC messages (default 9000).
1714			send_port: Port to send state updates to (default 9001).
1715			send_host: The IP address to send updates to (default "127.0.0.1").
1716		"""
1717
1718		self._osc_server = subsequence.osc.OscServer(
1719			self,
1720			receive_port = receive_port,
1721			send_port = send_port,
1722			send_host = send_host
1723		)
1724
1725	def osc_map (self, address: str, handler: typing.Callable) -> None:
1726
1727		"""
1728		Register a custom OSC handler.
1729
1730		Must be called after :meth:`osc` has been configured.
1731
1732		Parameters:
1733			address: OSC address pattern to match (e.g. ``"/my/param"``).
1734			handler: Callable invoked with ``(address, *args)`` when a
1735				matching message arrives.
1736
1737		Example::
1738
1739			composition.osc("/control")
1740
1741			def on_intensity (address, value):
1742				composition.data["intensity"] = float(value)
1743
1744			composition.osc_map("/intensity", on_intensity)
1745		"""
1746
1747		if self._osc_server is None:
1748			raise RuntimeError("Call composition.osc() before composition.osc_map()")
1749
1750		self._osc_server.map(address, handler)
1751
1752	def set_bpm (self, bpm: float) -> None:
1753
1754		"""
1755		Instantly change the tempo.
1756
1757		Parameters:
1758			bpm: The new tempo in beats per minute.
1759
1760		When Ableton Link is active, this proposes the new tempo to the Link
1761		network instead of applying it locally.  The network-authoritative tempo
1762		is picked up on the next pulse.
1763		"""
1764
1765		self._sequencer.set_bpm(bpm)
1766
1767		if not self.is_clock_following and self._link_quantum is None:
1768			self.bpm = bpm
1769
1770	def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None:
1771
1772		"""
1773		Smoothly ramp the tempo to a target value over a number of bars.
1774
1775		Parameters:
1776			bpm: Target tempo in beats per minute.
1777			bars: Duration of the transition in bars.
1778			shape: Easing curve name.  Defaults to ``"linear"``.
1779			       ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural-
1780			       sounding tempo changes.  See :mod:`subsequence.easing` for all
1781			       available shapes.
1782
1783		Example:
1784			```python
1785			# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
1786			comp.target_bpm(140, bars=8, shape="ease_in_out")
1787			```
1788		"""
1789
1790		self._sequencer.set_target_bpm(bpm, bars, shape)
1791
1792	def live_info (self) -> typing.Dict[str, typing.Any]:
1793
1794		"""
1795		Return a dictionary containing the current state of the composition.
1796		
1797		Includes BPM, key, current bar, active section, current chord, 
1798		running patterns, and custom data.
1799		"""
1800
1801		section_info = None
1802		if self._form_state is not None:
1803			section = self._form_state.get_section_info()
1804			if section is not None:
1805				section_info = {
1806					"name": section.name,
1807					"bar": section.bar,
1808					"bars": section.bars,
1809					"progress": section.progress
1810				}
1811
1812		chord_name = None
1813		if self._harmonic_state is not None:
1814			chord = self._harmonic_state.get_current_chord()
1815			if chord is not None:
1816				chord_name = chord.name()
1817
1818		pattern_list = []
1819		channel_offset = 0 if self._zero_indexed_channels else 1
1820		for name, pat in self._running_patterns.items():
1821			pattern_list.append({
1822				"name": name,
1823				"channel": pat.channel + channel_offset,
1824				"length": pat.length,
1825				"cycle": pat._cycle_count,
1826				"muted": pat._muted,
1827				"tweaks": dict(pat._tweaks)
1828			})
1829
1830		return {
1831			"bpm": self._sequencer.current_bpm,
1832			"key": self.key,
1833			"bar": self._builder_bar,
1834			"section": section_info,
1835			"chord": chord_name,
1836			"patterns": pattern_list,
1837			"input_device": self._input_device,
1838			"clock_follow": self.is_clock_following,
1839			"data": self.data
1840		}
1841
1842	def mute (self, name: str) -> None:
1843
1844		"""
1845		Mute a running pattern by name.
1846		
1847		The pattern continues to 'run' and increment its cycle count in 
1848		the background, but it will not produce any MIDI notes until unmuted.
1849
1850		Parameters:
1851			name: The function name of the pattern to mute.
1852		"""
1853
1854		if name not in self._running_patterns:
1855			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1856
1857		self._running_patterns[name]._muted = True
1858		logger.info(f"Muted pattern: {name}")
1859
1860	def unmute (self, name: str) -> None:
1861
1862		"""
1863		Unmute a previously muted pattern.
1864		"""
1865
1866		if name not in self._running_patterns:
1867			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1868
1869		self._running_patterns[name]._muted = False
1870		logger.info(f"Unmuted pattern: {name}")
1871
1872	def tweak (self, name: str, **kwargs: typing.Any) -> None:
1873
1874		"""Override parameters for a running pattern.
1875
1876		Values set here are available inside the pattern's builder
1877		function via ``p.param()``.  They persist across rebuilds
1878		until explicitly changed or cleared.  Changes take effect
1879		on the next rebuild cycle.
1880
1881		Parameters:
1882			name: The function name of the pattern.
1883			**kwargs: Parameter names and their new values.
1884
1885		Example (from the live REPL)::
1886
1887			composition.tweak("bass", pitches=[48, 52, 55, 60])
1888		"""
1889
1890		if name not in self._running_patterns:
1891			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1892
1893		self._running_patterns[name]._tweaks.update(kwargs)
1894		logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}")
1895
1896	def clear_tweak (self, name: str, *param_names: str) -> None:
1897
1898		"""Remove tweaked parameters from a running pattern.
1899
1900		If no parameter names are given, all tweaks for the pattern
1901		are cleared and every ``p.param()`` call reverts to its
1902		default.
1903
1904		Parameters:
1905			name: The function name of the pattern.
1906			*param_names: Specific parameter names to clear.  If
1907				omitted, all tweaks are removed.
1908		"""
1909
1910		if name not in self._running_patterns:
1911			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1912
1913		if not param_names:
1914			self._running_patterns[name]._tweaks.clear()
1915			logger.info(f"Cleared all tweaks for pattern '{name}'")
1916		else:
1917			for param_name in param_names:
1918				self._running_patterns[name]._tweaks.pop(param_name, None)
1919			logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}")
1920
1921	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
1922
1923		"""Return a copy of the current tweaks for a running pattern.
1924
1925		Parameters:
1926			name: The function name of the pattern.
1927		"""
1928
1929		if name not in self._running_patterns:
1930			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1931
1932		return dict(self._running_patterns[name]._tweaks)
1933
1934	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
1935
1936		"""
1937		Register a custom function to run on a repeating beat-based cycle.
1938
1939		Subsequence automatically runs synchronous functions in a thread pool
1940		so they don't block the timing-critical MIDI clock. Async functions
1941		are run directly on the event loop.
1942
1943		Parameters:
1944			fn: The function to call.
1945			cycle_beats: How often to call it (e.g., 4 = every bar).
1946			reschedule_lookahead: How far in advance to schedule the next call.
1947			wait_for_initial: If True, run the function once during startup
1948				and wait for it to complete before playback begins. This
1949				ensures ``composition.data`` is populated before patterns
1950				first build. Implies ``defer=True`` for the repeating
1951				schedule.
1952			defer: If True, skip the pulse-0 fire and defer the first
1953				repeating call to just before the second cycle boundary.
1954		"""
1955
1956		self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer))
1957
1958	def form (
1959		self,
1960		sections: typing.Union[
1961			typing.List[typing.Tuple[str, int]],
1962			typing.Iterator[typing.Tuple[str, int]],
1963			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
1964		],
1965		loop: bool = False,
1966		start: typing.Optional[str] = None
1967	) -> None:
1968
1969		"""
1970		Define the structure (sections) of the composition.
1971
1972		You can define form in three ways:
1973		1. **Graph (Dict)**: Dynamic transitions based on weights.
1974		2. **Sequence (List)**: A fixed order of sections.
1975		3. **Generator**: A Python generator that yields `(name, bars)` pairs.
1976
1977		Parameters:
1978			sections: The form definition (Dict, List, or Generator).
1979			loop: Whether to cycle back to the start (List mode only).
1980			start: The section to start with (Graph mode only).
1981
1982		Example:
1983			```python
1984			# A simple pop structure
1985			comp.form([
1986				("verse", 8),
1987				("chorus", 8),
1988				("verse", 8),
1989				("chorus", 16)
1990			])
1991			```
1992		"""
1993
1994		self._form_state = subsequence.form_state.FormState(sections, loop=loop, start=start)
1995
1996	@staticmethod
1997	def _resolve_length (
1998		beats: typing.Optional[float],
1999		bars: typing.Optional[float],
2000		steps: typing.Optional[float],
2001		unit: typing.Optional[float],
2002		default: float = 4.0
2003	) -> typing.Tuple[float, int]:
2004
2005		"""
2006		Resolve the beat_length and default_grid from the duration parameters.
2007
2008		Two modes:
2009		- **Duration mode** (no ``unit``): specify ``beats=`` or ``bars=``.
2010		  ``beats=4`` = 4 quarter notes; ``bars=2`` = 8 beats.
2011		- **Step mode** (with ``unit``): specify ``steps=`` and ``unit=``.
2012		  ``steps=6, unit=dur.SIXTEENTH`` = 6 sixteenth notes = 1.5 beats.
2013
2014		Constraints:
2015		- ``beats`` and ``bars`` are mutually exclusive.
2016		- ``steps`` requires ``unit``; ``unit`` requires ``steps``.
2017		- ``steps`` cannot be combined with ``beats`` or ``bars``.
2018
2019		Returns:
2020			(beat_length, default_grid) — beat_length in beats (quarter notes),
2021			default_grid in 16th-note steps.
2022		"""
2023
2024		if beats is not None and bars is not None:
2025			raise ValueError("Specify only one of beats= or bars=")
2026
2027		if steps is not None and (beats is not None or bars is not None):
2028			raise ValueError("steps= cannot be combined with beats= or bars=")
2029
2030		if unit is not None and steps is None:
2031			raise ValueError("unit= requires steps= (e.g. steps=6, unit=dur.SIXTEENTH)")
2032
2033		if steps is not None:
2034			if unit is None:
2035				raise ValueError("steps= requires unit= (e.g. unit=dur.SIXTEENTH)")
2036			return steps * unit, int(steps)
2037
2038		if bars is not None:
2039			raw = bars * 4
2040		elif beats is not None:
2041			raw = beats
2042		else:
2043			raw = default
2044
2045		return raw, round(raw / subsequence.constants.durations.SIXTEENTH)
2046
2047	def pattern (
2048		self,
2049		channel: int,
2050		beats: typing.Optional[float] = None,
2051		bars: typing.Optional[float] = None,
2052		steps: typing.Optional[float] = None,
2053		unit: typing.Optional[float] = None,
2054		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2055		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
2056		reschedule_lookahead: float = 1,
2057		voice_leading: bool = False,
2058		device: subsequence.midi_utils.DeviceId = None,
2059	) -> typing.Callable:
2060
2061		"""
2062		Register a function as a repeating MIDI pattern.
2063
2064		The decorated function will be called once per cycle to 'rebuild' its
2065		content. This allows for generative logic that evolves over time.
2066
2067		Two ways to specify pattern length:
2068
2069		- **Duration mode** (default): use ``beats=`` or ``bars=``.
2070		  The grid defaults to sixteenth-note resolution.
2071		- **Step mode**: use ``steps=`` paired with ``unit=``.
2072		  The grid equals the step count, so ``p.hit_steps()`` indices map
2073		  directly to steps.
2074
2075		Parameters:
2076			channel: MIDI channel. By default uses 0-based numbering (0-15)
2077				matching the raw MIDI protocol. Set
2078				``zero_indexed_channels=False`` on the ``Composition`` to use
2079				1-based numbering (1-16) instead.
2080			beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar.
2081			bars: Duration in bars (4 beats each, assumes 4/4). ``bars=2`` = 8 beats.
2082			steps: Step count for step mode. Requires ``unit=``.
2083			unit: Duration of one step in beats (e.g. ``dur.SIXTEENTH``).
2084				Requires ``steps=``.
2085			drum_note_map: Optional mapping for drum instruments.
2086			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
2087				Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``.
2088			reschedule_lookahead: Beats in advance to compute the next cycle.
2089			voice_leading: If True, chords in this pattern will automatically
2090				use inversions that minimize voice movement.
2091
2092		Example:
2093			```python
2094			@comp.pattern(channel=1, beats=4)
2095			def chords (p):
2096				p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)
2097
2098			@comp.pattern(channel=1, bars=2)
2099			def long_phrase (p):
2100				...
2101
2102			@comp.pattern(channel=1, steps=6, unit=dur.SIXTEENTH)
2103			def riff (p):
2104				p.sequence(steps=[0, 1, 3, 5], pitches=60)
2105			```
2106		"""
2107
2108		channel = self._resolve_channel(channel)
2109
2110		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit)
2111
2112		# Resolve device string name to index if possible now; otherwise store
2113		# the raw DeviceId and resolve it in _run() once all devices are open.
2114		resolved_device: subsequence.midi_utils.DeviceId = device
2115
2116		def decorator (fn: typing.Callable) -> typing.Callable:
2117
2118			"""
2119			Wrap the builder function and register it as a pending pattern.
2120			During live sessions, hot-swap an existing pattern's builder instead.
2121			"""
2122
2123			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
2124			if self._is_live and fn.__name__ in self._running_patterns:
2125				running = self._running_patterns[fn.__name__]
2126				running._builder_fn = fn
2127				running._wants_chord = _fn_has_parameter(fn, "chord")
2128				logger.info(f"Hot-swapped pattern: {fn.__name__}")
2129				return fn
2130
2131			pending = _PendingPattern(
2132				builder_fn = fn,
2133				channel = channel,  # already resolved to 0-indexed
2134				length = beat_length,
2135				default_grid = default_grid,
2136				drum_note_map = drum_note_map,
2137				cc_name_map = cc_name_map,
2138				reschedule_lookahead = reschedule_lookahead,
2139				voice_leading = voice_leading,
2140				# For int/None: resolve immediately.  For str: store 0 as
2141				# placeholder; _resolve_pending_devices() fixes it in _run().
2142				device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device,
2143				raw_device = resolved_device,
2144			)
2145
2146			self._pending_patterns.append(pending)
2147
2148			return fn
2149
2150		return decorator
2151
2152	def layer (
2153		self,
2154		*builder_fns: typing.Callable,
2155		channel: int,
2156		beats: typing.Optional[float] = None,
2157		bars: typing.Optional[float] = None,
2158		steps: typing.Optional[float] = None,
2159		unit: typing.Optional[float] = None,
2160		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2161		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
2162		reschedule_lookahead: float = 1,
2163		voice_leading: bool = False,
2164		device: subsequence.midi_utils.DeviceId = None,
2165	) -> None:
2166
2167		"""
2168		Combine multiple functions into a single MIDI pattern.
2169
2170		This is useful for composing complex patterns out of reusable
2171		building blocks (e.g., a 'kick' function and a 'snare' function).
2172
2173		See ``pattern()`` for the full description of ``beats``, ``bars``,
2174		``steps``, and ``unit``.
2175
2176		Parameters:
2177			builder_fns: One or more pattern builder functions.
2178			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
2179			beats: Duration in beats (quarter notes).
2180			bars: Duration in bars (4 beats each, assumes 4/4).
2181			steps: Step count for step mode. Requires ``unit=``.
2182			unit: Duration of one step in beats. Requires ``steps=``.
2183			drum_note_map: Optional mapping for drum instruments.
2184			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
2185			reschedule_lookahead: Beats in advance to compute the next cycle.
2186			voice_leading: If True, chords use smooth voice leading.
2187		"""
2188
2189		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit)
2190
2191		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
2192
2193		if wants_chord:
2194
2195			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
2196
2197				for fn in builder_fns:
2198					if _fn_has_parameter(fn, "chord"):
2199						fn(p, chord)
2200					else:
2201						fn(p)
2202
2203		else:
2204
2205			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
2206
2207				for fn in builder_fns:
2208					fn(p)
2209
2210		resolved = self._resolve_channel(channel)
2211
2212		pending = _PendingPattern(
2213			builder_fn = merged_builder,
2214			channel = resolved,  # already resolved to 0-indexed
2215			length = beat_length,
2216			default_grid = default_grid,
2217			drum_note_map = drum_note_map,
2218			cc_name_map = cc_name_map,
2219			reschedule_lookahead = reschedule_lookahead,
2220			voice_leading = voice_leading,
2221				device = 0 if (device is None or isinstance(device, str)) else device,
2222			raw_device = device,
2223		)
2224
2225		self._pending_patterns.append(pending)
2226
2227	def trigger (
2228		self,
2229		fn: typing.Callable,
2230		channel: int,
2231		beats: typing.Optional[float] = None,
2232		bars: typing.Optional[float] = None,
2233		steps: typing.Optional[float] = None,
2234		unit: typing.Optional[float] = None,
2235		quantize: float = 0,
2236		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2237		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
2238		chord: bool = False,
2239		device: subsequence.midi_utils.DeviceId = None,
2240	) -> None:
2241
2242		"""
2243		Trigger a one-shot pattern immediately or on a quantized boundary.
2244
2245		This is useful for real-time response to sensors, OSC messages, or other
2246		external events. The builder function is called immediately with a fresh
2247		PatternBuilder, and the generated events are injected into the queue at
2248		the specified quantize boundary.
2249
2250		The builder function has the same API as a ``@composition.pattern``
2251		decorated function and can use all PatternBuilder methods: ``p.note()``,
2252		``p.euclidean()``, ``p.arpeggio()``, and so on.
2253
2254		See ``pattern()`` for the full description of ``beats``, ``bars``,
2255		``steps``, and ``unit``. Default is 1 beat.
2256
2257		Parameters:
2258			fn: The pattern builder function (same signature as ``@comp.pattern``).
2259			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
2260			beats: Duration in beats (quarter notes, default 1).
2261			bars: Duration in bars (4 beats each, assumes 4/4).
2262			steps: Step count for step mode. Requires ``unit=``.
2263			unit: Duration of one step in beats. Requires ``steps=``.
2264			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
2265				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
2266				constants from ``subsequence.constants.durations``.
2267			drum_note_map: Optional drum name mapping for this pattern.
2268			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
2269			chord: If ``True``, the builder function receives the current chord as
2270				a second parameter (same as ``@composition.pattern``).
2271
2272		Example:
2273			```python
2274			# Immediate single note
2275			composition.trigger(
2276				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
2277				channel=0
2278			)
2279
2280			# Quantized fill (next bar)
2281			import subsequence.constants.durations as dur
2282			composition.trigger(
2283				lambda p: p.euclidean("snare", pulses=7, velocity=90),
2284				channel=9,
2285				drum_note_map=gm_drums.GM_DRUM_MAP,
2286				quantize=dur.WHOLE
2287			)
2288
2289			# With chord context
2290			composition.trigger(
2291				lambda p: p.arpeggio(p.chord.tones(root=60), spacing=dur.SIXTEENTH),
2292				channel=0,
2293				quantize=dur.QUARTER,
2294				chord=True
2295			)
2296			```
2297		"""
2298
2299		# Resolve channel numbering
2300		resolved_channel = self._resolve_channel(channel)
2301
2302		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit, default=1.0)
2303
2304		# Resolve device index
2305		resolved_device_idx = self._resolve_device_id(device)
2306
2307		# Create a temporary Pattern
2308		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx)
2309
2310		# Create a PatternBuilder
2311		builder = subsequence.pattern_builder.PatternBuilder(
2312			pattern=pattern,
2313			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
2314			drum_note_map=drum_note_map,
2315			cc_name_map=cc_name_map,
2316			section=self._form_state.get_section_info() if self._form_state else None,
2317			bar=self._builder_bar,
2318			conductor=self.conductor,
2319			rng=random.Random(),  # Fresh random state for each trigger
2320			tweaks={},
2321			default_grid=default_grid,
2322			data=self.data
2323		)
2324
2325		# Call the builder function
2326		try:
2327
2328			if chord and self._harmonic_state is not None:
2329				current_chord = self._harmonic_state.get_current_chord()
2330				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
2331				fn(builder, injected)
2332
2333			else:
2334				fn(builder)
2335
2336		except Exception:
2337			logger.exception("Error in trigger builder — pattern will be silent")
2338			return
2339
2340		# Calculate the start pulse based on quantize
2341		current_pulse = self._sequencer.pulse_count
2342		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
2343
2344		if quantize == 0:
2345			# Immediate: use current pulse
2346			start_pulse = current_pulse
2347
2348		else:
2349			# Quantize to the next multiple of (quantize * pulses_per_beat)
2350			quantize_pulses = int(quantize * pulses_per_beat)
2351			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
2352
2353		# Schedule the pattern for one-shot execution
2354		try:
2355			loop = asyncio.get_running_loop()
2356			# Already on the event loop
2357			asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse))
2358
2359		except RuntimeError:
2360			# Not on the event loop — schedule via call_soon_threadsafe
2361			if self._sequencer._event_loop is not None:
2362				asyncio.run_coroutine_threadsafe(
2363					self._sequencer.schedule_pattern(pattern, start_pulse),
2364					loop=self._sequencer._event_loop
2365				)
2366			else:
2367				logger.warning("trigger() called before playback started; pattern ignored")
2368
2369	@property
2370	def is_clock_following (self) -> bool:
2371
2372		"""True if either the primary or any additional device is following external clock."""
2373
2374		return self._clock_follow or any(cf for _, _, cf in self._additional_inputs)
2375
2376
2377	def play (self) -> None:
2378
2379		"""
2380		Start the composition.
2381
2382		This call blocks until the program is interrupted (e.g., via Ctrl+C).
2383		It initializes the MIDI hardware, launches the background sequencer,
2384		and begins playback.
2385		"""
2386
2387		try:
2388			asyncio.run(self._run())
2389
2390		except KeyboardInterrupt:
2391			pass
2392
2393
2394	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
2395
2396		"""Render the composition to a MIDI file without real-time playback.
2397
2398		Runs the sequencer as fast as possible (no timing delays) and stops
2399		when the first active limit is reached.  The result is saved as a
2400		standard MIDI file that can be imported into any DAW.
2401
2402		All patterns, scheduled callbacks, and harmony logic run exactly as
2403		they would during live playback — BPM transitions, generative fills,
2404		and probabilistic gates all work in render mode.  The only difference
2405		is that time is simulated rather than wall-clock driven.
2406
2407		Parameters:
2408			bars: Number of bars to render, or ``None`` for no bar limit
2409			      (default ``None``).  When both *bars* and *max_minutes* are
2410			      active, playback stops at whichever limit is reached first.
2411			filename: Output MIDI filename (default ``"render.mid"``).
2412			max_minutes: Safety cap on the length of rendered MIDI in minutes
2413			             (default ``60.0``).  Pass ``None`` to disable the time
2414			             cap — you must then provide an explicit *bars* value.
2415
2416		Raises:
2417			ValueError: If both *bars* and *max_minutes* are ``None``, which
2418			            would produce an infinite render.
2419
2420		Examples:
2421			```python
2422			# Default: renders up to 60 minutes of MIDI content.
2423			composition.render()
2424
2425			# Render exactly 64 bars (time cap still active as backstop).
2426			composition.render(bars=64, filename="demo.mid")
2427
2428			# Render up to 5 minutes of an infinite generative composition.
2429			composition.render(max_minutes=5, filename="five_min.mid")
2430
2431			# Remove the time cap — must supply bars instead.
2432			composition.render(bars=128, max_minutes=None, filename="long.mid")
2433			```
2434		"""
2435
2436		if bars is None and max_minutes is None:
2437			raise ValueError(
2438				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
2439				"Passing both as None would produce an infinite render."
2440			)
2441
2442		self._sequencer.recording = True
2443		self._sequencer.record_filename = filename
2444		self._sequencer.render_mode = True
2445		self._sequencer.render_bars = bars if bars is not None else 0
2446		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
2447		asyncio.run(self._run())
2448
2449	async def _run (self) -> None:
2450
2451		"""
2452		Async entry point that schedules all patterns and runs the sequencer.
2453		"""
2454
2455		# 1. Pre-calculate MIDI input indices and configure sequencer clock follow.
2456		if self._input_device is not None:
2457			self._sequencer.input_device_name = self._input_device
2458			self._sequencer.clock_follow = self._clock_follow
2459			self._sequencer.clock_device_idx = 0
2460
2461			if not self._clock_follow:
2462				# Find first additional input that wants to be the clock master.
2463				for idx, (_, _, cf) in enumerate(self._additional_inputs, start=1):
2464					if cf:
2465						self._sequencer.clock_follow = True
2466						self._sequencer.clock_device_idx = idx
2467						break
2468
2469		# Populate input device name mapping early (before opening ports) so we can
2470		# resolve CC mappings to integer device indices immediately.
2471		if self._sequencer.input_device_name:
2472			self._input_device_names[self._sequencer.input_device_name] = 0
2473			if self._input_device_alias is not None:
2474				self._input_device_names[self._input_device_alias] = 0
2475
2476		for idx, (dev_name, alias, _) in enumerate(self._additional_inputs, start=1):
2477			self._input_device_names[dev_name] = idx
2478			if alias:
2479				self._input_device_names[alias] = idx
2480
2481		# 2. Pre-calculate output device names.
2482		if self._sequencer.output_device_name:
2483			self._output_device_names[self._sequencer.output_device_name] = 0
2484
2485		# 3. Resolve name-based device ids in cc_map/cc_forward/pending patterns early.
2486		# This ensures we have integer indices ready for the background callback thread.
2487		self._resolve_pending_devices()
2488		for mapping in self._cc_mappings:
2489			raw = mapping.get('input_device')
2490			if isinstance(raw, str):
2491				mapping['input_device'] = self._resolve_input_device_id(raw)
2492		for fwd in self._cc_forwards:
2493			raw_in = fwd.get('input_device')
2494			if isinstance(raw_in, str):
2495				fwd['input_device'] = self._resolve_input_device_id(raw_in)
2496			raw_out = fwd.get('output_device')
2497			if isinstance(raw_out, str):
2498				fwd['output_device'] = self._resolve_device_id(raw_out)
2499
2500		# 4. Share CC input mappings, forwards, and a reference to composition.data
2501		# with the sequencer BEFORE opening the ports. This ensures that any initial
2502		# messages in the OS buffer are correctly mapped as soon as the port opens.
2503		self._sequencer.cc_mappings = self._cc_mappings
2504		self._sequencer.cc_forwards = self._cc_forwards
2505		self._sequencer._composition_data = self.data
2506
2507		# 5. Open MIDI input ports early. Even without a deliberate sleep, opening
2508		# them before pattern building minimizes the window for missed messages.
2509		# Primary input
2510		self._sequencer._open_midi_inputs()
2511
2512		# Additional inputs
2513		for idx, (dev_name, alias, cf) in enumerate(self._additional_inputs, start=1):
2514			# Use the pre-calculated index
2515			callback = self._sequencer._make_input_callback(idx)
2516			open_name, port = subsequence.midi_utils.select_input_device(dev_name, callback)
2517			if open_name and port is not None:
2518				self._sequencer.add_input_device(open_name, port)
2519			else:
2520				logger.warning(f"Could not open additional input device '{dev_name}'")
2521
2522		# 6. Open additional MIDI output devices.
2523		for dev_name, alias in self._additional_outputs:
2524			open_name, port = subsequence.midi_utils.select_output_device(dev_name)
2525			if open_name and port is not None:
2526				idx = self._sequencer.add_output_device(open_name, port)
2527				self._output_device_names[open_name] = idx
2528				if alias is not None:
2529					self._output_device_names[alias] = idx
2530			else:
2531				logger.warning(f"Could not open additional output device '{dev_name}'")
2532
2533		# Resolve any name-based output device IDs on patterns that may have been added
2534		# for additional output devices.
2535		self._resolve_pending_devices()
2536
2537		# Pass clock output flag (suppressed automatically when clock_follow=True).
2538		self._sequencer.clock_output = self._clock_output and not self.is_clock_following
2539
2540		# Create Ableton Link clock if comp.link() was called.
2541		if self._link_quantum is not None:
2542			self._sequencer._link_clock = subsequence.link_clock.LinkClock(
2543				bpm = self.bpm,
2544				quantum = self._link_quantum,
2545				loop = asyncio.get_running_loop(),
2546			)
2547
2548		# Derive child RNGs from the master seed so each component gets
2549		# an independent, deterministic stream.  When no seed is set,
2550		# each component creates its own unseeded RNG (existing behaviour).
2551		self._pattern_rngs: typing.List[random.Random] = []
2552
2553		if self._seed is not None:
2554			master = random.Random(self._seed)
2555
2556			if self._harmonic_state is not None:
2557				self._harmonic_state.rng = random.Random(master.randint(0, 2 ** 63))
2558
2559			if self._form_state is not None:
2560				self._form_state._rng = random.Random(master.randint(0, 2 ** 63))
2561
2562			for _ in self._pending_patterns:
2563				self._pattern_rngs.append(random.Random(master.randint(0, 2 ** 63)))
2564
2565		if self._harmonic_state is not None and self._harmony_cycle_beats is not None:
2566
2567			def _get_section_progression () -> typing.Optional[typing.Tuple[str, int, typing.Optional[Progression]]]:
2568				"""Return (section_name, section_index, Progression|None) for the current section, or None."""
2569				if self._form_state is None:
2570					return None
2571				info = self._form_state.get_section_info()
2572				if info is None:
2573					return None
2574				prog = self._section_progressions.get(info.name)
2575				return (info.name, info.index, prog)
2576
2577			await schedule_harmonic_clock(
2578				sequencer = self._sequencer,
2579				get_harmonic_state = lambda: self._harmonic_state,
2580				cycle_beats = self._harmony_cycle_beats,
2581				reschedule_lookahead = self._harmony_reschedule_lookahead,
2582				get_section_progression = _get_section_progression,
2583			)
2584
2585		if self._form_state is not None:
2586
2587			await schedule_form(
2588				sequencer = self._sequencer,
2589				form_state = self._form_state,
2590				reschedule_lookahead = 1
2591			)
2592
2593		# Bar counter - always active so p.bar is available to all builders.
2594		def _advance_builder_bar (pulse: int) -> None:
2595			self._builder_bar += 1
2596
2597		first_bar_pulse = int(self.time_signature[0] * self._sequencer.pulses_per_beat)
2598
2599		await self._sequencer.schedule_callback_repeating(
2600			callback = _advance_builder_bar,
2601			interval_beats = self.time_signature[0],
2602			start_pulse = first_bar_pulse,
2603			reschedule_lookahead = 1
2604		)
2605
2606		# Run wait_for_initial=True scheduled functions and block until all complete.
2607		# This ensures composition.data is populated before patterns build.
2608		initial_tasks = [t for t in self._pending_scheduled if t.wait_for_initial]
2609
2610		if initial_tasks:
2611
2612			names = ", ".join(t.fn.__name__ for t in initial_tasks)
2613			logger.info(f"Waiting for initial scheduled {'function' if len(initial_tasks) == 1 else 'functions'} before start: {names}")
2614
2615			async def _run_initial (fn: typing.Callable) -> None:
2616
2617				accepts_ctx = _fn_has_parameter(fn, "p")
2618				ctx = ScheduleContext(cycle=0)
2619
2620				try:
2621					if asyncio.iscoroutinefunction(fn):
2622						await (fn(ctx) if accepts_ctx else fn())
2623					else:
2624						loop = asyncio.get_running_loop()
2625						call = (lambda: fn(ctx)) if accepts_ctx else fn
2626						await loop.run_in_executor(None, call)
2627				except Exception as exc:
2628					logger.warning(f"Initial run of {fn.__name__!r} failed: {exc}")
2629
2630			await asyncio.gather(*[_run_initial(t.fn) for t in initial_tasks])
2631
2632		for pending_task in self._pending_scheduled:
2633
2634			accepts_ctx = _fn_has_parameter(pending_task.fn, "p")
2635			wrapped = _make_safe_callback(pending_task.fn, accepts_context=accepts_ctx)
2636
2637			# wait_for_initial=True implies defer — no point firing at pulse 0
2638			# after the blocking run just completed.  defer=True skips the
2639			# backshift fire so the first repeating call happens one full cycle
2640			# later.
2641			if pending_task.wait_for_initial or pending_task.defer:
2642				start_pulse = int(pending_task.cycle_beats * self._sequencer.pulses_per_beat)
2643			else:
2644				start_pulse = 0
2645
2646			await self._sequencer.schedule_callback_repeating(
2647				callback = wrapped,
2648				interval_beats = pending_task.cycle_beats,
2649				start_pulse = start_pulse,
2650				reschedule_lookahead = pending_task.reschedule_lookahead
2651			)
2652
2653		# Build Pattern objects from pending registrations.
2654		patterns: typing.List[subsequence.pattern.Pattern] = []
2655
2656		for i, pending in enumerate(self._pending_patterns):
2657
2658			pattern_rng = self._pattern_rngs[i] if i < len(self._pattern_rngs) else None
2659			pattern = self._build_pattern_from_pending(pending, pattern_rng)
2660			patterns.append(pattern)
2661
2662		await schedule_patterns(
2663			sequencer = self._sequencer,
2664			patterns = patterns,
2665			start_pulse = 0
2666		)
2667
2668		# Populate the running patterns dict for live hot-swap and mute/unmute.
2669		for i, pending in enumerate(self._pending_patterns):
2670			name = pending.builder_fn.__name__
2671			self._running_patterns[name] = patterns[i]
2672
2673		if self._display is not None and not self._sequencer.render_mode:
2674			self._display.start()
2675			self._sequencer.on_event("bar",  self._display.update)
2676			self._sequencer.on_event("beat", self._display.update)
2677
2678		if self._live_server is not None:
2679			await self._live_server.start()
2680
2681		if self._osc_server is not None:
2682			await self._osc_server.start()
2683			self._sequencer.osc_server = self._osc_server
2684
2685			def _send_osc_status (bar: int) -> None:
2686				if self._osc_server:
2687					self._osc_server.send("/bar", bar)
2688					self._osc_server.send("/bpm", self._sequencer.current_bpm)
2689					
2690					if self._harmonic_state:
2691						self._osc_server.send("/chord", self._harmonic_state.current_chord.name())
2692					
2693					if self._form_state:
2694						info = self._form_state.get_section_info()
2695						if info:
2696							self._osc_server.send("/section", info.name)
2697
2698			self._sequencer.on_event("bar", _send_osc_status)
2699
2700		# Start keystroke listener if hotkeys are enabled and not in render mode.
2701		if self._hotkeys_enabled and not self._sequencer.render_mode:
2702			self._keystroke_listener = subsequence.keystroke.KeystrokeListener()
2703			self._keystroke_listener.start()
2704
2705			if self._keystroke_listener.active:
2706				# Listener started successfully — register the bar handler
2707				# and show all bindings so the user knows what's available.
2708				self._sequencer.on_event("bar", self._process_hotkeys)
2709				self._list_hotkeys()
2710			# If not active, KeystrokeListener.start() already logged a warning.
2711
2712		if self._web_ui_enabled and not self._sequencer.render_mode:
2713			self._web_ui_server = subsequence.web_ui.WebUI(self)
2714			self._web_ui_server.start()
2715
2716		await run_until_stopped(self._sequencer)
2717
2718		if self._web_ui_server is not None:
2719			self._web_ui_server.stop()
2720
2721		if self._live_server is not None:
2722			await self._live_server.stop()
2723
2724		if self._osc_server is not None:
2725			await self._osc_server.stop()
2726			self._sequencer.osc_server = None
2727
2728		if self._display is not None:
2729			self._display.stop()
2730
2731		if self._keystroke_listener is not None:
2732			self._keystroke_listener.stop()
2733			self._keystroke_listener = None
2734
2735	def _build_pattern_from_pending (self, pending: _PendingPattern, rng: typing.Optional[random.Random] = None) -> subsequence.pattern.Pattern:
2736
2737		"""
2738		Create a Pattern from a pending registration using a temporary subclass.
2739		"""
2740
2741		composition_ref = self
2742
2743		class _DecoratorPattern (subsequence.pattern.Pattern):
2744
2745			"""
2746			Pattern subclass that delegates to a builder function on each reschedule.
2747			"""
2748
2749			def __init__ (self, pending: _PendingPattern, pattern_rng: typing.Optional[random.Random] = None) -> None:
2750
2751				"""
2752				Initialize the decorator pattern from pending registration details.
2753				"""
2754
2755				super().__init__(
2756					channel = pending.channel,
2757					length = pending.length,
2758					reschedule_lookahead = min(
2759						pending.reschedule_lookahead,
2760						composition_ref._harmony_reschedule_lookahead
2761					),
2762					device = pending.device,
2763				)
2764
2765				self._builder_fn = pending.builder_fn
2766				self._drum_note_map = pending.drum_note_map
2767				self._cc_name_map = pending.cc_name_map
2768				self._default_grid: int = pending.default_grid
2769				self._wants_chord = _fn_has_parameter(pending.builder_fn, "chord")
2770				self._cycle_count = 0
2771				self._rng = pattern_rng
2772				self._muted = False
2773				self._voice_leading_state: typing.Optional[subsequence.voicings.VoiceLeadingState] = (
2774					subsequence.voicings.VoiceLeadingState() if pending.voice_leading else None
2775				)
2776				self._tweaks: typing.Dict[str, typing.Any] = {}
2777
2778				self._rebuild()
2779
2780			def _rebuild (self) -> None:
2781
2782				"""
2783				Clear steps and call the builder function to repopulate.
2784				"""
2785
2786				self.steps = {}
2787				self.cc_events = []
2788				self.osc_events = []
2789				current_cycle = self._cycle_count
2790				self._cycle_count += 1
2791
2792				if self._muted:
2793					return
2794
2795				builder = subsequence.pattern_builder.PatternBuilder(
2796					pattern = self,
2797					cycle = current_cycle,
2798					drum_note_map = self._drum_note_map,
2799					cc_name_map = self._cc_name_map,
2800					section = composition_ref._form_state.get_section_info() if composition_ref._form_state else None,
2801					bar = composition_ref._builder_bar,
2802					conductor = composition_ref.conductor,
2803					rng = self._rng,
2804					tweaks = self._tweaks,
2805					default_grid = self._default_grid,
2806					data = composition_ref.data
2807				)
2808
2809				try:
2810
2811					if self._wants_chord and composition_ref._harmonic_state is not None:
2812						chord = composition_ref._harmonic_state.get_current_chord()
2813						injected = _InjectedChord(chord, self._voice_leading_state)
2814						self._builder_fn(builder, injected)
2815
2816					else:
2817						self._builder_fn(builder)
2818
2819				except Exception:
2820					logger.exception("Error in pattern builder '%s' (cycle %d) - pattern will be silent this cycle", self._builder_fn.__name__, current_cycle)
2821
2822				# Auto-apply global tuning if set and not already applied by the builder.
2823				if (
2824					composition_ref._tuning is not None
2825					and not builder._tuning_applied
2826					and not (composition_ref._tuning_exclude_drums and self._drum_note_map)
2827				):
2828					import subsequence.tuning as _tuning_mod
2829					_tuning_mod.apply_tuning_to_pattern(
2830						self,
2831						composition_ref._tuning,
2832						bend_range=composition_ref._tuning_bend_range,
2833						channels=composition_ref._tuning_channels,
2834						reference_note=composition_ref._tuning_reference_note,
2835					)
2836
2837			def on_reschedule (self) -> None:
2838
2839				"""
2840				Rebuild the pattern from the builder function before the next cycle.
2841				"""
2842
2843				self._rebuild()
2844
2845		return _DecoratorPattern(pending, rng)

The top-level controller for a musical piece.

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

Typical workflow:

  1. Initialize Composition with BPM and Key.
  2. Define harmony and form (optional).
  3. Register patterns using the @composition.pattern decorator.
  4. Call composition.play() to start the music.
Composition( output_device: Optional[str] = None, bpm: float = 120, time_signature: Tuple[int, int] = (4, 4), key: Optional[str] = None, seed: Optional[int] = None, record: bool = False, record_filename: Optional[str] = None, zero_indexed_channels: bool = False)
581	def __init__ (
582		self,
583		output_device: typing.Optional[str] = None,
584		bpm: float = 120,
585		time_signature: typing.Tuple[int, int] = (4, 4),
586		key: typing.Optional[str] = None,
587		seed: typing.Optional[int] = None,
588		record: bool = False,
589		record_filename: typing.Optional[str] = None,
590		zero_indexed_channels: bool = False
591	) -> None:
592
593		"""
594		Initialize a new composition.
595
596		Parameters:
597			output_device: The name of the MIDI output port to use. If `None`,
598				Subsequence will attempt to find a device, prompting if necessary.
599			bpm: Initial tempo in beats per minute (default 120).
600			key: The root key of the piece (e.g., "C", "F#", "Bb").
601				Required if you plan to use `harmony()`.
602			seed: An optional integer for deterministic randomness. When set,
603				every random decision (chord choices, drum probability, etc.)
604				will be identical on every run.
605			record: When True, record all MIDI events to a file.
606			record_filename: Optional filename for the recording (defaults to timestamp).
607			zero_indexed_channels: When False (default), MIDI channels use
608				1-based numbering (1-16) matching instrument labelling.
609				Channel 10 is drums, the way musicians and hardware panels
610				show it. When True, channels use 0-based numbering (0-15)
611				matching the raw MIDI protocol.
612
613		Example:
614			```python
615			comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
616			```
617		"""
618
619		self.output_device = output_device
620		self.bpm = bpm
621		self.time_signature = time_signature
622		self.key = key
623		self._seed: typing.Optional[int] = seed
624		self._zero_indexed_channels: bool = zero_indexed_channels
625
626		self._sequencer = subsequence.sequencer.Sequencer(
627			output_device_name = output_device,
628			initial_bpm = bpm,
629			time_signature = time_signature,
630			record = record,
631			record_filename = record_filename
632		)
633
634		self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None
635		self._harmony_cycle_beats: typing.Optional[int] = None
636		self._harmony_reschedule_lookahead: float = 1
637		self._section_progressions: typing.Dict[str, Progression] = {}
638		self._pending_patterns: typing.List[_PendingPattern] = []
639		self._pending_scheduled: typing.List[_PendingScheduled] = []
640		self._form_state: typing.Optional[subsequence.form_state.FormState] = None
641		self._builder_bar: int = 0
642		self._display: typing.Optional[subsequence.display.Display] = None
643		self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None
644		self._is_live: bool = False
645		self._running_patterns: typing.Dict[str, typing.Any] = {}
646		self._input_device: typing.Optional[str] = None
647		self._input_device_alias: typing.Optional[str] = None
648		self._clock_follow: bool = False
649		self._clock_output: bool = False
650		self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = []
651		self._cc_forwards: typing.List[typing.Dict[str, typing.Any]] = []
652		# Additional output devices registered with midi_output() after construction.
653		# Each entry: (device_name: str, alias: Optional[str])
654		self._additional_outputs: typing.List[typing.Tuple[str, typing.Optional[str]]] = []
655		# Additional input devices: (device_name: str, alias: Optional[str], clock_follow: bool)
656		self._additional_inputs: typing.List[typing.Tuple[str, typing.Optional[str], bool]] = []
657		# Maps alias/name → output device index (populated in _run after all devices are opened).
658		self._output_device_names: typing.Dict[str, int] = {}
659		# Maps alias/name → input device index (populated in _run after all input devices are opened).
660		self._input_device_names: typing.Dict[str, int] = {}
661		self.data: typing.Dict[str, typing.Any] = {}
662		self._osc_server: typing.Optional[subsequence.osc.OscServer] = None
663		self.conductor = subsequence.conductor.Conductor()
664		self._web_ui_enabled: bool = False
665		self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None
666		self._link_quantum: typing.Optional[float] = None
667
668		# Hotkey state — populated by hotkeys() and hotkey().
669		self._hotkeys_enabled: bool = False
670		self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {}
671		self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = []
672		self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None
673
674		# Tuning state — populated by tuning().
675		self._tuning: typing.Optional[typing.Any] = None       # subsequence.tuning.Tuning
676		self._tuning_bend_range: float = 2.0
677		self._tuning_channels: typing.Optional[typing.List[int]] = None
678		self._tuning_reference_note: int = 60
679		self._tuning_exclude_drums: bool = True

Initialize a new composition.

Arguments:
  • output_device: The name of the MIDI output port to use. If None, Subsequence will attempt to find a device, prompting if necessary.
  • 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().
  • 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.
Example:
comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
output_device
bpm
time_signature
key
data: Dict[str, Any]
conductor
harmonic_state: Optional[subsequence.harmonic_state.HarmonicState]
748	@property
749	def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]:
750		"""The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called."""
751		return self._harmonic_state

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

form_state: Optional[subsequence.form_state.FormState]
753	@property
754	def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]:
755		"""The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called."""
756		return self._form_state

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

sequencer: subsequence.sequencer.Sequencer
758	@property
759	def sequencer (self) -> subsequence.sequencer.Sequencer:
760		"""The underlying ``Sequencer`` instance."""
761		return self._sequencer

The underlying Sequencer instance.

running_patterns: Dict[str, Any]
763	@property
764	def running_patterns (self) -> typing.Dict[str, typing.Any]:
765		"""The currently active patterns, keyed by name."""
766		return self._running_patterns

The currently active patterns, keyed by name.

builder_bar: int
768	@property
769	def builder_bar (self) -> int:
770		"""Current bar index used by pattern builders."""
771		return self._builder_bar

Current bar index used by pattern builders.

def harmony( self, style: Union[str, subsequence.chord_graphs.ChordGraph] = 'functional_major', 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 = 0.4, reschedule_lookahead: float = 1) -> None:
782	def harmony (
783		self,
784		style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major",
785		cycle_beats: int = 4,
786		dominant_7th: bool = True,
787		gravity: float = 1.0,
788		nir_strength: float = 0.5,
789		minor_turnaround_weight: float = 0.0,
790		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
791		reschedule_lookahead: float = 1
792	) -> None:
793
794		"""
795		Configure the harmonic logic and chord change intervals.
796
797		Subsequence uses a weighted transition graph to choose the next chord.
798		You can influence these choices using 'gravity' (favoring the tonic) and
799		'NIR strength' (melodic inertia based on Narmour's model).
800
801		Parameters:
802			style: The harmonic style to use. Built-in: "functional_major"
803				(alias "diatonic_major"), "turnaround", "aeolian_minor",
804				"phrygian_minor", "lydian_major", "dorian_minor",
805				"chromatic_mediant", "suspended", "mixolydian", "whole_tone",
806				"diminished". See README for full descriptions.
807			cycle_beats: How many beats each chord lasts (default 4).
808			dominant_7th: Whether to include V7 chords (default True).
809			gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
810			nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement
811				expectations.
812			minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
813			root_diversity: Root-repetition damping (0.0 to 1.0). Each recent
814				chord sharing a candidate's root reduces the weight to 40% at
815				the default (0.4). Set to 1.0 to disable.
816			reschedule_lookahead: How many beats in advance to calculate the
817				next chord.
818
819		Example:
820			```python
821			# A moody minor progression that changes every 8 beats
822			comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
823			```
824		"""
825
826		if self.key is None:
827			raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor")
828
829		preserved_history: typing.List[subsequence.chords.Chord] = []
830		preserved_current: typing.Optional[subsequence.chords.Chord] = None
831
832		if self._harmonic_state is not None:
833			preserved_history = self._harmonic_state.history.copy()
834			preserved_current = self._harmonic_state.current_chord
835
836		self._harmonic_state = subsequence.harmonic_state.HarmonicState(
837			key_name = self.key,
838			graph_style = style,
839			include_dominant_7th = dominant_7th,
840			key_gravity_blend = gravity,
841			nir_strength = nir_strength,
842			minor_turnaround_weight = minor_turnaround_weight,
843			root_diversity = root_diversity
844		)
845
846		if preserved_history:
847			self._harmonic_state.history = preserved_history
848		if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current):
849			self._harmonic_state.current_chord = preserved_current
850
851		self._harmony_cycle_beats = cycle_beats
852		self._harmony_reschedule_lookahead = reschedule_lookahead

Configure the harmonic logic and chord change intervals.

Subsequence uses a weighted transition graph to choose the next chord. You can influence these choices using 'gravity' (favoring the tonic) and 'NIR strength' (melodic inertia based on Narmour's model).

Arguments:
  • style: The harmonic style to use. Built-in: "functional_major" (alias "diatonic_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 chord lasts (default 4).
  • 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.
Example:
# A moody minor progression that changes every 8 beats
comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
def freeze(self, bars: int) -> subsequence.composition.Progression:
854	def freeze (self, bars: int) -> "Progression":
855
856		"""Capture a chord progression from the live harmony engine.
857
858		Runs the harmony engine forward by *bars* chord changes, records each
859		chord, and returns it as a :class:`Progression` that can be bound to a
860		form section with :meth:`section_chords`.
861
862		The engine state **advances** — successive ``freeze()`` calls produce a
863		continuing compositional journey so section progressions feel like parts
864		of a whole rather than isolated islands.
865
866		Parameters:
867			bars: Number of chords to capture (one per harmony cycle).
868
869		Returns:
870			A :class:`Progression` with the captured chords and trailing
871			history for NIR continuity.
872
873		Raises:
874			ValueError: If :meth:`harmony` has not been called first.
875
876		Example::
877
878			composition.harmony(style="functional_major", cycle_beats=4)
879			verse  = composition.freeze(8)   # 8 chords, engine advances
880			chorus = composition.freeze(4)   # next 4 chords, continuing on
881			composition.section_chords("verse",  verse)
882			composition.section_chords("chorus", chorus)
883		"""
884
885		hs = self._require_harmonic_state()
886
887		if bars < 1:
888			raise ValueError("bars must be at least 1")
889		collected: typing.List[subsequence.chords.Chord] = [hs.current_chord]
890
891		for _ in range(bars - 1):
892			hs.step()
893			collected.append(hs.current_chord)
894
895		# Advance past the last captured chord so the next freeze() call or
896		# live playback does not duplicate it.
897		hs.step()
898
899		return Progression(
900			chords = tuple(collected),
901			trailing_history = tuple(hs.history),
902		)

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.

Arguments:
  • bars: Number of chords to capture (one per harmony cycle).
Returns:

A Progression with the captured chords and trailing history for NIR continuity.

Raises:
  • ValueError: If harmony() has not been called first.

Example::

    composition.harmony(style="functional_major", cycle_beats=4)
    verse  = composition.freeze(8)   # 8 chords, engine advances
    chorus = composition.freeze(4)   # next 4 chords, continuing on
    composition.section_chords("verse",  verse)
    composition.section_chords("chorus", chorus)
def section_chords( self, section_name: str, progression: subsequence.composition.Progression) -> None:
904	def section_chords (self, section_name: str, progression: "Progression") -> None:
905
906		"""Bind a frozen :class:`Progression` to a named form section.
907
908		Every time *section_name* plays, the harmonic clock replays the
909		progression's chords in order instead of calling the live engine.
910		Sections without a bound progression continue generating live chords.
911
912		Parameters:
913			section_name: Name of the section as defined in :meth:`form`.
914			progression: The :class:`Progression` returned by :meth:`freeze`.
915
916		Raises:
917			ValueError: If the form has been configured and *section_name* is
918				not a known section name.
919
920		Example::
921
922			composition.section_chords("verse",  verse_progression)
923			composition.section_chords("chorus", chorus_progression)
924			# "bridge" is not bound — it generates live chords
925		"""
926
927		if (
928			self._form_state is not None
929			and self._form_state._section_bars is not None
930			and section_name not in self._form_state._section_bars
931		):
932			known = ", ".join(sorted(self._form_state._section_bars))
933			raise ValueError(
934				f"Section '{section_name}' not found in form. "
935				f"Known sections: {known}"
936			)
937
938		self._section_progressions[section_name] = progression

Bind a frozen Progression to a named form section.

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

Arguments:
  • section_name: Name of the section as defined in form().
  • progression: The Progression returned by freeze().
Raises:
  • ValueError: If the form has been configured and section_name is not a known section name.

Example::

    composition.section_chords("verse",  verse_progression)
    composition.section_chords("chorus", chorus_progression)
    # "bridge" is not bound — it generates live chords
def on_event(self, event_name: str, callback: Callable[..., Any]) -> None:
940	def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None:
941
942		"""
943		Register a callback for a sequencer event (e.g., "bar", "start", "stop").
944		"""
945
946		self._sequencer.on_event(event_name, callback)

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

def hotkeys(self, enabled: bool = True) -> None:
953	def hotkeys (self, enabled: bool = True) -> None:
954
955		"""Enable or disable the global hotkey listener.
956
957		Must be called **before** :meth:`play` to take effect.  When enabled, a
958		background thread reads single keystrokes from stdin without requiring
959		Enter.  The ``?`` key is always reserved and lists all active bindings.
960
961		Hotkeys have zero impact on playback when disabled — the listener
962		thread is never started.
963
964		Args:
965		    enabled: ``True`` (default) to enable hotkeys; ``False`` to disable.
966
967		Example::
968
969		    composition.hotkeys()
970		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
971		    composition.play()
972		"""
973
974		self._hotkeys_enabled = enabled

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.

Arguments:
  • enabled: True (default) to enable hotkeys; False to disable.

Example::

composition.hotkeys()
composition.hotkey("a", lambda: composition.form_jump("chorus"))
composition.play()
def hotkey( self, key: str, action: Callable[[], NoneType], quantize: int = 0, label: Optional[str] = None) -> None:
 977	def hotkey (
 978		self,
 979		key:      str,
 980		action:   typing.Callable[[], None],
 981		quantize: int = 0,
 982		label:    typing.Optional[str] = None,
 983	) -> None:
 984
 985		"""Register a single-key shortcut that fires during playback.
 986
 987		The listener must be enabled first with :meth:`hotkeys`.
 988
 989		Most actions — form jumps, ``composition.data`` writes, and
 990		:meth:`tweak` calls — should use ``quantize=0`` (the default).  Their
 991		musical effect is naturally delayed to the next pattern rebuild cycle,
 992		which provides automatic musical quantization without extra configuration.
 993
 994		Use ``quantize=N`` for actions where you want an explicit bar-boundary
 995		guarantee, such as :meth:`mute` / :meth:`unmute`.
 996
 997		The ``?`` key is reserved and cannot be overridden.
 998
 999		Args:
1000		    key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``).
1001		    action: Zero-argument callable to execute.
1002		    quantize: ``0`` = execute immediately (default).  ``N`` = execute
1003		        on the next global bar number divisible by *N*.
1004		    label: Display name for the ``?`` help listing.  Auto-derived from
1005		        the function name or lambda body if omitted.
1006
1007		Raises:
1008		    ValueError: If ``key`` is the reserved ``?`` character, or if
1009		        ``key`` is not exactly one character.
1010
1011		Example::
1012
1013		    composition.hotkeys()
1014
1015		    # Immediate — musical effect happens at next pattern rebuild
1016		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
1017		    composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
1018
1019		    # Explicit 4-bar phrase boundary
1020		    composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
1021
1022		    # Named function — label is derived automatically
1023		    def drop_to_breakdown ():
1024		        composition.form_jump("breakdown")
1025		        composition.mute("lead")
1026
1027		    composition.hotkey("d", drop_to_breakdown)
1028
1029		    composition.play()
1030		"""
1031
1032		if len(key) != 1:
1033			raise ValueError(f"hotkey key must be a single character, got {key!r}")
1034
1035		if key == _HOTKEY_RESERVED:
1036			raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.")
1037
1038		derived = label if label is not None else _derive_label(action)
1039
1040		self._hotkey_bindings[key] = HotkeyBinding(
1041			key      = key,
1042			action   = action,
1043			quantize = quantize,
1044			label    = derived,
1045		)

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.

Arguments:
  • 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 key is the reserved ? character, or if key is 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()
def form_jump(self, section_name: str) -> None:
1048	def form_jump (self, section_name: str) -> None:
1049
1050		"""Jump the form to a named section immediately.
1051
1052		Delegates to :meth:`subsequence.form_state.FormState.jump_to`.  Only works when the
1053		composition uses graph-mode form (a dict passed to :meth:`form`).
1054
1055		The musical effect is heard at the *next pattern rebuild cycle* — already-
1056		queued MIDI notes are unaffected.  This natural delay means ``form_jump``
1057		is effective without needing explicit quantization.
1058
1059		Args:
1060		    section_name: The section to jump to.
1061
1062		Raises:
1063		    ValueError: If no form is configured, or the form is not in graph
1064		        mode, or *section_name* is unknown.
1065
1066		Example::
1067
1068		    composition.hotkey("c", lambda: composition.form_jump("chorus"))
1069		"""
1070
1071		if self._form_state is None:
1072			raise ValueError("form_jump() requires a form to be configured via composition.form().")
1073
1074		self._form_state.jump_to(section_name)

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 to form()).

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.

Arguments:
  • 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"))
def form_next(self, section_name: str) -> None:
1077	def form_next (self, section_name: str) -> None:
1078
1079		"""Queue the next section — takes effect when the current section ends.
1080
1081		Unlike :meth:`form_jump`, this does not interrupt the current section.
1082		The queued section replaces the automatically pre-decided next section
1083		and takes effect at the natural section boundary.  The performer can
1084		change their mind by calling ``form_next`` again before the boundary.
1085
1086		Delegates to :meth:`subsequence.form_state.FormState.queue_next`.  Only works when the
1087		composition uses graph-mode form (a dict passed to :meth:`form`).
1088
1089		Args:
1090		    section_name: The section to queue.
1091
1092		Raises:
1093		    ValueError: If no form is configured, or the form is not in graph
1094		        mode, or *section_name* is unknown.
1095
1096		Example::
1097
1098		    composition.hotkey("c", lambda: composition.form_next("chorus"))
1099		"""
1100
1101		if self._form_state is None:
1102			raise ValueError("form_next() requires a form to be configured via composition.form().")
1103
1104		self._form_state.queue_next(section_name)

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(). Only works when the composition uses graph-mode form (a dict passed to form()).

Arguments:
  • 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"))
def seed(self, value: int) -> None:
1187	def seed (self, value: int) -> None:
1188
1189		"""
1190		Set a random seed for deterministic, repeatable playback.
1191
1192		If a seed is set, Subsequence will produce the exact same sequence 
1193		every time you run the script. This is vital for finishing tracks or 
1194		reproducing a specific 'performance'.
1195
1196		Parameters:
1197			value: An integer seed.
1198
1199		Example:
1200			```python
1201			# Fix the randomness
1202			comp.seed(42)
1203			```
1204		"""
1205
1206		self._seed = value

Set a random seed for deterministic, repeatable playback.

If a seed is set, Subsequence will produce the exact same sequence every time you run the script. This is vital for finishing tracks or reproducing a specific 'performance'.

Arguments:
  • value: An integer seed.
Example:
# Fix the randomness
comp.seed(42)
def tuning( self, source: Union[str, os.PathLike, NoneType] = None, *, cents: Optional[List[float]] = None, ratios: Optional[List[float]] = None, equal: Optional[int] = None, bend_range: float = 2.0, channels: Optional[List[int]] = None, reference_note: int = 60, exclude_drums: bool = True) -> None:
1208	def tuning (
1209		self,
1210		source: typing.Optional[typing.Union[str, "os.PathLike"]] = None,
1211		*,
1212		cents: typing.Optional[typing.List[float]] = None,
1213		ratios: typing.Optional[typing.List[float]] = None,
1214		equal: typing.Optional[int] = None,
1215		bend_range: float = 2.0,
1216		channels: typing.Optional[typing.List[int]] = None,
1217		reference_note: int = 60,
1218		exclude_drums: bool = True,
1219	) -> None:
1220
1221		"""Set a global microtonal tuning for the composition.
1222
1223		The tuning is applied automatically after each pattern rebuild (before
1224		the pattern is scheduled).  Drum patterns (those registered with a
1225		``drum_note_map``) are excluded by default.
1226
1227		Supply exactly one of the source parameters:
1228
1229		- ``source``: path to a Scala ``.scl`` file.
1230		- ``cents``: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).
1231		- ``ratios``: list of frequency ratios (e.g., ``[9/8, 5/4, 4/3, 3/2, 2]``).
1232		- ``equal``: integer for N-tone equal temperament (e.g., ``equal=19``).
1233
1234		For polyphonic parts, supply a ``channels`` pool.  Notes are spread
1235		across those MIDI channels so each can carry an independent pitch bend.
1236		The synth must be configured to match ``bend_range`` (its pitch-bend range
1237		setting in semitones).
1238
1239		Parameters:
1240			source: Path to a ``.scl`` file.
1241			cents: Cent offsets for scale degrees 1..N.
1242			ratios: Frequency ratios for scale degrees 1..N.
1243			equal: Number of equal divisions of the period.
1244			bend_range: Synth pitch-bend range in semitones (default ±2).
1245			channels: Channel pool for polyphonic rotation.
1246			reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).
1247			exclude_drums: When True (default), skip patterns that have a
1248			    ``drum_note_map`` (they use fixed GM pitches, not tuned ones).
1249
1250		Example:
1251			```python
1252			# Quarter-comma meantone from a Scala file
1253			comp.tuning("meanquar.scl")
1254
1255			# Just intonation from ratios
1256			comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2])
1257
1258			# 19-TET, monophonic
1259			comp.tuning(equal=19, bend_range=2.0)
1260
1261			# 31-TET with channel rotation for polyphony (channels 1-6)
1262			comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])
1263			```
1264		"""
1265		import subsequence.tuning as _tuning_mod
1266
1267		given = sum(x is not None for x in [source, cents, ratios, equal])
1268		if given == 0:
1269			raise ValueError("composition.tuning() requires one of: source, cents, ratios, or equal")
1270		if given > 1:
1271			raise ValueError("composition.tuning() accepts only one source parameter")
1272
1273		if source is not None:
1274			t = _tuning_mod.Tuning.from_scl(source)
1275		elif cents is not None:
1276			t = _tuning_mod.Tuning.from_cents(cents)
1277		elif ratios is not None:
1278			t = _tuning_mod.Tuning.from_ratios(ratios)
1279		else:
1280			t = _tuning_mod.Tuning.equal(equal)  # type: ignore[arg-type]
1281
1282		self._tuning = t
1283		self._tuning_bend_range = bend_range
1284		self._tuning_channels = channels
1285		self._tuning_reference_note = reference_note
1286		self._tuning_exclude_drums = exclude_drums

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 .scl file.
  • 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).

Arguments:
  • source: Path to a .scl file.
  • 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])
def display( self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
1288	def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
1289
1290		"""
1291		Enable or disable the live terminal dashboard.
1292
1293		When enabled, Subsequence uses a safe logging handler that allows a
1294		persistent status line (BPM, Key, Bar, Section, Chord) to stay at
1295		the bottom of the terminal while logs scroll above it.
1296
1297		Parameters:
1298			enabled: Whether to show the display (default True).
1299			grid: When True, render an ASCII grid visualisation of all
1300				running patterns above the status line. The grid updates
1301				once per bar, showing which steps have notes and at what
1302				velocity.
1303			grid_scale: Horizontal zoom factor for the grid (default
1304				``1.0``).  Higher values add visual columns between
1305				grid steps, revealing micro-timing from swing and groove.
1306				Snapped to the nearest integer internally for uniform
1307				marker spacing.
1308		"""
1309
1310		if enabled:
1311			self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale)
1312		else:
1313			self._display = 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.

Arguments:
  • 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.
def web_ui(self) -> None:
1315	def web_ui (self) -> None:
1316
1317		"""
1318		Enable the realtime Web UI Dashboard.
1319
1320		When enabled, Subsequence instantiates a WebSocket server that broadcasts 
1321		the current state, signals, and active patterns (with high-res timing and note data) 
1322		to any connected browser clients.
1323		"""
1324
1325		self._web_ui_enabled = True

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.

def midi_input( self, device: str, clock_follow: bool = False, name: Optional[str] = None) -> None:
1327	def midi_input (self, device: str, clock_follow: bool = False, name: typing.Optional[str] = None) -> None:
1328
1329		"""
1330		Configure a MIDI input device for external sync and MIDI messages.
1331
1332		May be called multiple times to register additional input devices.
1333		The first call sets the primary input (device 0).  Subsequent calls
1334		add additional input devices (device 1, 2, …).  Only one device may
1335		have ``clock_follow=True``.
1336
1337		Parameters:
1338			device: The name of the MIDI input port.
1339			clock_follow: If True, Subsequence will slave its clock to incoming
1340				MIDI Ticks. It will also follow MIDI Start/Stop/Continue
1341				commands. Only one device can have this enabled at a time.
1342			name: Optional alias for use with ``cc_map(input_device=…)`` and
1343				``cc_forward(input_device=…)``.  When omitted, the raw device
1344				name is used.
1345
1346		Example:
1347			```python
1348			# Single controller (unchanged usage)
1349			comp.midi_input("Scarlett 2i4", clock_follow=True)
1350
1351			# Multiple controllers
1352			comp.midi_input("Arturia KeyStep", name="keys")
1353			comp.midi_input("Faderfox EC4", name="faders")
1354			```
1355		"""
1356
1357		if clock_follow:
1358			if self.is_clock_following:
1359				raise ValueError("Only one input device can be configured to follow external clock (clock_follow=True)")
1360
1361		if self._input_device is None:
1362			# First call: set primary input device (device 0)
1363			self._input_device = device
1364			self._input_device_alias = name
1365			self._clock_follow = clock_follow
1366		else:
1367			# Subsequent calls: register additional input devices
1368			self._additional_inputs.append((device, name, clock_follow))

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.

Arguments:
  • 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=…) and cc_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")
def midi_output(self, device: str, name: Optional[str] = None) -> int:
1370	def midi_output (self, device: str, name: typing.Optional[str] = None) -> int:
1371
1372		"""
1373		Register an additional MIDI output device.
1374
1375		The first output device is always the one passed to
1376		``Composition(output_device=…)`` — that is device 0.
1377		Each call to ``midi_output()`` adds the next device (1, 2, …).
1378
1379		Parameters:
1380			device: The name of the MIDI output port.
1381			name: Optional alias for use with ``pattern(device=…)``,
1382				``cc_forward(output_device=…)``, etc.  When omitted, the raw
1383				device name is used.
1384
1385		Returns:
1386			The integer device index assigned (1, 2, 3, …).
1387
1388		Example:
1389			```python
1390			comp = subsequence.Composition(bpm=120, output_device="MOTU Express")
1391
1392			# Returns 1 — use as device=1 or device="integra"
1393			comp.midi_output("Roland Integra", name="integra")
1394
1395			@comp.pattern(channel=1, beats=4, device="integra")
1396			def strings (p):
1397				p.note(60, beat=0)
1398			```
1399		"""
1400
1401		idx = 1 + len(self._additional_outputs)  # device 0 is always the primary
1402		self._additional_outputs.append((device, name))
1403		return idx

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

Arguments:
  • device: The name of the MIDI output port.
  • name: Optional alias for use with pattern(device=…), cc_forward(output_device=…), etc. When omitted, the raw device name is used.
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")

@comp.pattern(channel=1, beats=4, device="integra")
def strings (p):
        p.note(60, beat=0)
def clock_output(self, enabled: bool = True) -> None:
1405	def clock_output (self, enabled: bool = True) -> None:
1406
1407		"""
1408		Send MIDI timing clock to connected hardware.
1409
1410		When enabled, Subsequence acts as a MIDI clock master and sends
1411		standard clock messages on the output port: a Start message (0xFA)
1412		when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN),
1413		and a Stop message (0xFC) when playback ends.
1414
1415		This allows hardware synthesizers, drum machines, and effect units to
1416		slave their tempo to Subsequence automatically.
1417
1418		**Note:** Clock output is automatically disabled when ``midi_input()``
1419		is called with ``clock_follow=True``, to prevent a clock feedback loop.
1420
1421		Parameters:
1422			enabled: Whether to send MIDI clock (default True).
1423
1424		Example:
1425			```python
1426			comp = subsequence.Composition(bpm=120, output_device="...")
1427			comp.clock_output()   # hardware will follow Subsequence tempo
1428			```
1429		"""
1430
1431		self._clock_output = enabled

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.

Arguments:
  • enabled: Whether to send MIDI clock (default True).
Example:
comp = subsequence.Composition(bpm=120, output_device="...")
comp.clock_output()   # hardware will follow Subsequence tempo
def cc_map( self, cc: int, key: str, channel: Optional[int] = None, min_val: float = 0.0, max_val: float = 1.0, input_device: Union[int, str, NoneType] = None) -> None:
1479	def cc_map (
1480		self,
1481		cc: int,
1482		key: str,
1483		channel: typing.Optional[int] = None,
1484		min_val: float = 0.0,
1485		max_val: float = 1.0,
1486		input_device: subsequence.midi_utils.DeviceId = None,
1487	) -> None:
1488
1489		"""
1490		Map an incoming MIDI CC to a ``composition.data`` key.
1491
1492		When the composition receives a CC message on the configured MIDI
1493		input port, the value is scaled from the CC range (0–127) to
1494		*[min_val, max_val]* and stored in ``composition.data[key]``.
1495
1496		This lets hardware knobs, faders, and expression pedals control live
1497		parameters without writing any callback code.
1498
1499		**Requires** ``midi_input()`` to be called first to open an input port.
1500
1501		Parameters:
1502			cc: MIDI Control Change number (0–127).
1503			key: The ``composition.data`` key to write.
1504			channel: If given, only respond to CC messages on this channel.
1505				Uses the same numbering convention as ``pattern()`` (0-15
1506				by default, or 1-16 with ``zero_indexed_channels=False``).
1507				``None`` matches any channel (default).
1508			min_val: Scaled minimum — written when CC value is 0 (default 0.0).
1509			max_val: Scaled maximum — written when CC value is 127 (default 1.0).
1510			input_device: Only respond to CC messages from this input device
1511				(index or name).  ``None`` responds to any input device (default).
1512
1513		Example:
1514			```python
1515			comp.midi_input("Arturia KeyStep")
1516			comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
1517			comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader
1518
1519			# Multi-device: only listen to CC 74 from the "faders" controller
1520			comp.cc_map(74, "filter", input_device="faders")
1521			```
1522		"""
1523
1524		resolved_channel = self._resolve_channel(channel) if channel is not None else None
1525
1526		self._cc_mappings.append({
1527			'cc': cc,
1528			'key': key,
1529			'channel': resolved_channel,
1530			'min_val': min_val,
1531			'max_val': max_val,
1532			'input_device': input_device,  # resolved to int index in _run()
1533		})

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

Arguments:
  • cc: MIDI Control Change number (0–127).
  • key: The composition.data key to write.
  • channel: If given, only respond to CC messages on this channel. Uses the same numbering convention as pattern() (0-15 by default, or 1-16 with zero_indexed_channels=False). None matches 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). None responds 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")
def cc_forward( self, cc: int, output: Union[str, Callable], *, channel: Optional[int] = None, output_channel: Optional[int] = None, mode: str = 'instant', input_device: Union[int, str, NoneType] = None, output_device: Union[int, str, NoneType] = None) -> None:
1595	def cc_forward (
1596		self,
1597		cc: int,
1598		output: typing.Union[str, typing.Callable],
1599		*,
1600		channel: typing.Optional[int] = None,
1601		output_channel: typing.Optional[int] = None,
1602		mode: str = "instant",
1603		input_device: subsequence.midi_utils.DeviceId = None,
1604		output_device: subsequence.midi_utils.DeviceId = None,
1605	) -> None:
1606
1607		"""
1608		Forward an incoming MIDI CC to the MIDI output in real-time.
1609
1610		Unlike ``cc_map()`` which writes incoming CC values to ``composition.data``
1611		for use at pattern rebuild time, ``cc_forward()`` routes the signal
1612		directly to the MIDI output — bypassing the pattern cycle entirely.
1613
1614		Both ``cc_map()`` and ``cc_forward()`` may be registered for the same CC
1615		number; they operate independently.
1616
1617		Parameters:
1618			cc: Incoming CC number to listen for (0–127).
1619			output: What to send. Either a **preset string**:
1620
1621				- ``"cc"`` — identity forward, same CC number and value.
1622				- ``"cc:N"`` — forward as CC number N (e.g. ``"cc:74"``).
1623				- ``"pitchwheel"`` — scale 0–127 to -8192..8191 and send as pitch bend.
1624
1625				Or a **callable** with signature
1626				``(value: int, channel: int) -> Optional[mido.Message]``.
1627				Return a fully formed ``mido.Message`` to send, or ``None`` to suppress.
1628				``channel`` is 0-indexed (the incoming channel).
1629			channel: If given, only respond to CC messages on this channel.
1630				Uses the same numbering convention as ``cc_map()``.
1631				``None`` matches any channel (default).
1632			output_channel: Override the output channel. ``None`` uses the
1633				incoming channel. Uses the same numbering convention as ``pattern()``.
1634			mode: Dispatch mode:
1635
1636				- ``"instant"`` *(default)* — send immediately on the MIDI input
1637				  callback thread. Lowest latency (~1–5 ms). Instant forwards are
1638				  **not** recorded when recording is enabled.
1639				- ``"queued"`` — inject into the sequencer event queue and send at
1640				  the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards
1641				  **are** recorded when recording is enabled.
1642
1643		Example:
1644			```python
1645			comp.midi_input("Arturia KeyStep")
1646
1647			# CC 1 → CC 1 (identity, instant)
1648			comp.cc_forward(1, "cc")
1649
1650			# CC 1 → pitch bend on channel 1, queued (recordable)
1651			comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued")
1652
1653			# CC 1 → CC 74, custom channel
1654			comp.cc_forward(1, "cc:74", output_channel=2)
1655
1656			# Custom transform — remap CC range 0–127 to CC 74 range 40–100
1657			import subsequence.midi as midi
1658			comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch))
1659
1660			# Forward AND map to data simultaneously — both active on the same CC
1661			comp.cc_map(1, "mod_wheel")
1662			comp.cc_forward(1, "cc:74")
1663			```
1664		"""
1665
1666		if not 0 <= cc <= 127:
1667			raise ValueError(f"cc_forward(): cc {cc} out of range 0–127")
1668
1669		if mode not in ('instant', 'queued'):
1670			raise ValueError(f"cc_forward(): mode must be 'instant' or 'queued', got '{mode}'")
1671
1672		resolved_in_channel = self._resolve_channel(channel) if channel is not None else None
1673		resolved_out_channel = self._resolve_channel(output_channel) if output_channel is not None else None
1674
1675		transform = self._make_cc_forward_transform(output, cc, resolved_out_channel)
1676
1677		self._cc_forwards.append({
1678			'cc': cc,
1679			'channel': resolved_in_channel,
1680			'output_channel': resolved_out_channel,
1681			'mode': mode,
1682			'transform': transform,
1683			'input_device': input_device,   # resolved to int index in _run()
1684			'output_device': output_device, # resolved to int index in _run()
1685		})

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.

Arguments:
  • 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 formed mido.Message to send, or None to suppress. channel is 0-indexed (the incoming channel).

  • channel: If given, only respond to CC messages on this channel. Uses the same numbering convention as cc_map(). None matches any channel (default).
  • output_channel: Override the output channel. None uses the incoming channel. Uses the same numbering convention as pattern().
  • 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")
def live(self, port: int = 5555) -> None:
1688	def live (self, port: int = 5555) -> None:
1689
1690		"""
1691		Enable the live coding eval server.
1692
1693		This allows you to connect to a running composition using the 
1694		`subsequence.live_client` REPL and hot-swap pattern code or 
1695		modify variables in real-time.
1696
1697		Parameters:
1698			port: The TCP port to listen on (default 5555).
1699		"""
1700
1701		self._live_server = subsequence.live_server.LiveServer(self, port=port)
1702		self._is_live = True

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.

Arguments:
  • port: The TCP port to listen on (default 5555).
def osc( self, receive_port: int = 9000, send_port: int = 9001, send_host: str = '127.0.0.1') -> None:
1704	def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None:
1705
1706		"""
1707		Enable bi-directional Open Sound Control (OSC).
1708
1709		Subsequence will listen for commands (like `/bpm` or `/mute`) and 
1710		broadcast its internal state (like `/chord` or `/bar`) over UDP.
1711
1712		Parameters:
1713			receive_port: Port to listen for incoming OSC messages (default 9000).
1714			send_port: Port to send state updates to (default 9001).
1715			send_host: The IP address to send updates to (default "127.0.0.1").
1716		"""
1717
1718		self._osc_server = subsequence.osc.OscServer(
1719			self,
1720			receive_port = receive_port,
1721			send_port = send_port,
1722			send_host = send_host
1723		)

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.

Arguments:
  • 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").
def osc_map(self, address: str, handler: Callable) -> None:
1725	def osc_map (self, address: str, handler: typing.Callable) -> None:
1726
1727		"""
1728		Register a custom OSC handler.
1729
1730		Must be called after :meth:`osc` has been configured.
1731
1732		Parameters:
1733			address: OSC address pattern to match (e.g. ``"/my/param"``).
1734			handler: Callable invoked with ``(address, *args)`` when a
1735				matching message arrives.
1736
1737		Example::
1738
1739			composition.osc("/control")
1740
1741			def on_intensity (address, value):
1742				composition.data["intensity"] = float(value)
1743
1744			composition.osc_map("/intensity", on_intensity)
1745		"""
1746
1747		if self._osc_server is None:
1748			raise RuntimeError("Call composition.osc() before composition.osc_map()")
1749
1750		self._osc_server.map(address, handler)

Register a custom OSC handler.

Must be called after osc() has been configured.

Arguments:
  • address: OSC address pattern to match (e.g. "/my/param").
  • handler: Callable invoked with (address, *args) when a matching message arrives.

Example::

    composition.osc("/control")

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

    composition.osc_map("/intensity", on_intensity)
def set_bpm(self, bpm: float) -> None:
1752	def set_bpm (self, bpm: float) -> None:
1753
1754		"""
1755		Instantly change the tempo.
1756
1757		Parameters:
1758			bpm: The new tempo in beats per minute.
1759
1760		When Ableton Link is active, this proposes the new tempo to the Link
1761		network instead of applying it locally.  The network-authoritative tempo
1762		is picked up on the next pulse.
1763		"""
1764
1765		self._sequencer.set_bpm(bpm)
1766
1767		if not self.is_clock_following and self._link_quantum is None:
1768			self.bpm = bpm

Instantly change the tempo.

Arguments:
  • 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.

def target_bpm(self, bpm: float, bars: int, shape: str = 'linear') -> None:
1770	def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None:
1771
1772		"""
1773		Smoothly ramp the tempo to a target value over a number of bars.
1774
1775		Parameters:
1776			bpm: Target tempo in beats per minute.
1777			bars: Duration of the transition in bars.
1778			shape: Easing curve name.  Defaults to ``"linear"``.
1779			       ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural-
1780			       sounding tempo changes.  See :mod:`subsequence.easing` for all
1781			       available shapes.
1782
1783		Example:
1784			```python
1785			# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
1786			comp.target_bpm(140, bars=8, shape="ease_in_out")
1787			```
1788		"""
1789
1790		self._sequencer.set_target_bpm(bpm, bars, shape)

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

Arguments:
  • 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. See subsequence.easing for 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")
def live_info(self) -> Dict[str, Any]:
1792	def live_info (self) -> typing.Dict[str, typing.Any]:
1793
1794		"""
1795		Return a dictionary containing the current state of the composition.
1796		
1797		Includes BPM, key, current bar, active section, current chord, 
1798		running patterns, and custom data.
1799		"""
1800
1801		section_info = None
1802		if self._form_state is not None:
1803			section = self._form_state.get_section_info()
1804			if section is not None:
1805				section_info = {
1806					"name": section.name,
1807					"bar": section.bar,
1808					"bars": section.bars,
1809					"progress": section.progress
1810				}
1811
1812		chord_name = None
1813		if self._harmonic_state is not None:
1814			chord = self._harmonic_state.get_current_chord()
1815			if chord is not None:
1816				chord_name = chord.name()
1817
1818		pattern_list = []
1819		channel_offset = 0 if self._zero_indexed_channels else 1
1820		for name, pat in self._running_patterns.items():
1821			pattern_list.append({
1822				"name": name,
1823				"channel": pat.channel + channel_offset,
1824				"length": pat.length,
1825				"cycle": pat._cycle_count,
1826				"muted": pat._muted,
1827				"tweaks": dict(pat._tweaks)
1828			})
1829
1830		return {
1831			"bpm": self._sequencer.current_bpm,
1832			"key": self.key,
1833			"bar": self._builder_bar,
1834			"section": section_info,
1835			"chord": chord_name,
1836			"patterns": pattern_list,
1837			"input_device": self._input_device,
1838			"clock_follow": self.is_clock_following,
1839			"data": self.data
1840		}

Return a dictionary containing the current state of the composition.

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

def mute(self, name: str) -> None:
1842	def mute (self, name: str) -> None:
1843
1844		"""
1845		Mute a running pattern by name.
1846		
1847		The pattern continues to 'run' and increment its cycle count in 
1848		the background, but it will not produce any MIDI notes until unmuted.
1849
1850		Parameters:
1851			name: The function name of the pattern to mute.
1852		"""
1853
1854		if name not in self._running_patterns:
1855			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1856
1857		self._running_patterns[name]._muted = True
1858		logger.info(f"Muted pattern: {name}")

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.

Arguments:
  • name: The function name of the pattern to mute.
def unmute(self, name: str) -> None:
1860	def unmute (self, name: str) -> None:
1861
1862		"""
1863		Unmute a previously muted pattern.
1864		"""
1865
1866		if name not in self._running_patterns:
1867			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1868
1869		self._running_patterns[name]._muted = False
1870		logger.info(f"Unmuted pattern: {name}")

Unmute a previously muted pattern.

def tweak(self, name: str, **kwargs: Any) -> None:
1872	def tweak (self, name: str, **kwargs: typing.Any) -> None:
1873
1874		"""Override parameters for a running pattern.
1875
1876		Values set here are available inside the pattern's builder
1877		function via ``p.param()``.  They persist across rebuilds
1878		until explicitly changed or cleared.  Changes take effect
1879		on the next rebuild cycle.
1880
1881		Parameters:
1882			name: The function name of the pattern.
1883			**kwargs: Parameter names and their new values.
1884
1885		Example (from the live REPL)::
1886
1887			composition.tweak("bass", pitches=[48, 52, 55, 60])
1888		"""
1889
1890		if name not in self._running_patterns:
1891			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1892
1893		self._running_patterns[name]._tweaks.update(kwargs)
1894		logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}")

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.

Arguments:
  • 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])
def clear_tweak(self, name: str, *param_names: str) -> None:
1896	def clear_tweak (self, name: str, *param_names: str) -> None:
1897
1898		"""Remove tweaked parameters from a running pattern.
1899
1900		If no parameter names are given, all tweaks for the pattern
1901		are cleared and every ``p.param()`` call reverts to its
1902		default.
1903
1904		Parameters:
1905			name: The function name of the pattern.
1906			*param_names: Specific parameter names to clear.  If
1907				omitted, all tweaks are removed.
1908		"""
1909
1910		if name not in self._running_patterns:
1911			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1912
1913		if not param_names:
1914			self._running_patterns[name]._tweaks.clear()
1915			logger.info(f"Cleared all tweaks for pattern '{name}'")
1916		else:
1917			for param_name in param_names:
1918				self._running_patterns[name]._tweaks.pop(param_name, None)
1919			logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}")

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.

Arguments:
  • name: The function name of the pattern.
  • *param_names: Specific parameter names to clear. If omitted, all tweaks are removed.
def get_tweaks(self, name: str) -> Dict[str, Any]:
1921	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
1922
1923		"""Return a copy of the current tweaks for a running pattern.
1924
1925		Parameters:
1926			name: The function name of the pattern.
1927		"""
1928
1929		if name not in self._running_patterns:
1930			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1931
1932		return dict(self._running_patterns[name]._tweaks)

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

Arguments:
  • name: The function name of the pattern.
def schedule( self, fn: Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
1934	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
1935
1936		"""
1937		Register a custom function to run on a repeating beat-based cycle.
1938
1939		Subsequence automatically runs synchronous functions in a thread pool
1940		so they don't block the timing-critical MIDI clock. Async functions
1941		are run directly on the event loop.
1942
1943		Parameters:
1944			fn: The function to call.
1945			cycle_beats: How often to call it (e.g., 4 = every bar).
1946			reschedule_lookahead: How far in advance to schedule the next call.
1947			wait_for_initial: If True, run the function once during startup
1948				and wait for it to complete before playback begins. This
1949				ensures ``composition.data`` is populated before patterns
1950				first build. Implies ``defer=True`` for the repeating
1951				schedule.
1952			defer: If True, skip the pulse-0 fire and defer the first
1953				repeating call to just before the second cycle boundary.
1954		"""
1955
1956		self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer))

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.

Arguments:
  • 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.data is populated before patterns first build. Implies defer=True for the repeating schedule.
  • defer: If True, skip the pulse-0 fire and defer the first repeating call to just before the second cycle boundary.
def form( self, sections: Union[List[Tuple[str, int]], Iterator[Tuple[str, int]], Dict[str, Tuple[int, Optional[List[Tuple[str, int]]]]]], loop: bool = False, start: Optional[str] = None) -> None:
1958	def form (
1959		self,
1960		sections: typing.Union[
1961			typing.List[typing.Tuple[str, int]],
1962			typing.Iterator[typing.Tuple[str, int]],
1963			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
1964		],
1965		loop: bool = False,
1966		start: typing.Optional[str] = None
1967	) -> None:
1968
1969		"""
1970		Define the structure (sections) of the composition.
1971
1972		You can define form in three ways:
1973		1. **Graph (Dict)**: Dynamic transitions based on weights.
1974		2. **Sequence (List)**: A fixed order of sections.
1975		3. **Generator**: A Python generator that yields `(name, bars)` pairs.
1976
1977		Parameters:
1978			sections: The form definition (Dict, List, or Generator).
1979			loop: Whether to cycle back to the start (List mode only).
1980			start: The section to start with (Graph mode only).
1981
1982		Example:
1983			```python
1984			# A simple pop structure
1985			comp.form([
1986				("verse", 8),
1987				("chorus", 8),
1988				("verse", 8),
1989				("chorus", 16)
1990			])
1991			```
1992		"""
1993
1994		self._form_state = subsequence.form_state.FormState(sections, loop=loop, start=start)

Define the structure (sections) of the composition.

You can define form in three ways:

  1. Graph (Dict): Dynamic transitions based on weights.
  2. Sequence (List): A fixed order of sections.
  3. Generator: A Python generator that yields (name, bars) pairs.
Arguments:
  • sections: The form definition (Dict, List, or Generator).
  • loop: Whether to cycle back to the start (List mode only).
  • start: The section to start with (Graph mode only).
Example:
# A simple pop structure
comp.form([
        ("verse", 8),
        ("chorus", 8),
        ("verse", 8),
        ("chorus", 16)
])
def pattern( self, channel: int, beats: Optional[float] = None, bars: Optional[float] = None, steps: Optional[float] = None, unit: Optional[float] = None, drum_note_map: Optional[Dict[str, int]] = None, cc_name_map: Optional[Dict[str, int]] = None, reschedule_lookahead: float = 1, voice_leading: bool = False, device: Union[int, str, NoneType] = None) -> Callable:
2047	def pattern (
2048		self,
2049		channel: int,
2050		beats: typing.Optional[float] = None,
2051		bars: typing.Optional[float] = None,
2052		steps: typing.Optional[float] = None,
2053		unit: typing.Optional[float] = None,
2054		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2055		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
2056		reschedule_lookahead: float = 1,
2057		voice_leading: bool = False,
2058		device: subsequence.midi_utils.DeviceId = None,
2059	) -> typing.Callable:
2060
2061		"""
2062		Register a function as a repeating MIDI pattern.
2063
2064		The decorated function will be called once per cycle to 'rebuild' its
2065		content. This allows for generative logic that evolves over time.
2066
2067		Two ways to specify pattern length:
2068
2069		- **Duration mode** (default): use ``beats=`` or ``bars=``.
2070		  The grid defaults to sixteenth-note resolution.
2071		- **Step mode**: use ``steps=`` paired with ``unit=``.
2072		  The grid equals the step count, so ``p.hit_steps()`` indices map
2073		  directly to steps.
2074
2075		Parameters:
2076			channel: MIDI channel. By default uses 0-based numbering (0-15)
2077				matching the raw MIDI protocol. Set
2078				``zero_indexed_channels=False`` on the ``Composition`` to use
2079				1-based numbering (1-16) instead.
2080			beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar.
2081			bars: Duration in bars (4 beats each, assumes 4/4). ``bars=2`` = 8 beats.
2082			steps: Step count for step mode. Requires ``unit=``.
2083			unit: Duration of one step in beats (e.g. ``dur.SIXTEENTH``).
2084				Requires ``steps=``.
2085			drum_note_map: Optional mapping for drum instruments.
2086			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
2087				Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``.
2088			reschedule_lookahead: Beats in advance to compute the next cycle.
2089			voice_leading: If True, chords in this pattern will automatically
2090				use inversions that minimize voice movement.
2091
2092		Example:
2093			```python
2094			@comp.pattern(channel=1, beats=4)
2095			def chords (p):
2096				p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)
2097
2098			@comp.pattern(channel=1, bars=2)
2099			def long_phrase (p):
2100				...
2101
2102			@comp.pattern(channel=1, steps=6, unit=dur.SIXTEENTH)
2103			def riff (p):
2104				p.sequence(steps=[0, 1, 3, 5], pitches=60)
2105			```
2106		"""
2107
2108		channel = self._resolve_channel(channel)
2109
2110		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit)
2111
2112		# Resolve device string name to index if possible now; otherwise store
2113		# the raw DeviceId and resolve it in _run() once all devices are open.
2114		resolved_device: subsequence.midi_utils.DeviceId = device
2115
2116		def decorator (fn: typing.Callable) -> typing.Callable:
2117
2118			"""
2119			Wrap the builder function and register it as a pending pattern.
2120			During live sessions, hot-swap an existing pattern's builder instead.
2121			"""
2122
2123			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
2124			if self._is_live and fn.__name__ in self._running_patterns:
2125				running = self._running_patterns[fn.__name__]
2126				running._builder_fn = fn
2127				running._wants_chord = _fn_has_parameter(fn, "chord")
2128				logger.info(f"Hot-swapped pattern: {fn.__name__}")
2129				return fn
2130
2131			pending = _PendingPattern(
2132				builder_fn = fn,
2133				channel = channel,  # already resolved to 0-indexed
2134				length = beat_length,
2135				default_grid = default_grid,
2136				drum_note_map = drum_note_map,
2137				cc_name_map = cc_name_map,
2138				reschedule_lookahead = reschedule_lookahead,
2139				voice_leading = voice_leading,
2140				# For int/None: resolve immediately.  For str: store 0 as
2141				# placeholder; _resolve_pending_devices() fixes it in _run().
2142				device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device,
2143				raw_device = resolved_device,
2144			)
2145
2146			self._pending_patterns.append(pending)
2147
2148			return fn
2149
2150		return decorator

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= or bars=. The grid defaults to sixteenth-note resolution.
  • Step mode: use steps= paired with unit=. The grid equals the step count, so p.hit_steps() indices map directly to steps.
Arguments:
  • channel: MIDI channel. By default uses 0-based numbering (0-15) matching the raw MIDI protocol. Set zero_indexed_channels=False on the Composition to use 1-based numbering (1-16) instead.
  • beats: Duration in beats (quarter notes). beats=4 = 1 bar.
  • bars: Duration in bars (4 beats each, assumes 4/4). bars=2 = 8 beats.
  • steps: Step count for step mode. Requires unit=.
  • unit: Duration of one step in beats (e.g. dur.SIXTEENTH). Requires steps=.
  • 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() and p.cc_ramp().
  • 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.
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, unit=dur.SIXTEENTH)
def riff (p):
        p.sequence(steps=[0, 1, 3, 5], pitches=60)
def layer( self, *builder_fns: Callable, channel: int, beats: Optional[float] = None, bars: Optional[float] = None, steps: Optional[float] = None, unit: Optional[float] = None, drum_note_map: Optional[Dict[str, int]] = None, cc_name_map: Optional[Dict[str, int]] = None, reschedule_lookahead: float = 1, voice_leading: bool = False, device: Union[int, str, NoneType] = None) -> None:
2152	def layer (
2153		self,
2154		*builder_fns: typing.Callable,
2155		channel: int,
2156		beats: typing.Optional[float] = None,
2157		bars: typing.Optional[float] = None,
2158		steps: typing.Optional[float] = None,
2159		unit: typing.Optional[float] = None,
2160		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2161		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
2162		reschedule_lookahead: float = 1,
2163		voice_leading: bool = False,
2164		device: subsequence.midi_utils.DeviceId = None,
2165	) -> None:
2166
2167		"""
2168		Combine multiple functions into a single MIDI pattern.
2169
2170		This is useful for composing complex patterns out of reusable
2171		building blocks (e.g., a 'kick' function and a 'snare' function).
2172
2173		See ``pattern()`` for the full description of ``beats``, ``bars``,
2174		``steps``, and ``unit``.
2175
2176		Parameters:
2177			builder_fns: One or more pattern builder functions.
2178			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
2179			beats: Duration in beats (quarter notes).
2180			bars: Duration in bars (4 beats each, assumes 4/4).
2181			steps: Step count for step mode. Requires ``unit=``.
2182			unit: Duration of one step in beats. Requires ``steps=``.
2183			drum_note_map: Optional mapping for drum instruments.
2184			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
2185			reschedule_lookahead: Beats in advance to compute the next cycle.
2186			voice_leading: If True, chords use smooth voice leading.
2187		"""
2188
2189		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit)
2190
2191		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
2192
2193		if wants_chord:
2194
2195			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
2196
2197				for fn in builder_fns:
2198					if _fn_has_parameter(fn, "chord"):
2199						fn(p, chord)
2200					else:
2201						fn(p)
2202
2203		else:
2204
2205			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
2206
2207				for fn in builder_fns:
2208					fn(p)
2209
2210		resolved = self._resolve_channel(channel)
2211
2212		pending = _PendingPattern(
2213			builder_fn = merged_builder,
2214			channel = resolved,  # already resolved to 0-indexed
2215			length = beat_length,
2216			default_grid = default_grid,
2217			drum_note_map = drum_note_map,
2218			cc_name_map = cc_name_map,
2219			reschedule_lookahead = reschedule_lookahead,
2220			voice_leading = voice_leading,
2221				device = 0 if (device is None or isinstance(device, str)) else device,
2222			raw_device = device,
2223		)
2224
2225		self._pending_patterns.append(pending)

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

Arguments:
  • builder_fns: One or more pattern builder functions.
  • channel: MIDI channel (0-15, or 1-16 with zero_indexed_channels=False).
  • beats: Duration in beats (quarter notes).
  • bars: Duration in bars (4 beats each, assumes 4/4).
  • steps: Step count for step mode. Requires unit=.
  • unit: 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.
  • reschedule_lookahead: Beats in advance to compute the next cycle.
  • voice_leading: If True, chords use smooth voice leading.
def trigger( self, fn: Callable, channel: int, beats: Optional[float] = None, bars: Optional[float] = None, steps: Optional[float] = None, unit: Optional[float] = None, quantize: float = 0, drum_note_map: Optional[Dict[str, int]] = None, cc_name_map: Optional[Dict[str, int]] = None, chord: bool = False, device: Union[int, str, NoneType] = None) -> None:
2227	def trigger (
2228		self,
2229		fn: typing.Callable,
2230		channel: int,
2231		beats: typing.Optional[float] = None,
2232		bars: typing.Optional[float] = None,
2233		steps: typing.Optional[float] = None,
2234		unit: typing.Optional[float] = None,
2235		quantize: float = 0,
2236		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2237		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
2238		chord: bool = False,
2239		device: subsequence.midi_utils.DeviceId = None,
2240	) -> None:
2241
2242		"""
2243		Trigger a one-shot pattern immediately or on a quantized boundary.
2244
2245		This is useful for real-time response to sensors, OSC messages, or other
2246		external events. The builder function is called immediately with a fresh
2247		PatternBuilder, and the generated events are injected into the queue at
2248		the specified quantize boundary.
2249
2250		The builder function has the same API as a ``@composition.pattern``
2251		decorated function and can use all PatternBuilder methods: ``p.note()``,
2252		``p.euclidean()``, ``p.arpeggio()``, and so on.
2253
2254		See ``pattern()`` for the full description of ``beats``, ``bars``,
2255		``steps``, and ``unit``. Default is 1 beat.
2256
2257		Parameters:
2258			fn: The pattern builder function (same signature as ``@comp.pattern``).
2259			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
2260			beats: Duration in beats (quarter notes, default 1).
2261			bars: Duration in bars (4 beats each, assumes 4/4).
2262			steps: Step count for step mode. Requires ``unit=``.
2263			unit: Duration of one step in beats. Requires ``steps=``.
2264			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
2265				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
2266				constants from ``subsequence.constants.durations``.
2267			drum_note_map: Optional drum name mapping for this pattern.
2268			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
2269			chord: If ``True``, the builder function receives the current chord as
2270				a second parameter (same as ``@composition.pattern``).
2271
2272		Example:
2273			```python
2274			# Immediate single note
2275			composition.trigger(
2276				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
2277				channel=0
2278			)
2279
2280			# Quantized fill (next bar)
2281			import subsequence.constants.durations as dur
2282			composition.trigger(
2283				lambda p: p.euclidean("snare", pulses=7, velocity=90),
2284				channel=9,
2285				drum_note_map=gm_drums.GM_DRUM_MAP,
2286				quantize=dur.WHOLE
2287			)
2288
2289			# With chord context
2290			composition.trigger(
2291				lambda p: p.arpeggio(p.chord.tones(root=60), spacing=dur.SIXTEENTH),
2292				channel=0,
2293				quantize=dur.QUARTER,
2294				chord=True
2295			)
2296			```
2297		"""
2298
2299		# Resolve channel numbering
2300		resolved_channel = self._resolve_channel(channel)
2301
2302		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit, default=1.0)
2303
2304		# Resolve device index
2305		resolved_device_idx = self._resolve_device_id(device)
2306
2307		# Create a temporary Pattern
2308		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx)
2309
2310		# Create a PatternBuilder
2311		builder = subsequence.pattern_builder.PatternBuilder(
2312			pattern=pattern,
2313			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
2314			drum_note_map=drum_note_map,
2315			cc_name_map=cc_name_map,
2316			section=self._form_state.get_section_info() if self._form_state else None,
2317			bar=self._builder_bar,
2318			conductor=self.conductor,
2319			rng=random.Random(),  # Fresh random state for each trigger
2320			tweaks={},
2321			default_grid=default_grid,
2322			data=self.data
2323		)
2324
2325		# Call the builder function
2326		try:
2327
2328			if chord and self._harmonic_state is not None:
2329				current_chord = self._harmonic_state.get_current_chord()
2330				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
2331				fn(builder, injected)
2332
2333			else:
2334				fn(builder)
2335
2336		except Exception:
2337			logger.exception("Error in trigger builder — pattern will be silent")
2338			return
2339
2340		# Calculate the start pulse based on quantize
2341		current_pulse = self._sequencer.pulse_count
2342		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
2343
2344		if quantize == 0:
2345			# Immediate: use current pulse
2346			start_pulse = current_pulse
2347
2348		else:
2349			# Quantize to the next multiple of (quantize * pulses_per_beat)
2350			quantize_pulses = int(quantize * pulses_per_beat)
2351			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
2352
2353		# Schedule the pattern for one-shot execution
2354		try:
2355			loop = asyncio.get_running_loop()
2356			# Already on the event loop
2357			asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse))
2358
2359		except RuntimeError:
2360			# Not on the event loop — schedule via call_soon_threadsafe
2361			if self._sequencer._event_loop is not None:
2362				asyncio.run_coroutine_threadsafe(
2363					self._sequencer.schedule_pattern(pattern, start_pulse),
2364					loop=self._sequencer._event_loop
2365				)
2366			else:
2367				logger.warning("trigger() called before playback started; pattern ignored")

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 unit. Default is 1 beat.

Arguments:
  • fn: The pattern builder function (same signature as @comp.pattern).
  • channel: MIDI channel (0-15, or 1-16 with zero_indexed_channels=False).
  • beats: Duration in beats (quarter notes, default 1).
  • bars: Duration in bars (4 beats each, assumes 4/4).
  • steps: Step count for step mode. Requires unit=.
  • unit: 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. Use dur.* constants from subsequence.constants.durations.
  • drum_note_map: Optional drum name mapping for this pattern.
  • cc_name_map: Optional mapping of CC names to MIDI CC numbers.
  • chord: If True, the builder function receives the current chord as a second parameter (same as @composition.pattern).
Example:
# Immediate single note
composition.trigger(
        lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
        channel=0
)

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

# With chord context
composition.trigger(
        lambda p: p.arpeggio(p.chord.tones(root=60), spacing=dur.SIXTEENTH),
        channel=0,
        quantize=dur.QUARTER,
        chord=True
)
is_clock_following: bool
2369	@property
2370	def is_clock_following (self) -> bool:
2371
2372		"""True if either the primary or any additional device is following external clock."""
2373
2374		return self._clock_follow or any(cf for _, _, cf in self._additional_inputs)

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

def play(self) -> None:
2377	def play (self) -> None:
2378
2379		"""
2380		Start the composition.
2381
2382		This call blocks until the program is interrupted (e.g., via Ctrl+C).
2383		It initializes the MIDI hardware, launches the background sequencer,
2384		and begins playback.
2385		"""
2386
2387		try:
2388			asyncio.run(self._run())
2389
2390		except KeyboardInterrupt:
2391			pass

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.

def render( self, bars: Optional[int] = None, filename: str = 'render.mid', max_minutes: Optional[float] = 60.0) -> None:
2394	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
2395
2396		"""Render the composition to a MIDI file without real-time playback.
2397
2398		Runs the sequencer as fast as possible (no timing delays) and stops
2399		when the first active limit is reached.  The result is saved as a
2400		standard MIDI file that can be imported into any DAW.
2401
2402		All patterns, scheduled callbacks, and harmony logic run exactly as
2403		they would during live playback — BPM transitions, generative fills,
2404		and probabilistic gates all work in render mode.  The only difference
2405		is that time is simulated rather than wall-clock driven.
2406
2407		Parameters:
2408			bars: Number of bars to render, or ``None`` for no bar limit
2409			      (default ``None``).  When both *bars* and *max_minutes* are
2410			      active, playback stops at whichever limit is reached first.
2411			filename: Output MIDI filename (default ``"render.mid"``).
2412			max_minutes: Safety cap on the length of rendered MIDI in minutes
2413			             (default ``60.0``).  Pass ``None`` to disable the time
2414			             cap — you must then provide an explicit *bars* value.
2415
2416		Raises:
2417			ValueError: If both *bars* and *max_minutes* are ``None``, which
2418			            would produce an infinite render.
2419
2420		Examples:
2421			```python
2422			# Default: renders up to 60 minutes of MIDI content.
2423			composition.render()
2424
2425			# Render exactly 64 bars (time cap still active as backstop).
2426			composition.render(bars=64, filename="demo.mid")
2427
2428			# Render up to 5 minutes of an infinite generative composition.
2429			composition.render(max_minutes=5, filename="five_min.mid")
2430
2431			# Remove the time cap — must supply bars instead.
2432			composition.render(bars=128, max_minutes=None, filename="long.mid")
2433			```
2434		"""
2435
2436		if bars is None and max_minutes is None:
2437			raise ValueError(
2438				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
2439				"Passing both as None would produce an infinite render."
2440			)
2441
2442		self._sequencer.recording = True
2443		self._sequencer.record_filename = filename
2444		self._sequencer.render_mode = True
2445		self._sequencer.render_bars = bars if bars is not None else 0
2446		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
2447		asyncio.run(self._run())

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.

Arguments:
  • bars: Number of bars to render, or None for no bar limit (default None). 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). Pass None to 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")
@dataclasses.dataclass
class Groove:
 14@dataclasses.dataclass
 15class Groove:
 16
 17	"""
 18	A timing/velocity template applied to quantized grid positions.
 19
 20	A groove is a repeating pattern of per-step timing offsets and optional
 21	velocity adjustments aligned to a rhythmic grid. Apply it as a post-build
 22	transform with ``p.groove(template)`` to give a pattern its characteristic
 23	feel — swing, shuffle, MPC-style pocket, or anything extracted from an
 24	Ableton ``.agr`` file.
 25
 26	Parameters:
 27		offsets: Timing offset per grid slot, in beats. Repeats cyclically.
 28			Positive values delay the note; negative values push it earlier.
 29		grid: Grid size in beats (0.25 = 16th notes, 0.5 = 8th notes).
 30		velocities: Optional velocity scale per grid slot (1.0 = unchanged).
 31			Repeats cyclically alongside offsets.
 32
 33	Example::
 34
 35		# Ableton-style 57% swing on 16th notes
 36		groove = Groove.swing(percent=57)
 37
 38		# Custom groove with timing and velocity
 39		groove = Groove(
 40			grid=0.25,
 41			offsets=[0.0, +0.02, 0.0, -0.01],
 42			velocities=[1.0, 0.7, 0.9, 0.6],
 43		)
 44	"""
 45
 46	offsets: typing.List[float]
 47	grid: float = 0.25
 48	velocities: typing.Optional[typing.List[float]] = None
 49
 50	def __post_init__ (self) -> None:
 51		if not self.offsets:
 52			raise ValueError("offsets must not be empty")
 53		if self.grid <= 0:
 54			raise ValueError("grid must be positive")
 55		if self.velocities is not None and not self.velocities:
 56			raise ValueError("velocities must not be empty (use None for no velocity adjustment)")
 57
 58	@staticmethod
 59	def swing (percent: float = 57.0, grid: float = 0.25) -> "Groove":
 60
 61		"""
 62		Create a swing groove from a percentage.
 63
 64		50% is straight (no swing). 67% is approximately triplet swing.
 65		57% is a moderate shuffle — the Ableton default.
 66
 67		Parameters:
 68			percent: Swing amount (50–75 is the useful range).
 69			grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths).
 70		"""
 71
 72		if percent < 50.0 or percent > 99.0:
 73			raise ValueError("swing percent must be between 50 and 99")
 74		pair_duration = grid * 2
 75		offset = (percent / 100.0 - 0.5) * pair_duration
 76		return Groove(offsets=[0.0, offset], grid=grid)
 77
 78	@staticmethod
 79	def from_agr (path: str) -> "Groove":
 80
 81		"""
 82		Import timing and velocity data from an Ableton .agr groove file.
 83
 84		An ``.agr`` file is an XML document containing a MIDI clip whose
 85		note positions encode the groove's rhythmic feel. This method reads
 86		those note start times and velocities and converts them into the
 87		``Groove`` dataclass format (per-step offsets and velocity scales).
 88
 89		**What is extracted:**
 90
 91		- ``Time`` attribute of each ``MidiNoteEvent`` → timing offsets
 92		  relative to ideal grid positions.
 93		- ``Velocity`` attribute of each ``MidiNoteEvent`` → velocity
 94		  scaling (normalised to the highest velocity in the file).
 95		- ``TimingAmount`` from the Groove element → pre-scales the timing
 96		  offsets (100 = full, 70 = 70% of the groove's timing).
 97		- ``VelocityAmount`` from the Groove element → pre-scales velocity
 98		  deviation (100 = full groove velocity, 0 = no velocity changes).
 99
100		The resulting ``Groove`` reflects the file author's intended
101		strength. Use ``strength=`` when applying to further adjust.
102
103		**What is NOT imported:**
104
105		``RandomAmount`` (use ``p.randomize()`` separately for random
106		jitter) and ``QuantizationAmount`` (not applicable - Subsequence
107		notes are already grid-quantized by construction).
108
109		Other ``MidiNoteEvent`` fields (``Duration``, ``VelocityDeviation``,
110		``OffVelocity``, ``Probability``) are also ignored.
111
112		Parameters:
113			path: Path to the .agr file.
114		"""
115
116		tree = xml.etree.ElementTree.parse(path)
117		root = tree.getroot()
118
119		# Find the MIDI clip
120		clip = root.find(".//MidiClip")
121		if clip is None:
122			raise ValueError(f"No MidiClip found in {path}")
123
124		# Get clip length
125		current_end = clip.find("CurrentEnd")
126		if current_end is None:
127			raise ValueError(f"No CurrentEnd found in {path}")
128		clip_length = float(current_end.get("Value", "4"))
129
130		# Read Groove Pool blend parameters
131		groove_elem = root.find(".//Groove")
132		timing_amount = 100.0
133		velocity_amount = 100.0
134		if groove_elem is not None:
135			timing_el = groove_elem.find("TimingAmount")
136			if timing_el is not None:
137				timing_amount = float(timing_el.get("Value", "100"))
138			velocity_el = groove_elem.find("VelocityAmount")
139			if velocity_el is not None:
140				velocity_amount = float(velocity_el.get("Value", "100"))
141
142		timing_scale = timing_amount / 100.0
143		velocity_scale = velocity_amount / 100.0
144
145		# Extract note events sorted by time
146		events = clip.findall(".//MidiNoteEvent")
147		if not events:
148			raise ValueError(f"No MidiNoteEvent elements found in {path}")
149
150		times: typing.List[float] = []
151		velocities_raw: typing.List[float] = []
152		for event in events:
153			times.append(float(event.get("Time", "0")))
154			velocities_raw.append(float(event.get("Velocity", "127")))
155
156		times.sort()
157		note_count = len(times)
158
159		# Infer grid from clip length and note count
160		grid = clip_length / note_count
161
162		# Calculate offsets from ideal grid positions, scaled by TimingAmount
163		offsets: typing.List[float] = []
164		for i, time in enumerate(times):
165			ideal = i * grid
166			offsets.append((time - ideal) * timing_scale)
167
168		# Calculate velocity scales (relative to max velocity in the file),
169		# blended toward 1.0 by VelocityAmount
170		max_vel = max(velocities_raw)
171		has_velocity_variation = any(v != max_vel for v in velocities_raw)
172		groove_velocities: typing.Optional[typing.List[float]] = None
173		if has_velocity_variation and max_vel > 0:
174			raw_scales = [v / max_vel for v in velocities_raw]
175			# velocity_scale=1.0 → full groove velocity; 0.0 → all 1.0 (no change)
176			groove_velocities = [1.0 + (s - 1.0) * velocity_scale for s in raw_scales]
177			# If blending has removed all variation, set to None
178			if all(abs(v - 1.0) < 1e-9 for v in groove_velocities):
179				groove_velocities = None
180
181		return Groove(offsets=offsets, grid=grid, velocities=groove_velocities)

A timing/velocity template applied to quantized grid positions.

A groove is a repeating pattern of per-step timing offsets and optional velocity adjustments aligned to a rhythmic grid. Apply it as a post-build transform with p.groove(template) to give a pattern its characteristic feel — swing, shuffle, MPC-style pocket, or anything extracted from an Ableton .agr file.

Arguments:
  • offsets: Timing offset per grid slot, in beats. Repeats cyclically. Positive values delay the note; negative values push it earlier.
  • grid: Grid size in beats (0.25 = 16th notes, 0.5 = 8th notes).
  • velocities: Optional velocity scale per grid slot (1.0 = unchanged). Repeats cyclically alongside offsets.

Example::

    # Ableton-style 57% swing on 16th notes
    groove = Groove.swing(percent=57)

    # Custom groove with timing and velocity
    groove = Groove(
            grid=0.25,
            offsets=[0.0, +0.02, 0.0, -0.01],
            velocities=[1.0, 0.7, 0.9, 0.6],
    )
Groove( offsets: List[float], grid: float = 0.25, velocities: Optional[List[float]] = None)
offsets: List[float]
grid: float = 0.25
velocities: Optional[List[float]] = None
@staticmethod
def swing(percent: float = 57.0, grid: float = 0.25) -> Groove:
58	@staticmethod
59	def swing (percent: float = 57.0, grid: float = 0.25) -> "Groove":
60
61		"""
62		Create a swing groove from a percentage.
63
64		50% is straight (no swing). 67% is approximately triplet swing.
65		57% is a moderate shuffle — the Ableton default.
66
67		Parameters:
68			percent: Swing amount (50–75 is the useful range).
69			grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths).
70		"""
71
72		if percent < 50.0 or percent > 99.0:
73			raise ValueError("swing percent must be between 50 and 99")
74		pair_duration = grid * 2
75		offset = (percent / 100.0 - 0.5) * pair_duration
76		return Groove(offsets=[0.0, offset], grid=grid)

Create a swing groove from a percentage.

50% is straight (no swing). 67% is approximately triplet swing. 57% is a moderate shuffle — the Ableton default.

Arguments:
  • percent: Swing amount (50–75 is the useful range).
  • grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths).
@staticmethod
def from_agr(path: str) -> Groove:
 78	@staticmethod
 79	def from_agr (path: str) -> "Groove":
 80
 81		"""
 82		Import timing and velocity data from an Ableton .agr groove file.
 83
 84		An ``.agr`` file is an XML document containing a MIDI clip whose
 85		note positions encode the groove's rhythmic feel. This method reads
 86		those note start times and velocities and converts them into the
 87		``Groove`` dataclass format (per-step offsets and velocity scales).
 88
 89		**What is extracted:**
 90
 91		- ``Time`` attribute of each ``MidiNoteEvent`` → timing offsets
 92		  relative to ideal grid positions.
 93		- ``Velocity`` attribute of each ``MidiNoteEvent`` → velocity
 94		  scaling (normalised to the highest velocity in the file).
 95		- ``TimingAmount`` from the Groove element → pre-scales the timing
 96		  offsets (100 = full, 70 = 70% of the groove's timing).
 97		- ``VelocityAmount`` from the Groove element → pre-scales velocity
 98		  deviation (100 = full groove velocity, 0 = no velocity changes).
 99
100		The resulting ``Groove`` reflects the file author's intended
101		strength. Use ``strength=`` when applying to further adjust.
102
103		**What is NOT imported:**
104
105		``RandomAmount`` (use ``p.randomize()`` separately for random
106		jitter) and ``QuantizationAmount`` (not applicable - Subsequence
107		notes are already grid-quantized by construction).
108
109		Other ``MidiNoteEvent`` fields (``Duration``, ``VelocityDeviation``,
110		``OffVelocity``, ``Probability``) are also ignored.
111
112		Parameters:
113			path: Path to the .agr file.
114		"""
115
116		tree = xml.etree.ElementTree.parse(path)
117		root = tree.getroot()
118
119		# Find the MIDI clip
120		clip = root.find(".//MidiClip")
121		if clip is None:
122			raise ValueError(f"No MidiClip found in {path}")
123
124		# Get clip length
125		current_end = clip.find("CurrentEnd")
126		if current_end is None:
127			raise ValueError(f"No CurrentEnd found in {path}")
128		clip_length = float(current_end.get("Value", "4"))
129
130		# Read Groove Pool blend parameters
131		groove_elem = root.find(".//Groove")
132		timing_amount = 100.0
133		velocity_amount = 100.0
134		if groove_elem is not None:
135			timing_el = groove_elem.find("TimingAmount")
136			if timing_el is not None:
137				timing_amount = float(timing_el.get("Value", "100"))
138			velocity_el = groove_elem.find("VelocityAmount")
139			if velocity_el is not None:
140				velocity_amount = float(velocity_el.get("Value", "100"))
141
142		timing_scale = timing_amount / 100.0
143		velocity_scale = velocity_amount / 100.0
144
145		# Extract note events sorted by time
146		events = clip.findall(".//MidiNoteEvent")
147		if not events:
148			raise ValueError(f"No MidiNoteEvent elements found in {path}")
149
150		times: typing.List[float] = []
151		velocities_raw: typing.List[float] = []
152		for event in events:
153			times.append(float(event.get("Time", "0")))
154			velocities_raw.append(float(event.get("Velocity", "127")))
155
156		times.sort()
157		note_count = len(times)
158
159		# Infer grid from clip length and note count
160		grid = clip_length / note_count
161
162		# Calculate offsets from ideal grid positions, scaled by TimingAmount
163		offsets: typing.List[float] = []
164		for i, time in enumerate(times):
165			ideal = i * grid
166			offsets.append((time - ideal) * timing_scale)
167
168		# Calculate velocity scales (relative to max velocity in the file),
169		# blended toward 1.0 by VelocityAmount
170		max_vel = max(velocities_raw)
171		has_velocity_variation = any(v != max_vel for v in velocities_raw)
172		groove_velocities: typing.Optional[typing.List[float]] = None
173		if has_velocity_variation and max_vel > 0:
174			raw_scales = [v / max_vel for v in velocities_raw]
175			# velocity_scale=1.0 → full groove velocity; 0.0 → all 1.0 (no change)
176			groove_velocities = [1.0 + (s - 1.0) * velocity_scale for s in raw_scales]
177			# If blending has removed all variation, set to None
178			if all(abs(v - 1.0) < 1e-9 for v in groove_velocities):
179				groove_velocities = None
180
181		return Groove(offsets=offsets, grid=grid, velocities=groove_velocities)

Import timing and velocity data from an Ableton .agr groove file.

An .agr file is an XML document containing a MIDI clip whose note positions encode the groove's rhythmic feel. This method reads those note start times and velocities and converts them into the Groove dataclass format (per-step offsets and velocity scales).

What is extracted:

  • Time attribute of each MidiNoteEvent → timing offsets relative to ideal grid positions.
  • Velocity attribute of each MidiNoteEvent → velocity scaling (normalised to the highest velocity in the file).
  • TimingAmount from the Groove element → pre-scales the timing offsets (100 = full, 70 = 70% of the groove's timing).
  • VelocityAmount from the Groove element → pre-scales velocity deviation (100 = full groove velocity, 0 = no velocity changes).

The resulting Groove reflects the file author's intended strength. Use strength= when applying to further adjust.

What is NOT imported:

RandomAmount (use p.randomize() separately for random jitter) and QuantizationAmount (not applicable - Subsequence notes are already grid-quantized by construction).

Other MidiNoteEvent fields (Duration, VelocityDeviation, OffVelocity, Probability) are also ignored.

Arguments:
  • path: Path to the .agr file.
class MelodicState:
 24class MelodicState:
 25
 26	"""Persistent melodic context that applies NIR scoring to single-note lines."""
 27
 28
 29	def __init__ (
 30		self,
 31		key: str = "C",
 32		mode: str = "ionian",
 33		low: int = 48,
 34		high: int = 72,
 35		nir_strength: float = 0.5,
 36		chord_weight: float = 0.4,
 37		rest_probability: float = 0.0,
 38		pitch_diversity: float = 0.6,
 39	) -> None:
 40
 41		"""Initialise a melodic state for a given key, mode, and MIDI register.
 42
 43		Parameters:
 44			key: Root note of the key (e.g. ``"C"``, ``"F#"``, ``"Bb"``).
 45			mode: Scale mode name.  Accepts any mode registered with
 46			      :func:`~subsequence.intervals.scale_pitch_classes` (e.g.
 47			      ``"ionian"``, ``"aeolian"``, ``"dorian"``).
 48			low: Lowest MIDI note (inclusive) in the pitch pool.
 49			high: Highest MIDI note (inclusive) in the pitch pool.
 50			nir_strength: 0.0–1.0.  Scales how strongly the NIR rules
 51			    influence candidate scores.  0.0 = uniform; 1.0 = full boost.
 52			chord_weight: 0.0–1.0.  Additive multiplier bonus for candidates
 53			    whose pitch class belongs to the current chord tones.
 54			rest_probability: 0.0–1.0.  Probability of producing a rest
 55			    (returning ``None``) at any given step.
 56			pitch_diversity: 0.0–1.0.  Exponential penalty per recent
 57			    repetition of the same pitch.  Lower values discourage
 58			    repetition more aggressively.
 59		"""
 60
 61		self.key = key
 62		self.mode = mode
 63		self.low = low
 64		self.high = high
 65		self.nir_strength = nir_strength
 66		self.chord_weight = chord_weight
 67		self.rest_probability = rest_probability
 68		self.pitch_diversity = pitch_diversity
 69
 70		key_pc = subsequence.chords.key_name_to_pc(key)
 71
 72		# Pitch pool: all scale tones within [low, high].
 73		self._pitch_pool: typing.List[int] = subsequence.intervals.scale_notes(
 74			key, mode, low=low, high=high
 75		)
 76
 77		# Tonic pitch class for Rule C (closure).
 78		self._tonic_pc: int = key_pc
 79
 80		# History of last N absolute MIDI pitches (capped at 4, same as HarmonicState).
 81		self.history: typing.List[int] = []
 82
 83
 84	def choose_next (
 85		self,
 86		chord_tones: typing.Optional[typing.List[int]],
 87		rng: random.Random,
 88	) -> typing.Optional[int]:
 89
 90		"""Score all pitch-pool candidates and return the chosen pitch, or None for a rest."""
 91
 92		if self.rest_probability > 0.0 and rng.random() < self.rest_probability:
 93			return None
 94
 95		if not self._pitch_pool:
 96			return None
 97
 98		# Resolve chord tones to pitch classes for fast membership testing.
 99		chord_tone_pcs: typing.Set[int] = (
100			{t % 12 for t in chord_tones} if chord_tones else set()
101		)
102
103		scores = [self._score_candidate(p, chord_tone_pcs) for p in self._pitch_pool]
104
105		# Weighted random choice: select using cumulative score as a probability weight.
106		total = sum(scores)
107
108		if total <= 0.0:
109			chosen = rng.choice(self._pitch_pool)
110
111		else:
112			r = rng.uniform(0.0, total)
113			cumulative = 0.0
114			chosen = self._pitch_pool[-1]
115
116			for pitch, score in zip(self._pitch_pool, scores):
117				cumulative += score
118				if r <= cumulative:
119					chosen = pitch
120					break
121
122		# Persist history for the next call (capped at 4 entries).
123		self.history.append(chosen)
124		if len(self.history) > 4:
125			self.history.pop(0)
126
127		return chosen
128
129
130	def _score_candidate (
131		self,
132		candidate: int,
133		chord_tone_pcs: typing.Set[int],
134	) -> float:
135
136		"""Score one candidate pitch using NIR rules, chord weighting, range gravity, and pitch diversity."""
137
138		score = 1.0
139
140		# --- NIR rules (require at least one history note for Realization) ---
141		if self.history:
142			last_note = self.history[-1]
143
144			target_diff = candidate - last_note
145			target_interval = abs(target_diff)
146			target_direction = 1 if target_diff > 0 else -1 if target_diff < 0 else 0
147
148			# Rules A & B require an Implication context (prev -> last -> candidate).
149			if len(self.history) >= 2:
150				prev_note = self.history[-2]
151
152				prev_diff = last_note - prev_note
153				prev_interval = abs(prev_diff)
154				prev_direction = 1 if prev_diff > 0 else -1 if prev_diff < 0 else 0
155
156				# Rule A: Reversal (gap fill) — after a large leap, expect direction change.
157				if prev_interval > 4:
158					if target_direction != prev_direction and target_direction != 0:
159						score += 0.5
160
161					if target_interval < 4:
162						score += 0.3
163
164				# Rule B: Process (continuation) — after a small step, expect more of the same.
165				elif 0 < prev_interval < 3:
166					if target_direction == prev_direction:
167						score += 0.4
168
169					if abs(target_interval - prev_interval) <= 1:
170						score += 0.2
171
172			# Rule C: Closure — the tonic is a cognitively stable landing point.
173			if candidate % 12 == self._tonic_pc:
174				score += 0.2
175
176			# Rule D: Proximity — smaller intervals are generally preferred.
177			if 0 < target_interval <= 3:
178				score += 0.3
179
180			# Scale the entire NIR boost by nir_strength, leaving the base at 1.0.
181			score = 1.0 + (score - 1.0) * self.nir_strength
182
183		# --- Chord tone boost ---
184		if candidate % 12 in chord_tone_pcs:
185			score *= 1.0 + self.chord_weight
186
187		# --- Range gravity: penalise notes far from the centre of [low, high] ---
188		centre = (self.low + self.high) / 2.0
189		half_range = max(1.0, (self.high - self.low) / 2.0)
190		distance_ratio = abs(candidate - centre) / half_range
191		score *= 1.0 - 0.3 * (distance_ratio ** 2)
192
193		# --- Pitch diversity: exponential penalty for recently-heard pitches ---
194		recent_occurrences = sum(1 for h in self.history if h == candidate)
195		score *= self.pitch_diversity ** recent_occurrences
196
197		return max(0.0, score)

Persistent melodic context that applies NIR scoring to single-note lines.

MelodicState( key: str = 'C', mode: str = 'ionian', low: int = 48, high: int = 72, nir_strength: float = 0.5, chord_weight: float = 0.4, rest_probability: float = 0.0, pitch_diversity: float = 0.6)
29	def __init__ (
30		self,
31		key: str = "C",
32		mode: str = "ionian",
33		low: int = 48,
34		high: int = 72,
35		nir_strength: float = 0.5,
36		chord_weight: float = 0.4,
37		rest_probability: float = 0.0,
38		pitch_diversity: float = 0.6,
39	) -> None:
40
41		"""Initialise a melodic state for a given key, mode, and MIDI register.
42
43		Parameters:
44			key: Root note of the key (e.g. ``"C"``, ``"F#"``, ``"Bb"``).
45			mode: Scale mode name.  Accepts any mode registered with
46			      :func:`~subsequence.intervals.scale_pitch_classes` (e.g.
47			      ``"ionian"``, ``"aeolian"``, ``"dorian"``).
48			low: Lowest MIDI note (inclusive) in the pitch pool.
49			high: Highest MIDI note (inclusive) in the pitch pool.
50			nir_strength: 0.0–1.0.  Scales how strongly the NIR rules
51			    influence candidate scores.  0.0 = uniform; 1.0 = full boost.
52			chord_weight: 0.0–1.0.  Additive multiplier bonus for candidates
53			    whose pitch class belongs to the current chord tones.
54			rest_probability: 0.0–1.0.  Probability of producing a rest
55			    (returning ``None``) at any given step.
56			pitch_diversity: 0.0–1.0.  Exponential penalty per recent
57			    repetition of the same pitch.  Lower values discourage
58			    repetition more aggressively.
59		"""
60
61		self.key = key
62		self.mode = mode
63		self.low = low
64		self.high = high
65		self.nir_strength = nir_strength
66		self.chord_weight = chord_weight
67		self.rest_probability = rest_probability
68		self.pitch_diversity = pitch_diversity
69
70		key_pc = subsequence.chords.key_name_to_pc(key)
71
72		# Pitch pool: all scale tones within [low, high].
73		self._pitch_pool: typing.List[int] = subsequence.intervals.scale_notes(
74			key, mode, low=low, high=high
75		)
76
77		# Tonic pitch class for Rule C (closure).
78		self._tonic_pc: int = key_pc
79
80		# History of last N absolute MIDI pitches (capped at 4, same as HarmonicState).
81		self.history: typing.List[int] = []

Initialise a melodic state for a given key, mode, and MIDI register.

Arguments:
  • key: Root note of the key (e.g. "C", "F#", "Bb").
  • mode: Scale mode name. Accepts any mode registered with ~subsequence.intervals.scale_pitch_classes() (e.g. "ionian", "aeolian", "dorian").
  • low: Lowest MIDI note (inclusive) in the pitch pool.
  • high: Highest MIDI note (inclusive) in the pitch pool.
  • nir_strength: 0.0–1.0. Scales how strongly the NIR rules influence candidate scores. 0.0 = uniform; 1.0 = full boost.
  • chord_weight: 0.0–1.0. Additive multiplier bonus for candidates whose pitch class belongs to the current chord tones.
  • rest_probability: 0.0–1.0. Probability of producing a rest (returning None) at any given step.
  • pitch_diversity: 0.0–1.0. Exponential penalty per recent repetition of the same pitch. Lower values discourage repetition more aggressively.
key
mode
low
high
nir_strength
chord_weight
rest_probability
pitch_diversity
history: List[int]
def choose_next( self, chord_tones: Optional[List[int]], rng: random.Random) -> Optional[int]:
 84	def choose_next (
 85		self,
 86		chord_tones: typing.Optional[typing.List[int]],
 87		rng: random.Random,
 88	) -> typing.Optional[int]:
 89
 90		"""Score all pitch-pool candidates and return the chosen pitch, or None for a rest."""
 91
 92		if self.rest_probability > 0.0 and rng.random() < self.rest_probability:
 93			return None
 94
 95		if not self._pitch_pool:
 96			return None
 97
 98		# Resolve chord tones to pitch classes for fast membership testing.
 99		chord_tone_pcs: typing.Set[int] = (
100			{t % 12 for t in chord_tones} if chord_tones else set()
101		)
102
103		scores = [self._score_candidate(p, chord_tone_pcs) for p in self._pitch_pool]
104
105		# Weighted random choice: select using cumulative score as a probability weight.
106		total = sum(scores)
107
108		if total <= 0.0:
109			chosen = rng.choice(self._pitch_pool)
110
111		else:
112			r = rng.uniform(0.0, total)
113			cumulative = 0.0
114			chosen = self._pitch_pool[-1]
115
116			for pitch, score in zip(self._pitch_pool, scores):
117				cumulative += score
118				if r <= cumulative:
119					chosen = pitch
120					break
121
122		# Persist history for the next call (capped at 4 entries).
123		self.history.append(chosen)
124		if len(self.history) > 4:
125			self.history.pop(0)
126
127		return chosen

Score all pitch-pool candidates and return the chosen pitch, or None for a rest.

@dataclasses.dataclass
class Tuning:
 41@dataclasses.dataclass
 42class Tuning:
 43
 44	"""A microtonal tuning system expressed as cent offsets from the unison.
 45
 46	The ``cents`` list contains the cent values for scale degrees 1 through N.
 47	Degree 0 (the unison, 0.0 cents) is always implicit and not stored.
 48	The last entry is typically 1200.0 cents (the octave) for octave-repeating
 49	scales, but any period is supported.
 50
 51	Create a ``Tuning`` from a file or programmatically:
 52
 53	    Tuning.from_scl("meanquar.scl")          # Scala .scl file
 54	    Tuning.from_cents([100, 200, ..., 1200])  # explicit cents
 55	    Tuning.from_ratios([9/8, 5/4, ..., 2])   # frequency ratios
 56	    Tuning.equal(19)                          # 19-tone equal temperament
 57	"""
 58
 59	cents: typing.List[float]
 60	description: str = ""
 61
 62	@property
 63	def size (self) -> int:
 64		"""Number of scale degrees per period (the .scl ``count`` line)."""
 65		return len(self.cents)
 66
 67	@property
 68	def period_cents (self) -> float:
 69		"""Cent span of one period (typically 1200.0 for octave-repeating scales)."""
 70		return self.cents[-1] if self.cents else 1200.0
 71
 72	# ── Factory methods ───────────────────────────────────────────────────────
 73
 74	@classmethod
 75	def from_scl (cls, source: typing.Union[str, os.PathLike]) -> "Tuning":
 76		"""Parse a Scala .scl file.
 77
 78		``source`` is a file path.  Lines beginning with ``!`` are comments.
 79		The first non-comment line is the description.  The second is the
 80		integer count of pitch values.  Each subsequent line is a pitch:
 81
 82		- Contains ``.`` → cents (float).
 83		- Contains ``/`` or is a bare integer → ratio; converted to cents via
 84		  ``1200 × log₂(ratio)``.
 85
 86		Raises ``ValueError`` for malformed files.
 87		"""
 88		with open(source, "r", encoding="utf-8") as fh:
 89			text = fh.read()
 90		return cls._parse_scl_text(text)
 91
 92	@classmethod
 93	def from_scl_string (cls, text: str) -> "Tuning":
 94		"""Parse a Scala .scl file from a string (useful for testing)."""
 95		return cls._parse_scl_text(text)
 96
 97	@classmethod
 98	def _parse_scl_text (cls, text: str) -> "Tuning":
 99		lines = [line.rstrip() for line in text.splitlines()]
100		non_comment: typing.List[str] = [l for l in lines if not l.lstrip().startswith("!")]
101
102		if len(non_comment) < 2:
103			raise ValueError("Malformed .scl: need description + count lines")
104
105		description = non_comment[0].strip()
106
107		try:
108			count = int(non_comment[1].strip())
109		except ValueError:
110			raise ValueError(f"Malformed .scl: expected integer count, got {non_comment[1]!r}")
111
112		pitch_lines = non_comment[2:2 + count]
113
114		if len(pitch_lines) < count:
115			raise ValueError(
116				f"Malformed .scl: expected {count} pitch values, got {len(pitch_lines)}"
117			)
118
119		cents_list: typing.List[float] = []
120		for raw in pitch_lines:
121			# Text after the pitch value is ignored (Scala spec)
122			token = raw.split()[0] if raw.split() else ""
123			cents_list.append(cls._parse_pitch_token(token))
124
125		return cls(cents=cents_list, description=description)
126
127	@staticmethod
128	def _parse_pitch_token (token: str) -> float:
129		"""Convert a single .scl pitch token to cents."""
130		if not token:
131			raise ValueError("Empty pitch token in .scl file")
132		if "." in token:
133			# Cents value
134			return float(token)
135		if "/" in token:
136			# Ratio like 3/2
137			num_str, den_str = token.split("/", 1)
138			ratio = int(num_str) / int(den_str)
139		else:
140			# Bare integer like 2 (interpreted as 2/1)
141			ratio = float(token)
142		if ratio <= 0:
143			raise ValueError(f"Non-positive ratio in .scl: {token!r}")
144		return 1200.0 * math.log2(ratio)
145
146	@classmethod
147	def from_cents (cls, cents: typing.List[float], description: str = "") -> "Tuning":
148		"""Construct a tuning from a list of cent values for degrees 1..N.
149
150		The implicit degree 0 (unison, 0.0 cents) is not included in ``cents``.
151		The last value is typically 1200.0 for an octave-repeating scale.
152		"""
153		return cls(cents=list(cents), description=description)
154
155	@classmethod
156	def from_ratios (cls, ratios: typing.List[float], description: str = "") -> "Tuning":
157		"""Construct a tuning from frequency ratios relative to 1/1.
158
159		Each ratio is converted to cents via ``1200 × log₂(ratio)``.
160		Pass ``2`` or ``2.0`` for the octave (1200 cents).
161		"""
162		cents = [1200.0 * math.log2(r) for r in ratios]
163		return cls(cents=cents, description=description)
164
165	@classmethod
166	def equal (cls, divisions: int = 12, period: float = 1200.0) -> "Tuning":
167		"""Construct an equal-tempered tuning with ``divisions`` equal steps per period.
168
169		``Tuning.equal(12)`` is standard 12-TET (no pitch bend needed).
170		``Tuning.equal(19)`` gives 19-tone equal temperament.
171		"""
172		step = period / divisions
173		cents = [step * i for i in range(1, divisions + 1)]
174		return cls(
175			cents=cents,
176			description=f"{divisions}-tone equal temperament",
177		)
178
179	# ── Core calculation ──────────────────────────────────────────────────────
180
181	def pitch_bend_for_note (
182		self,
183		midi_note: int,
184		reference_note: int = 60,
185		bend_range: float = 2.0,
186	) -> typing.Tuple[int, float]:
187		"""Return ``(nearest_12tet_note, bend_normalized)`` for a MIDI note number.
188
189		The MIDI note number is interpreted as a scale degree relative to
190		``reference_note`` (default 60 = C4, degree 0 of the scale).  The
191		tuning's cent table determines the exact frequency, and the nearest
192		12-TET MIDI note plus a fractional pitch bend corrects the remainder.
193
194		Parameters:
195			midi_note: The MIDI note to tune (0–127).
196			reference_note: MIDI note number that maps to degree 0 of the scale.
197			bend_range: Pitch wheel range in semitones (must match the synth's
198			    pitch-bend range setting).  Default ±2 semitones.
199
200		Returns:
201			A tuple ``(nearest_note, bend_normalized)`` where ``nearest_note``
202			is the integer MIDI note to send and ``bend_normalized`` is the
203			normalised pitch bend value (-1.0 to +1.0).
204		"""
205		if self.size == 0:
206			return midi_note, 0.0
207
208		steps_from_root = midi_note - reference_note
209		degree = steps_from_root % self.size
210		octave = steps_from_root // self.size
211
212		# Cent value for this degree (degree 0 = 0.0, degree k = cents[k-1])
213		degree_cents = 0.0 if degree == 0 else self.cents[degree - 1]
214
215		# Total cents from the root
216		total_cents = octave * self.period_cents + degree_cents
217
218		# Equivalent continuous 12-TET note number (100 cents per semitone)
219		continuous = reference_note + total_cents / 100.0
220
221		nearest = int(round(continuous))
222		nearest = max(0, min(127, nearest))
223
224		offset_semitones = continuous - nearest  # signed, in semitones
225
226		if bend_range <= 0:
227			bend_normalized = 0.0
228		else:
229			bend_normalized = max(-1.0, min(1.0, offset_semitones / bend_range))
230
231		return nearest, bend_normalized

A microtonal tuning system expressed as cent offsets from the unison.

The cents list contains the cent values for scale degrees 1 through N. Degree 0 (the unison, 0.0 cents) is always implicit and not stored. The last entry is typically 1200.0 cents (the octave) for octave-repeating scales, but any period is supported.

Create a Tuning from a file or programmatically:

Tuning.from_scl("meanquar.scl")          # Scala .scl file
Tuning.from_cents([100, 200, ..., 1200])  # explicit cents
Tuning.from_ratios([9/8, 5/4, ..., 2])   # frequency ratios
Tuning.equal(19)                          # 19-tone equal temperament
Tuning(cents: List[float], description: str = '')
cents: List[float]
description: str = ''
size: int
62	@property
63	def size (self) -> int:
64		"""Number of scale degrees per period (the .scl ``count`` line)."""
65		return len(self.cents)

Number of scale degrees per period (the .scl count line).

period_cents: float
67	@property
68	def period_cents (self) -> float:
69		"""Cent span of one period (typically 1200.0 for octave-repeating scales)."""
70		return self.cents[-1] if self.cents else 1200.0

Cent span of one period (typically 1200.0 for octave-repeating scales).

@classmethod
def from_scl(cls, source: Union[str, os.PathLike]) -> Tuning:
74	@classmethod
75	def from_scl (cls, source: typing.Union[str, os.PathLike]) -> "Tuning":
76		"""Parse a Scala .scl file.
77
78		``source`` is a file path.  Lines beginning with ``!`` are comments.
79		The first non-comment line is the description.  The second is the
80		integer count of pitch values.  Each subsequent line is a pitch:
81
82		- Contains ``.`` → cents (float).
83		- Contains ``/`` or is a bare integer → ratio; converted to cents via
84		  ``1200 × log₂(ratio)``.
85
86		Raises ``ValueError`` for malformed files.
87		"""
88		with open(source, "r", encoding="utf-8") as fh:
89			text = fh.read()
90		return cls._parse_scl_text(text)

Parse a Scala .scl file.

source is a file path. Lines beginning with ! are comments. The first non-comment line is the description. The second is the integer count of pitch values. Each subsequent line is a pitch:

  • Contains . → cents (float).
  • Contains / or is a bare integer → ratio; converted to cents via 1200 × log₂(ratio).

Raises ValueError for malformed files.

@classmethod
def from_scl_string(cls, text: str) -> Tuning:
92	@classmethod
93	def from_scl_string (cls, text: str) -> "Tuning":
94		"""Parse a Scala .scl file from a string (useful for testing)."""
95		return cls._parse_scl_text(text)

Parse a Scala .scl file from a string (useful for testing).

@classmethod
def from_cents( cls, cents: List[float], description: str = '') -> Tuning:
146	@classmethod
147	def from_cents (cls, cents: typing.List[float], description: str = "") -> "Tuning":
148		"""Construct a tuning from a list of cent values for degrees 1..N.
149
150		The implicit degree 0 (unison, 0.0 cents) is not included in ``cents``.
151		The last value is typically 1200.0 for an octave-repeating scale.
152		"""
153		return cls(cents=list(cents), description=description)

Construct a tuning from a list of cent values for degrees 1..N.

The implicit degree 0 (unison, 0.0 cents) is not included in cents. The last value is typically 1200.0 for an octave-repeating scale.

@classmethod
def from_ratios( cls, ratios: List[float], description: str = '') -> Tuning:
155	@classmethod
156	def from_ratios (cls, ratios: typing.List[float], description: str = "") -> "Tuning":
157		"""Construct a tuning from frequency ratios relative to 1/1.
158
159		Each ratio is converted to cents via ``1200 × log₂(ratio)``.
160		Pass ``2`` or ``2.0`` for the octave (1200 cents).
161		"""
162		cents = [1200.0 * math.log2(r) for r in ratios]
163		return cls(cents=cents, description=description)

Construct a tuning from frequency ratios relative to 1/1.

Each ratio is converted to cents via 1200 × log₂(ratio). Pass 2 or 2.0 for the octave (1200 cents).

@classmethod
def equal( cls, divisions: int = 12, period: float = 1200.0) -> Tuning:
165	@classmethod
166	def equal (cls, divisions: int = 12, period: float = 1200.0) -> "Tuning":
167		"""Construct an equal-tempered tuning with ``divisions`` equal steps per period.
168
169		``Tuning.equal(12)`` is standard 12-TET (no pitch bend needed).
170		``Tuning.equal(19)`` gives 19-tone equal temperament.
171		"""
172		step = period / divisions
173		cents = [step * i for i in range(1, divisions + 1)]
174		return cls(
175			cents=cents,
176			description=f"{divisions}-tone equal temperament",
177		)

Construct an equal-tempered tuning with divisions equal steps per period.

Tuning.equal(12) is standard 12-TET (no pitch bend needed). Tuning.equal(19) gives 19-tone equal temperament.

def pitch_bend_for_note( self, midi_note: int, reference_note: int = 60, bend_range: float = 2.0) -> Tuple[int, float]:
181	def pitch_bend_for_note (
182		self,
183		midi_note: int,
184		reference_note: int = 60,
185		bend_range: float = 2.0,
186	) -> typing.Tuple[int, float]:
187		"""Return ``(nearest_12tet_note, bend_normalized)`` for a MIDI note number.
188
189		The MIDI note number is interpreted as a scale degree relative to
190		``reference_note`` (default 60 = C4, degree 0 of the scale).  The
191		tuning's cent table determines the exact frequency, and the nearest
192		12-TET MIDI note plus a fractional pitch bend corrects the remainder.
193
194		Parameters:
195			midi_note: The MIDI note to tune (0–127).
196			reference_note: MIDI note number that maps to degree 0 of the scale.
197			bend_range: Pitch wheel range in semitones (must match the synth's
198			    pitch-bend range setting).  Default ±2 semitones.
199
200		Returns:
201			A tuple ``(nearest_note, bend_normalized)`` where ``nearest_note``
202			is the integer MIDI note to send and ``bend_normalized`` is the
203			normalised pitch bend value (-1.0 to +1.0).
204		"""
205		if self.size == 0:
206			return midi_note, 0.0
207
208		steps_from_root = midi_note - reference_note
209		degree = steps_from_root % self.size
210		octave = steps_from_root // self.size
211
212		# Cent value for this degree (degree 0 = 0.0, degree k = cents[k-1])
213		degree_cents = 0.0 if degree == 0 else self.cents[degree - 1]
214
215		# Total cents from the root
216		total_cents = octave * self.period_cents + degree_cents
217
218		# Equivalent continuous 12-TET note number (100 cents per semitone)
219		continuous = reference_note + total_cents / 100.0
220
221		nearest = int(round(continuous))
222		nearest = max(0, min(127, nearest))
223
224		offset_semitones = continuous - nearest  # signed, in semitones
225
226		if bend_range <= 0:
227			bend_normalized = 0.0
228		else:
229			bend_normalized = max(-1.0, min(1.0, offset_semitones / bend_range))
230
231		return nearest, bend_normalized

Return (nearest_12tet_note, bend_normalized) for a MIDI note number.

The MIDI note number is interpreted as a scale degree relative to reference_note (default 60 = C4, degree 0 of the scale). The tuning's cent table determines the exact frequency, and the nearest 12-TET MIDI note plus a fractional pitch bend corrects the remainder.

Arguments:
  • midi_note: The MIDI note to tune (0–127).
  • reference_note: MIDI note number that maps to degree 0 of the scale.
  • bend_range: Pitch wheel range in semitones (must match the synth's pitch-bend range setting). Default ±2 semitones.
Returns:

A tuple (nearest_note, bend_normalized) where nearest_note is the integer MIDI note to send and bend_normalized is the normalised pitch bend value (-1.0 to +1.0).

def register_scale( name: str, intervals: List[int], qualities: Optional[List[str]] = None) -> None:
358def register_scale (
359	name: str,
360	intervals: typing.List[int],
361	qualities: typing.Optional[typing.List[str]] = None
362) -> None:
363
364	"""
365	Register a custom scale for use with ``p.quantize()`` and
366	``scale_pitch_classes()``.
367
368	Parameters:
369		name: Scale name (used in ``p.quantize(key, name)``).
370		intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]``
371			for Hirajōshi). Must start with 0 and contain values 0–11.
372		qualities: Optional chord quality per scale degree (e.g.
373			``["minor", "major", "minor", "major", "diminished"]``).
374			Required only if you want to use the scale with
375			``diatonic_chords()`` or ``diatonic_chord_sequence()``.
376
377	Example::
378
379		import subsequence
380
381		subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11])
382
383		@comp.pattern(channel=0, length=4)
384		def melody (p):
385			p.note(60, beat=0)
386			p.quantize("C", "raga_bhairav")
387	"""
388
389	if not intervals or intervals[0] != 0:
390		raise ValueError("intervals must start with 0")
391	if any(i < 0 or i > 11 for i in intervals):
392		raise ValueError("intervals must contain values between 0 and 11")
393	if qualities is not None and len(qualities) != len(intervals):
394		raise ValueError(
395			f"qualities length ({len(qualities)}) must match "
396			f"intervals length ({len(intervals)})"
397		)
398
399	INTERVAL_DEFINITIONS[name] = intervals
400	SCALE_MODE_MAP[name] = (name, qualities)

Register a custom scale for use with p.quantize() and scale_pitch_classes().

Arguments:
  • name: Scale name (used in p.quantize(key, name)).
  • intervals: Semitone offsets from the root (e.g. [0, 2, 3, 7, 8] for Hirajōshi). Must start with 0 and contain values 0–11.
  • qualities: Optional chord quality per scale degree (e.g. ["minor", "major", "minor", "major", "diminished"]). Required only if you want to use the scale with diatonic_chords() or diatonic_chord_sequence().

Example::

    import subsequence

    subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11])

    @comp.pattern(channel=0, length=4)
    def melody (p):
            p.note(60, beat=0)
            p.quantize("C", "raga_bhairav")
def scale_notes( key: str, mode: str = 'ionian', low: int = 60, high: int = 72, count: Optional[int] = None) -> List[int]:
214def scale_notes (
215	key: str,
216	mode: str = "ionian",
217	low: int = 60,
218	high: int = 72,
219	count: typing.Optional[int] = None,
220) -> typing.List[int]:
221
222	"""Return MIDI note numbers for a scale within a pitch range.
223
224	Parameters:
225		key: Scale root as a note name (``"C"``, ``"F#"``, ``"Bb"``, etc.).
226		     This acts as a **pitch-class filter only** — it determines which
227		     semitone positions (0–11) are valid members of the scale, but does
228		     not affect which octave notes are drawn from. Notes are selected
229		     starting from ``low`` upward; ``key`` controls *which* notes are
230		     kept, not where the sequence starts. To guarantee the first
231		     returned note is the root, ``low`` must be a MIDI number whose
232		     pitch class matches ``key``. When starting from an arbitrary MIDI
233		     number, derive the key name with
234		     ``subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]``.
235		mode: Scale mode name. Supports all keys of :data:`SCALE_MODE_MAP`
236		      (e.g. ``"ionian"``, ``"dorian"``, ``"natural_minor"``,
237		      ``"major_pentatonic"``). Use :func:`register_scale` for custom scales.
238		low: Lowest MIDI note (inclusive). When ``count`` is set, this is
239		     the starting note from which the scale ascends. **If ``low`` is
240		     not a member of the scale defined by ``key``, it is silently
241		     skipped** and the first returned note will be the next in-scale
242		     pitch above ``low``.
243		high: Highest MIDI note (inclusive). Ignored when ``count`` is set.
244		count: Exact number of notes to return. Notes ascend from ``low``
245		       through successive scale degrees, cycling into higher octaves
246		       as needed. When ``None`` (default), all scale tones between
247		       ``low`` and ``high`` are returned.
248
249	Returns:
250		Sorted list of MIDI note numbers.
251
252	Examples:
253		```python
254		import subsequence
255		import subsequence.constants.midi_notes as notes
256
257		# C major: all tones from middle C to C5
258		subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5)
259		# → [60, 62, 64, 65, 67, 69, 71, 72]
260
261		# E natural minor (aeolian) across one octave
262		subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3)
263		# → [40, 42, 43, 45, 47, 48, 50, 52]
264
265		# 15 notes of A minor pentatonic ascending from A3
266		subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15)
267		# → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91]
268
269		# Misalignment: key="E" but low=C4 — first note is C, not E
270		subsequence.scale_notes("E", "minor", low=60, count=4)
271		# → [60, 62, 64, 67]  (C D E G — all in E natural minor, but starts on C)
272
273		# Fix: derive key name from root_pitch so low is always in the scale
274		root_pitch = 64  # E4
275		key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]  # → "E"
276		subsequence.scale_notes(key, "minor", low=root_pitch, count=4)
277		# → [64, 66, 67, 69]  (E F# G A — starts on the root)
278		```
279	"""
280
281	key_pc = subsequence.chords.key_name_to_pc(key)
282	pcs = set(scale_pitch_classes(key_pc, mode))
283
284	if count is not None:
285		if not pcs:
286			return []
287		result: typing.List[int] = []
288		pitch = low
289		while len(result) < count and pitch <= 127:
290			if pitch % 12 in pcs:
291				result.append(pitch)
292			pitch += 1
293		return result
294
295	return [p for p in range(low, high + 1) if p % 12 in pcs]

Return MIDI note numbers for a scale within a pitch range.

Arguments:
  • key: Scale root as a note name ("C", "F#", "Bb", etc.). This acts as a pitch-class filter only — it determines which semitone positions (0–11) are valid members of the scale, but does not affect which octave notes are drawn from. Notes are selected starting from low upward; key controls which notes are kept, not where the sequence starts. To guarantee the first returned note is the root, low must be a MIDI number whose pitch class matches key. When starting from an arbitrary MIDI number, derive the key name with subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12].
  • mode: Scale mode name. Supports all keys of SCALE_MODE_MAP (e.g. "ionian", "dorian", "natural_minor", "major_pentatonic"). Use register_scale() for custom scales.
  • low: Lowest MIDI note (inclusive). When count is set, this is the starting note from which the scale ascends. If low is not a member of the scale defined by key, it is silently skipped and the first returned note will be the next in-scale pitch above low.
  • high: Highest MIDI note (inclusive). Ignored when count is set.
  • count: Exact number of notes to return. Notes ascend from low through successive scale degrees, cycling into higher octaves as needed. When None (default), all scale tones between low and high are returned.
Returns:

Sorted list of MIDI note numbers.

Examples:
import subsequence
import subsequence.constants.midi_notes as notes

# C major: all tones from middle C to C5
subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5)
# → [60, 62, 64, 65, 67, 69, 71, 72]

# E natural minor (aeolian) across one octave
subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3)
# → [40, 42, 43, 45, 47, 48, 50, 52]

# 15 notes of A minor pentatonic ascending from A3
subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15)
# → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91]

# Misalignment: key="E" but low=C4 — first note is C, not E
subsequence.scale_notes("E", "minor", low=60, count=4)
# → [60, 62, 64, 67]  (C D E G — all in E natural minor, but starts on C)

# Fix: derive key name from root_pitch so low is always in the scale
root_pitch = 64  # E4
key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]  # → "E"
subsequence.scale_notes(key, "minor", low=root_pitch, count=4)
# → [64, 66, 67, 69]  (E F# G A — starts on the root)
def bank_select(bank: int) -> Tuple[int, int]:
111def bank_select (bank: int) -> typing.Tuple[int, int]:
112
113	"""
114	Convert a 14-bit MIDI bank number to (MSB, LSB) for use with
115	``p.program_change()``.
116
117	MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and
118	CC 32 (Bank LSB).  Together they encode a 14-bit bank number in the
119	range 0–16,383:
120
121	    MSB = bank // 128   (upper 7 bits, sent on CC 0)
122	    LSB = bank % 128    (lower 7 bits, sent on CC 32)
123
124	Args:
125		bank: Integer bank number, 0–16,383.  Values outside this range are
126		      clamped.
127
128	Returns:
129		``(msb, lsb)`` tuple, each value in 0–127.
130
131	Example:
132		```python
133		msb, lsb = subsequence.bank_select(128)   # → (1, 0)
134		p.program_change(48, bank_msb=msb, bank_lsb=lsb)
135		```
136	"""
137
138	bank = max(0, min(16383, bank))
139	return bank >> 7, bank & 0x7F

Convert a 14-bit MIDI bank number to (MSB, LSB) for use with p.program_change().

MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and CC 32 (Bank LSB). Together they encode a 14-bit bank number in the range 0–16,383:

MSB = bank // 128   (upper 7 bits, sent on CC 0)
LSB = bank % 128    (lower 7 bits, sent on CC 32)
Arguments:
  • bank: Integer bank number, 0–16,383. Values outside this range are clamped.
Returns:

(msb, lsb) tuple, each value in 0–127.

Example:
msb, lsb = subsequence.bank_select(128)   # → (1, 0)
p.program_change(48, bank_msb=msb, bank_lsb=lsb)