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

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

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

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

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

The underlying Sequencer instance.

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

The currently active patterns, keyed by name.

builder_bar: int
766	@property
767	def builder_bar (self) -> int:
768		"""Current bar index used by pattern builders."""
769		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:
780	def harmony (
781		self,
782		style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major",
783		cycle_beats: int = 4,
784		dominant_7th: bool = True,
785		gravity: float = 1.0,
786		nir_strength: float = 0.5,
787		minor_turnaround_weight: float = 0.0,
788		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
789		reschedule_lookahead: float = 1
790	) -> None:
791
792		"""
793		Configure the harmonic logic and chord change intervals.
794
795		Subsequence uses a weighted transition graph to choose the next chord.
796		You can influence these choices using 'gravity' (favoring the tonic) and
797		'NIR strength' (melodic inertia based on Narmour's model).
798
799		Parameters:
800			style: The harmonic style to use. Built-in: "functional_major"
801				(alias "diatonic_major"), "turnaround", "aeolian_minor",
802				"phrygian_minor", "lydian_major", "dorian_minor",
803				"chromatic_mediant", "suspended", "mixolydian", "whole_tone",
804				"diminished". See README for full descriptions.
805			cycle_beats: How many beats each chord lasts (default 4).
806			dominant_7th: Whether to include V7 chords (default True).
807			gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
808			nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement
809				expectations.
810			minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
811			root_diversity: Root-repetition damping (0.0 to 1.0). Each recent
812				chord sharing a candidate's root reduces the weight to 40% at
813				the default (0.4). Set to 1.0 to disable.
814			reschedule_lookahead: How many beats in advance to calculate the
815				next chord.
816
817		Example:
818			```python
819			# A moody minor progression that changes every 8 beats
820			comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
821			```
822		"""
823
824		if self.key is None:
825			raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor")
826
827		preserved_history: typing.List[subsequence.chords.Chord] = []
828		preserved_current: typing.Optional[subsequence.chords.Chord] = None
829
830		if self._harmonic_state is not None:
831			preserved_history = self._harmonic_state.history.copy()
832			preserved_current = self._harmonic_state.current_chord
833
834		self._harmonic_state = subsequence.harmonic_state.HarmonicState(
835			key_name = self.key,
836			graph_style = style,
837			include_dominant_7th = dominant_7th,
838			key_gravity_blend = gravity,
839			nir_strength = nir_strength,
840			minor_turnaround_weight = minor_turnaround_weight,
841			root_diversity = root_diversity
842		)
843
844		if preserved_history:
845			self._harmonic_state.history = preserved_history
846		if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current):
847			self._harmonic_state.current_chord = preserved_current
848
849		self._harmony_cycle_beats = cycle_beats
850		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:
852	def freeze (self, bars: int) -> "Progression":
853
854		"""Capture a chord progression from the live harmony engine.
855
856		Runs the harmony engine forward by *bars* chord changes, records each
857		chord, and returns it as a :class:`Progression` that can be bound to a
858		form section with :meth:`section_chords`.
859
860		The engine state **advances** — successive ``freeze()`` calls produce a
861		continuing compositional journey so section progressions feel like parts
862		of a whole rather than isolated islands.
863
864		Parameters:
865			bars: Number of chords to capture (one per harmony cycle).
866
867		Returns:
868			A :class:`Progression` with the captured chords and trailing
869			history for NIR continuity.
870
871		Raises:
872			ValueError: If :meth:`harmony` has not been called first.
873
874		Example::
875
876			composition.harmony(style="functional_major", cycle_beats=4)
877			verse  = composition.freeze(8)   # 8 chords, engine advances
878			chorus = composition.freeze(4)   # next 4 chords, continuing on
879			composition.section_chords("verse",  verse)
880			composition.section_chords("chorus", chorus)
881		"""
882
883		hs = self._require_harmonic_state()
884
885		if bars < 1:
886			raise ValueError("bars must be at least 1")
887		collected: typing.List[subsequence.chords.Chord] = [hs.current_chord]
888
889		for _ in range(bars - 1):
890			hs.step()
891			collected.append(hs.current_chord)
892
893		# Advance past the last captured chord so the next freeze() call or
894		# live playback does not duplicate it.
895		hs.step()
896
897		return Progression(
898			chords = tuple(collected),
899			trailing_history = tuple(hs.history),
900		)

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:
902	def section_chords (self, section_name: str, progression: "Progression") -> None:
903
904		"""Bind a frozen :class:`Progression` to a named form section.
905
906		Every time *section_name* plays, the harmonic clock replays the
907		progression's chords in order instead of calling the live engine.
908		Sections without a bound progression continue generating live chords.
909
910		Parameters:
911			section_name: Name of the section as defined in :meth:`form`.
912			progression: The :class:`Progression` returned by :meth:`freeze`.
913
914		Raises:
915			ValueError: If the form has been configured and *section_name* is
916				not a known section name.
917
918		Example::
919
920			composition.section_chords("verse",  verse_progression)
921			composition.section_chords("chorus", chorus_progression)
922			# "bridge" is not bound — it generates live chords
923		"""
924
925		if (
926			self._form_state is not None
927			and self._form_state._section_bars is not None
928			and section_name not in self._form_state._section_bars
929		):
930			known = ", ".join(sorted(self._form_state._section_bars))
931			raise ValueError(
932				f"Section '{section_name}' not found in form. "
933				f"Known sections: {known}"
934			)
935
936		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:
938	def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None:
939
940		"""
941		Register a callback for a sequencer event (e.g., "bar", "start", "stop").
942		"""
943
944		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:
951	def hotkeys (self, enabled: bool = True) -> None:
952
953		"""Enable or disable the global hotkey listener.
954
955		Must be called **before** :meth:`play` to take effect.  When enabled, a
956		background thread reads single keystrokes from stdin without requiring
957		Enter.  The ``?`` key is always reserved and lists all active bindings.
958
959		Hotkeys have zero impact on playback when disabled — the listener
960		thread is never started.
961
962		Args:
963		    enabled: ``True`` (default) to enable hotkeys; ``False`` to disable.
964
965		Example::
966
967		    composition.hotkeys()
968		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
969		    composition.play()
970		"""
971
972		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:
 975	def hotkey (
 976		self,
 977		key:      str,
 978		action:   typing.Callable[[], None],
 979		quantize: int = 0,
 980		label:    typing.Optional[str] = None,
 981	) -> None:
 982
 983		"""Register a single-key shortcut that fires during playback.
 984
 985		The listener must be enabled first with :meth:`hotkeys`.
 986
 987		Most actions — form jumps, ``composition.data`` writes, and
 988		:meth:`tweak` calls — should use ``quantize=0`` (the default).  Their
 989		musical effect is naturally delayed to the next pattern rebuild cycle,
 990		which provides automatic musical quantization without extra configuration.
 991
 992		Use ``quantize=N`` for actions where you want an explicit bar-boundary
 993		guarantee, such as :meth:`mute` / :meth:`unmute`.
 994
 995		The ``?`` key is reserved and cannot be overridden.
 996
 997		Args:
 998		    key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``).
 999		    action: Zero-argument callable to execute.
1000		    quantize: ``0`` = execute immediately (default).  ``N`` = execute
1001		        on the next global bar number divisible by *N*.
1002		    label: Display name for the ``?`` help listing.  Auto-derived from
1003		        the function name or lambda body if omitted.
1004
1005		Raises:
1006		    ValueError: If ``key`` is the reserved ``?`` character, or if
1007		        ``key`` is not exactly one character.
1008
1009		Example::
1010
1011		    composition.hotkeys()
1012
1013		    # Immediate — musical effect happens at next pattern rebuild
1014		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
1015		    composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
1016
1017		    # Explicit 4-bar phrase boundary
1018		    composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
1019
1020		    # Named function — label is derived automatically
1021		    def drop_to_breakdown():
1022		        composition.form_jump("breakdown")
1023		        composition.mute("lead")
1024
1025		    composition.hotkey("d", drop_to_breakdown)
1026
1027		    composition.play()
1028		"""
1029
1030		if len(key) != 1:
1031			raise ValueError(f"hotkey key must be a single character, got {key!r}")
1032
1033		if key == _HOTKEY_RESERVED:
1034			raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.")
1035
1036		derived = label if label is not None else _derive_label(action)
1037
1038		self._hotkey_bindings[key] = HotkeyBinding(
1039			key      = key,
1040			action   = action,
1041			quantize = quantize,
1042			label    = derived,
1043		)

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:
1046	def form_jump (self, section_name: str) -> None:
1047
1048		"""Jump the form to a named section immediately.
1049
1050		Delegates to :meth:`subsequence.form_state.FormState.jump_to`.  Only works when the
1051		composition uses graph-mode form (a dict passed to :meth:`form`).
1052
1053		The musical effect is heard at the *next pattern rebuild cycle* — already-
1054		queued MIDI notes are unaffected.  This natural delay means ``form_jump``
1055		is effective without needing explicit quantization.
1056
1057		Args:
1058		    section_name: The section to jump to.
1059
1060		Raises:
1061		    ValueError: If no form is configured, or the form is not in graph
1062		        mode, or *section_name* is unknown.
1063
1064		Example::
1065
1066		    composition.hotkey("c", lambda: composition.form_jump("chorus"))
1067		"""
1068
1069		if self._form_state is None:
1070			raise ValueError("form_jump() requires a form to be configured via composition.form().")
1071
1072		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:
1075	def form_next (self, section_name: str) -> None:
1076
1077		"""Queue the next section — takes effect when the current section ends.
1078
1079		Unlike :meth:`form_jump`, this does not interrupt the current section.
1080		The queued section replaces the automatically pre-decided next section
1081		and takes effect at the natural section boundary.  The performer can
1082		change their mind by calling ``form_next`` again before the boundary.
1083
1084		Delegates to :meth:`subsequence.form_state.FormState.queue_next`.  Only works when the
1085		composition uses graph-mode form (a dict passed to :meth:`form`).
1086
1087		Args:
1088		    section_name: The section to queue.
1089
1090		Raises:
1091		    ValueError: If no form is configured, or the form is not in graph
1092		        mode, or *section_name* is unknown.
1093
1094		Example::
1095
1096		    composition.hotkey("c", lambda: composition.form_next("chorus"))
1097		"""
1098
1099		if self._form_state is None:
1100			raise ValueError("form_next() requires a form to be configured via composition.form().")
1101
1102		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:
1185	def seed (self, value: int) -> None:
1186
1187		"""
1188		Set a random seed for deterministic, repeatable playback.
1189
1190		If a seed is set, Subsequence will produce the exact same sequence 
1191		every time you run the script. This is vital for finishing tracks or 
1192		reproducing a specific 'performance'.
1193
1194		Parameters:
1195			value: An integer seed.
1196
1197		Example:
1198			```python
1199			# Fix the randomness
1200			comp.seed(42)
1201			```
1202		"""
1203
1204		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:
1206	def tuning (
1207		self,
1208		source: typing.Optional[typing.Union[str, "os.PathLike"]] = None,
1209		*,
1210		cents: typing.Optional[typing.List[float]] = None,
1211		ratios: typing.Optional[typing.List[float]] = None,
1212		equal: typing.Optional[int] = None,
1213		bend_range: float = 2.0,
1214		channels: typing.Optional[typing.List[int]] = None,
1215		reference_note: int = 60,
1216		exclude_drums: bool = True,
1217	) -> None:
1218
1219		"""Set a global microtonal tuning for the composition.
1220
1221		The tuning is applied automatically after each pattern rebuild (before
1222		the pattern is scheduled).  Drum patterns (those registered with a
1223		``drum_note_map``) are excluded by default.
1224
1225		Supply exactly one of the source parameters:
1226
1227		- ``source``: path to a Scala ``.scl`` file.
1228		- ``cents``: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).
1229		- ``ratios``: list of frequency ratios (e.g., ``[9/8, 5/4, 4/3, 3/2, 2]``).
1230		- ``equal``: integer for N-tone equal temperament (e.g., ``equal=19``).
1231
1232		For polyphonic parts, supply a ``channels`` pool.  Notes are spread
1233		across those MIDI channels so each can carry an independent pitch bend.
1234		The synth must be configured to match ``bend_range`` (its pitch-bend range
1235		setting in semitones).
1236
1237		Parameters:
1238			source: Path to a ``.scl`` file.
1239			cents: Cent offsets for scale degrees 1..N.
1240			ratios: Frequency ratios for scale degrees 1..N.
1241			equal: Number of equal divisions of the period.
1242			bend_range: Synth pitch-bend range in semitones (default ±2).
1243			channels: Channel pool for polyphonic rotation.
1244			reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).
1245			exclude_drums: When True (default), skip patterns that have a
1246			    ``drum_note_map`` (they use fixed GM pitches, not tuned ones).
1247
1248		Example:
1249			```python
1250			# Quarter-comma meantone from a Scala file
1251			comp.tuning("meanquar.scl")
1252
1253			# Just intonation from ratios
1254			comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2])
1255
1256			# 19-TET, monophonic
1257			comp.tuning(equal=19, bend_range=2.0)
1258
1259			# 31-TET with channel rotation for polyphony (channels 1-6)
1260			comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])
1261			```
1262		"""
1263		import subsequence.tuning as _tuning_mod
1264
1265		given = sum(x is not None for x in [source, cents, ratios, equal])
1266		if given == 0:
1267			raise ValueError("composition.tuning() requires one of: source, cents, ratios, or equal")
1268		if given > 1:
1269			raise ValueError("composition.tuning() accepts only one source parameter")
1270
1271		if source is not None:
1272			t = _tuning_mod.Tuning.from_scl(source)
1273		elif cents is not None:
1274			t = _tuning_mod.Tuning.from_cents(cents)
1275		elif ratios is not None:
1276			t = _tuning_mod.Tuning.from_ratios(ratios)
1277		else:
1278			t = _tuning_mod.Tuning.equal(equal)  # type: ignore[arg-type]
1279
1280		self._tuning = t
1281		self._tuning_bend_range = bend_range
1282		self._tuning_channels = channels
1283		self._tuning_reference_note = reference_note
1284		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:
1286	def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
1287
1288		"""
1289		Enable or disable the live terminal dashboard.
1290
1291		When enabled, Subsequence uses a safe logging handler that allows a
1292		persistent status line (BPM, Key, Bar, Section, Chord) to stay at
1293		the bottom of the terminal while logs scroll above it.
1294
1295		Parameters:
1296			enabled: Whether to show the display (default True).
1297			grid: When True, render an ASCII grid visualisation of all
1298				running patterns above the status line. The grid updates
1299				once per bar, showing which steps have notes and at what
1300				velocity.
1301			grid_scale: Horizontal zoom factor for the grid (default
1302				``1.0``).  Higher values add visual columns between
1303				grid steps, revealing micro-timing from swing and groove.
1304				Snapped to the nearest integer internally for uniform
1305				marker spacing.
1306		"""
1307
1308		if enabled:
1309			self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale)
1310		else:
1311			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:
1313	def web_ui (self) -> None:
1314
1315		"""
1316		Enable the realtime Web UI Dashboard.
1317
1318		When enabled, Subsequence instantiates a WebSocket server that broadcasts 
1319		the current state, signals, and active patterns (with high-res timing and note data) 
1320		to any connected browser clients.
1321		"""
1322
1323		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:
1325	def midi_input (self, device: str, clock_follow: bool = False, name: typing.Optional[str] = None) -> None:
1326
1327		"""
1328		Configure a MIDI input device for external sync and MIDI messages.
1329
1330		May be called multiple times to register additional input devices.
1331		The first call sets the primary input (device 0).  Subsequent calls
1332		add additional input devices (device 1, 2, …).  Only one device may
1333		have ``clock_follow=True``.
1334
1335		Parameters:
1336			device: The name of the MIDI input port.
1337			clock_follow: If True, Subsequence will slave its clock to incoming
1338				MIDI Ticks. It will also follow MIDI Start/Stop/Continue
1339				commands. Only one device can have this enabled at a time.
1340			name: Optional alias for use with ``cc_map(input_device=…)`` and
1341				``cc_forward(input_device=…)``.  When omitted, the raw device
1342				name is used.
1343
1344		Example:
1345			```python
1346			# Single controller (unchanged usage)
1347			comp.midi_input("Scarlett 2i4", clock_follow=True)
1348
1349			# Multiple controllers
1350			comp.midi_input("Arturia KeyStep", name="keys")
1351			comp.midi_input("Faderfox EC4", name="faders")
1352			```
1353		"""
1354
1355		if clock_follow:
1356			if self.is_clock_following:
1357				raise ValueError("Only one input device can be configured to follow external clock (clock_follow=True)")
1358
1359		if self._input_device is None:
1360			# First call: set primary input device (device 0)
1361			self._input_device = device
1362			self._input_device_alias = name
1363			self._clock_follow = clock_follow
1364		else:
1365			# Subsequent calls: register additional input devices
1366			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:
1368	def midi_output (self, device: str, name: typing.Optional[str] = None) -> int:
1369
1370		"""
1371		Register an additional MIDI output device.
1372
1373		The first output device is always the one passed to
1374		``Composition(output_device=…)`` — that is device 0.
1375		Each call to ``midi_output()`` adds the next device (1, 2, …).
1376
1377		Parameters:
1378			device: The name of the MIDI output port.
1379			name: Optional alias for use with ``pattern(device=…)``,
1380				``cc_forward(output_device=…)``, etc.  When omitted, the raw
1381				device name is used.
1382
1383		Returns:
1384			The integer device index assigned (1, 2, 3, …).
1385
1386		Example:
1387			```python
1388			comp = subsequence.Composition(bpm=120, output_device="MOTU Express")
1389
1390			# Returns 1 — use as device=1 or device="integra"
1391			comp.midi_output("Roland Integra", name="integra")
1392
1393			@comp.pattern(channel=1, beats=4, device="integra")
1394			def strings(p):
1395				p.note(60, beat=0)
1396			```
1397		"""
1398
1399		idx = 1 + len(self._additional_outputs)  # device 0 is always the primary
1400		self._additional_outputs.append((device, name))
1401		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:
1403	def clock_output (self, enabled: bool = True) -> None:
1404
1405		"""
1406		Send MIDI timing clock to connected hardware.
1407
1408		When enabled, Subsequence acts as a MIDI clock master and sends
1409		standard clock messages on the output port: a Start message (0xFA)
1410		when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN),
1411		and a Stop message (0xFC) when playback ends.
1412
1413		This allows hardware synthesizers, drum machines, and effect units to
1414		slave their tempo to Subsequence automatically.
1415
1416		**Note:** Clock output is automatically disabled when ``midi_input()``
1417		is called with ``clock_follow=True``, to prevent a clock feedback loop.
1418
1419		Parameters:
1420			enabled: Whether to send MIDI clock (default True).
1421
1422		Example:
1423			```python
1424			comp = subsequence.Composition(bpm=120, output_device="...")
1425			comp.clock_output()   # hardware will follow Subsequence tempo
1426			```
1427		"""
1428
1429		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:
1477	def cc_map (
1478		self,
1479		cc: int,
1480		key: str,
1481		channel: typing.Optional[int] = None,
1482		min_val: float = 0.0,
1483		max_val: float = 1.0,
1484		input_device: subsequence.midi_utils.DeviceId = None,
1485	) -> None:
1486
1487		"""
1488		Map an incoming MIDI CC to a ``composition.data`` key.
1489
1490		When the composition receives a CC message on the configured MIDI
1491		input port, the value is scaled from the CC range (0–127) to
1492		*[min_val, max_val]* and stored in ``composition.data[key]``.
1493
1494		This lets hardware knobs, faders, and expression pedals control live
1495		parameters without writing any callback code.
1496
1497		**Requires** ``midi_input()`` to be called first to open an input port.
1498
1499		Parameters:
1500			cc: MIDI Control Change number (0–127).
1501			key: The ``composition.data`` key to write.
1502			channel: If given, only respond to CC messages on this channel.
1503				Uses the same numbering convention as ``pattern()`` (0-15
1504				by default, or 1-16 with ``zero_indexed_channels=False``).
1505				``None`` matches any channel (default).
1506			min_val: Scaled minimum — written when CC value is 0 (default 0.0).
1507			max_val: Scaled maximum — written when CC value is 127 (default 1.0).
1508			input_device: Only respond to CC messages from this input device
1509				(index or name).  ``None`` responds to any input device (default).
1510
1511		Example:
1512			```python
1513			comp.midi_input("Arturia KeyStep")
1514			comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
1515			comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader
1516
1517			# Multi-device: only listen to CC 74 from the "faders" controller
1518			comp.cc_map(74, "filter", input_device="faders")
1519			```
1520		"""
1521
1522		resolved_channel = self._resolve_channel(channel) if channel is not None else None
1523
1524		self._cc_mappings.append({
1525			'cc': cc,
1526			'key': key,
1527			'channel': resolved_channel,
1528			'min_val': min_val,
1529			'max_val': max_val,
1530			'input_device': input_device,  # resolved to int index in _run()
1531		})

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

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:
1686	def live (self, port: int = 5555) -> None:
1687
1688		"""
1689		Enable the live coding eval server.
1690
1691		This allows you to connect to a running composition using the 
1692		`subsequence.live_client` REPL and hot-swap pattern code or 
1693		modify variables in real-time.
1694
1695		Parameters:
1696			port: The TCP port to listen on (default 5555).
1697		"""
1698
1699		self._live_server = subsequence.live_server.LiveServer(self, port=port)
1700		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:
1702	def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None:
1703
1704		"""
1705		Enable bi-directional Open Sound Control (OSC).
1706
1707		Subsequence will listen for commands (like `/bpm` or `/mute`) and 
1708		broadcast its internal state (like `/chord` or `/bar`) over UDP.
1709
1710		Parameters:
1711			receive_port: Port to listen for incoming OSC messages (default 9000).
1712			send_port: Port to send state updates to (default 9001).
1713			send_host: The IP address to send updates to (default "127.0.0.1").
1714		"""
1715
1716		self._osc_server = subsequence.osc.OscServer(
1717			self,
1718			receive_port = receive_port,
1719			send_port = send_port,
1720			send_host = send_host
1721		)

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:
1723	def osc_map (self, address: str, handler: typing.Callable) -> None:
1724
1725		"""
1726		Register a custom OSC handler.
1727
1728		Must be called after :meth:`osc` has been configured.
1729
1730		Parameters:
1731			address: OSC address pattern to match (e.g. ``"/my/param"``).
1732			handler: Callable invoked with ``(address, *args)`` when a
1733				matching message arrives.
1734
1735		Example::
1736
1737			composition.osc("/control")
1738
1739			def on_intensity(address, value):
1740				composition.data["intensity"] = float(value)
1741
1742			composition.osc_map("/intensity", on_intensity)
1743		"""
1744
1745		if self._osc_server is None:
1746			raise RuntimeError("Call composition.osc() before composition.osc_map()")
1747
1748		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:
1750	def set_bpm (self, bpm: float) -> None:
1751
1752		"""
1753		Instantly change the tempo.
1754
1755		Parameters:
1756			bpm: The new tempo in beats per minute.
1757
1758		When Ableton Link is active, this proposes the new tempo to the Link
1759		network instead of applying it locally.  The network-authoritative tempo
1760		is picked up on the next pulse.
1761		"""
1762
1763		self._sequencer.set_bpm(bpm)
1764
1765		if not self.is_clock_following and self._link_quantum is None:
1766			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:
1768	def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None:
1769
1770		"""
1771		Smoothly ramp the tempo to a target value over a number of bars.
1772
1773		Parameters:
1774			bpm: Target tempo in beats per minute.
1775			bars: Duration of the transition in bars.
1776			shape: Easing curve name.  Defaults to ``"linear"``.
1777			       ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural-
1778			       sounding tempo changes.  See :mod:`subsequence.easing` for all
1779			       available shapes.
1780
1781		Example:
1782			```python
1783			# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
1784			comp.target_bpm(140, bars=8, shape="ease_in_out")
1785			```
1786		"""
1787
1788		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]:
1790	def live_info (self) -> typing.Dict[str, typing.Any]:
1791
1792		"""
1793		Return a dictionary containing the current state of the composition.
1794		
1795		Includes BPM, key, current bar, active section, current chord, 
1796		running patterns, and custom data.
1797		"""
1798
1799		section_info = None
1800		if self._form_state is not None:
1801			section = self._form_state.get_section_info()
1802			if section is not None:
1803				section_info = {
1804					"name": section.name,
1805					"bar": section.bar,
1806					"bars": section.bars,
1807					"progress": section.progress
1808				}
1809
1810		chord_name = None
1811		if self._harmonic_state is not None:
1812			chord = self._harmonic_state.get_current_chord()
1813			if chord is not None:
1814				chord_name = chord.name()
1815
1816		pattern_list = []
1817		channel_offset = 0 if self._zero_indexed_channels else 1
1818		for name, pat in self._running_patterns.items():
1819			pattern_list.append({
1820				"name": name,
1821				"channel": pat.channel + channel_offset,
1822				"length": pat.length,
1823				"cycle": pat._cycle_count,
1824				"muted": pat._muted,
1825				"tweaks": dict(pat._tweaks)
1826			})
1827
1828		return {
1829			"bpm": self._sequencer.current_bpm,
1830			"key": self.key,
1831			"bar": self._builder_bar,
1832			"section": section_info,
1833			"chord": chord_name,
1834			"patterns": pattern_list,
1835			"input_device": self._input_device,
1836			"clock_follow": self.is_clock_following,
1837			"data": self.data
1838		}

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:
1840	def mute (self, name: str) -> None:
1841
1842		"""
1843		Mute a running pattern by name.
1844		
1845		The pattern continues to 'run' and increment its cycle count in 
1846		the background, but it will not produce any MIDI notes until unmuted.
1847
1848		Parameters:
1849			name: The function name of the pattern to mute.
1850		"""
1851
1852		if name not in self._running_patterns:
1853			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1854
1855		self._running_patterns[name]._muted = True
1856		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:
1858	def unmute (self, name: str) -> None:
1859
1860		"""
1861		Unmute a previously muted pattern.
1862		"""
1863
1864		if name not in self._running_patterns:
1865			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1866
1867		self._running_patterns[name]._muted = False
1868		logger.info(f"Unmuted pattern: {name}")

Unmute a previously muted pattern.

def tweak(self, name: str, **kwargs: Any) -> None:
1870	def tweak (self, name: str, **kwargs: typing.Any) -> None:
1871
1872		"""Override parameters for a running pattern.
1873
1874		Values set here are available inside the pattern's builder
1875		function via ``p.param()``.  They persist across rebuilds
1876		until explicitly changed or cleared.  Changes take effect
1877		on the next rebuild cycle.
1878
1879		Parameters:
1880			name: The function name of the pattern.
1881			**kwargs: Parameter names and their new values.
1882
1883		Example (from the live REPL)::
1884
1885			composition.tweak("bass", pitches=[48, 52, 55, 60])
1886		"""
1887
1888		if name not in self._running_patterns:
1889			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1890
1891		self._running_patterns[name]._tweaks.update(kwargs)
1892		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:
1894	def clear_tweak (self, name: str, *param_names: str) -> None:
1895
1896		"""Remove tweaked parameters from a running pattern.
1897
1898		If no parameter names are given, all tweaks for the pattern
1899		are cleared and every ``p.param()`` call reverts to its
1900		default.
1901
1902		Parameters:
1903			name: The function name of the pattern.
1904			*param_names: Specific parameter names to clear.  If
1905				omitted, all tweaks are removed.
1906		"""
1907
1908		if name not in self._running_patterns:
1909			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1910
1911		if not param_names:
1912			self._running_patterns[name]._tweaks.clear()
1913			logger.info(f"Cleared all tweaks for pattern '{name}'")
1914		else:
1915			for param_name in param_names:
1916				self._running_patterns[name]._tweaks.pop(param_name, None)
1917			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]:
1919	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
1920
1921		"""Return a copy of the current tweaks for a running pattern.
1922
1923		Parameters:
1924			name: The function name of the pattern.
1925		"""
1926
1927		if name not in self._running_patterns:
1928			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1929
1930		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:
1932	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
1933
1934		"""
1935		Register a custom function to run on a repeating beat-based cycle.
1936
1937		Subsequence automatically runs synchronous functions in a thread pool
1938		so they don't block the timing-critical MIDI clock. Async functions
1939		are run directly on the event loop.
1940
1941		Parameters:
1942			fn: The function to call.
1943			cycle_beats: How often to call it (e.g., 4 = every bar).
1944			reschedule_lookahead: How far in advance to schedule the next call.
1945			wait_for_initial: If True, run the function once during startup
1946				and wait for it to complete before playback begins. This
1947				ensures ``composition.data`` is populated before patterns
1948				first build. Implies ``defer=True`` for the repeating
1949				schedule.
1950			defer: If True, skip the pulse-0 fire and defer the first
1951				repeating call to just before the second cycle boundary.
1952		"""
1953
1954		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:
1956	def form (
1957		self,
1958		sections: typing.Union[
1959			typing.List[typing.Tuple[str, int]],
1960			typing.Iterator[typing.Tuple[str, int]],
1961			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
1962		],
1963		loop: bool = False,
1964		start: typing.Optional[str] = None
1965	) -> None:
1966
1967		"""
1968		Define the structure (sections) of the composition.
1969
1970		You can define form in three ways:
1971		1. **Graph (Dict)**: Dynamic transitions based on weights.
1972		2. **Sequence (List)**: A fixed order of sections.
1973		3. **Generator**: A Python generator that yields `(name, bars)` pairs.
1974
1975		Parameters:
1976			sections: The form definition (Dict, List, or Generator).
1977			loop: Whether to cycle back to the start (List mode only).
1978			start: The section to start with (Graph mode only).
1979
1980		Example:
1981			```python
1982			# A simple pop structure
1983			comp.form([
1984				("verse", 8),
1985				("chorus", 8),
1986				("verse", 8),
1987				("chorus", 16)
1988			])
1989			```
1990		"""
1991
1992		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, reschedule_lookahead: float = 1, voice_leading: bool = False, device: Union[int, str, NoneType] = None) -> Callable:
2045	def pattern (
2046		self,
2047		channel: int,
2048		beats: typing.Optional[float] = None,
2049		bars: typing.Optional[float] = None,
2050		steps: typing.Optional[float] = None,
2051		unit: typing.Optional[float] = None,
2052		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2053		reschedule_lookahead: float = 1,
2054		voice_leading: bool = False,
2055		device: subsequence.midi_utils.DeviceId = None,
2056	) -> typing.Callable:
2057
2058		"""
2059		Register a function as a repeating MIDI pattern.
2060
2061		The decorated function will be called once per cycle to 'rebuild' its
2062		content. This allows for generative logic that evolves over time.
2063
2064		Two ways to specify pattern length:
2065
2066		- **Duration mode** (default): use ``beats=`` or ``bars=``.
2067		  The grid defaults to sixteenth-note resolution.
2068		- **Step mode**: use ``steps=`` paired with ``unit=``.
2069		  The grid equals the step count, so ``p.hit_steps()`` indices map
2070		  directly to steps.
2071
2072		Parameters:
2073			channel: MIDI channel. By default uses 0-based numbering (0-15)
2074				matching the raw MIDI protocol. Set
2075				``zero_indexed_channels=False`` on the ``Composition`` to use
2076				1-based numbering (1-16) instead.
2077			beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar.
2078			bars: Duration in bars (4 beats each, assumes 4/4). ``bars=2`` = 8 beats.
2079			steps: Step count for step mode. Requires ``unit=``.
2080			unit: Duration of one step in beats (e.g. ``dur.SIXTEENTH``).
2081				Requires ``steps=``.
2082			drum_note_map: Optional mapping for drum instruments.
2083			reschedule_lookahead: Beats in advance to compute the next cycle.
2084			voice_leading: If True, chords in this pattern will automatically
2085				use inversions that minimize voice movement.
2086
2087		Example:
2088			```python
2089			@comp.pattern(channel=1, beats=4)
2090			def chords(p):
2091				p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)
2092
2093			@comp.pattern(channel=1, bars=2)
2094			def long_phrase(p):
2095				...
2096
2097			@comp.pattern(channel=1, steps=6, unit=dur.SIXTEENTH)
2098			def riff(p):
2099				p.sequence(steps=[0, 1, 3, 5], pitches=60)
2100			```
2101		"""
2102
2103		channel = self._resolve_channel(channel)
2104
2105		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit)
2106
2107		# Resolve device string name to index if possible now; otherwise store
2108		# the raw DeviceId and resolve it in _run() once all devices are open.
2109		resolved_device: subsequence.midi_utils.DeviceId = device
2110
2111		def decorator (fn: typing.Callable) -> typing.Callable:
2112
2113			"""
2114			Wrap the builder function and register it as a pending pattern.
2115			During live sessions, hot-swap an existing pattern's builder instead.
2116			"""
2117
2118			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
2119			if self._is_live and fn.__name__ in self._running_patterns:
2120				running = self._running_patterns[fn.__name__]
2121				running._builder_fn = fn
2122				running._wants_chord = _fn_has_parameter(fn, "chord")
2123				logger.info(f"Hot-swapped pattern: {fn.__name__}")
2124				return fn
2125
2126			pending = _PendingPattern(
2127				builder_fn = fn,
2128				channel = channel,  # already resolved to 0-indexed
2129				length = beat_length,
2130				default_grid = default_grid,
2131				drum_note_map = drum_note_map,
2132				reschedule_lookahead = reschedule_lookahead,
2133				voice_leading = voice_leading,
2134				# For int/None: resolve immediately.  For str: store 0 as
2135				# placeholder; _resolve_pending_devices() fixes it in _run().
2136				device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device,
2137				raw_device = resolved_device,
2138			)
2139
2140			self._pending_patterns.append(pending)
2141
2142			return fn
2143
2144		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.
  • 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, reschedule_lookahead: float = 1, voice_leading: bool = False, device: Union[int, str, NoneType] = None) -> None:
2146	def layer (
2147		self,
2148		*builder_fns: typing.Callable,
2149		channel: int,
2150		beats: typing.Optional[float] = None,
2151		bars: typing.Optional[float] = None,
2152		steps: typing.Optional[float] = None,
2153		unit: typing.Optional[float] = None,
2154		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2155		reschedule_lookahead: float = 1,
2156		voice_leading: bool = False,
2157		device: subsequence.midi_utils.DeviceId = None,
2158	) -> None:
2159
2160		"""
2161		Combine multiple functions into a single MIDI pattern.
2162
2163		This is useful for composing complex patterns out of reusable
2164		building blocks (e.g., a 'kick' function and a 'snare' function).
2165
2166		See ``pattern()`` for the full description of ``beats``, ``bars``,
2167		``steps``, and ``unit``.
2168
2169		Parameters:
2170			builder_fns: One or more pattern builder functions.
2171			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
2172			beats: Duration in beats (quarter notes).
2173			bars: Duration in bars (4 beats each, assumes 4/4).
2174			steps: Step count for step mode. Requires ``unit=``.
2175			unit: Duration of one step in beats. Requires ``steps=``.
2176			drum_note_map: Optional mapping for drum instruments.
2177			reschedule_lookahead: Beats in advance to compute the next cycle.
2178			voice_leading: If True, chords use smooth voice leading.
2179		"""
2180
2181		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit)
2182
2183		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
2184
2185		if wants_chord:
2186
2187			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
2188
2189				for fn in builder_fns:
2190					if _fn_has_parameter(fn, "chord"):
2191						fn(p, chord)
2192					else:
2193						fn(p)
2194
2195		else:
2196
2197			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
2198
2199				for fn in builder_fns:
2200					fn(p)
2201
2202		resolved = self._resolve_channel(channel)
2203
2204		pending = _PendingPattern(
2205			builder_fn = merged_builder,
2206			channel = resolved,  # already resolved to 0-indexed
2207			length = beat_length,
2208			default_grid = default_grid,
2209			drum_note_map = drum_note_map,
2210			reschedule_lookahead = reschedule_lookahead,
2211			voice_leading = voice_leading,
2212				device = 0 if (device is None or isinstance(device, str)) else device,
2213			raw_device = device,
2214		)
2215
2216		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.
  • 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, chord: bool = False, device: Union[int, str, NoneType] = None) -> None:
2218	def trigger (
2219		self,
2220		fn: typing.Callable,
2221		channel: int,
2222		beats: typing.Optional[float] = None,
2223		bars: typing.Optional[float] = None,
2224		steps: typing.Optional[float] = None,
2225		unit: typing.Optional[float] = None,
2226		quantize: float = 0,
2227		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
2228		chord: bool = False,
2229		device: subsequence.midi_utils.DeviceId = None,
2230	) -> None:
2231
2232		"""
2233		Trigger a one-shot pattern immediately or on a quantized boundary.
2234
2235		This is useful for real-time response to sensors, OSC messages, or other
2236		external events. The builder function is called immediately with a fresh
2237		PatternBuilder, and the generated events are injected into the queue at
2238		the specified quantize boundary.
2239
2240		The builder function has the same API as a ``@composition.pattern``
2241		decorated function and can use all PatternBuilder methods: ``p.note()``,
2242		``p.euclidean()``, ``p.arpeggio()``, and so on.
2243
2244		See ``pattern()`` for the full description of ``beats``, ``bars``,
2245		``steps``, and ``unit``. Default is 1 beat.
2246
2247		Parameters:
2248			fn: The pattern builder function (same signature as ``@comp.pattern``).
2249			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
2250			beats: Duration in beats (quarter notes, default 1).
2251			bars: Duration in bars (4 beats each, assumes 4/4).
2252			steps: Step count for step mode. Requires ``unit=``.
2253			unit: Duration of one step in beats. Requires ``steps=``.
2254			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
2255				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
2256				constants from ``subsequence.constants.durations``.
2257			drum_note_map: Optional drum name mapping for this pattern.
2258			chord: If ``True``, the builder function receives the current chord as
2259				a second parameter (same as ``@composition.pattern``).
2260
2261		Example:
2262			```python
2263			# Immediate single note
2264			composition.trigger(
2265				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
2266				channel=0
2267			)
2268
2269			# Quantized fill (next bar)
2270			import subsequence.constants.durations as dur
2271			composition.trigger(
2272				lambda p: p.euclidean("snare", pulses=7, velocity=90),
2273				channel=9,
2274				drum_note_map=gm_drums.GM_DRUM_MAP,
2275				quantize=dur.WHOLE
2276			)
2277
2278			# With chord context
2279			composition.trigger(
2280				lambda p: p.arpeggio(p.chord.tones(root=60), spacing=dur.SIXTEENTH),
2281				channel=0,
2282				quantize=dur.QUARTER,
2283				chord=True
2284			)
2285			```
2286		"""
2287
2288		# Resolve channel numbering
2289		resolved_channel = self._resolve_channel(channel)
2290
2291		beat_length, default_grid = self._resolve_length(beats, bars, steps, unit, default=1.0)
2292
2293		# Resolve device index
2294		resolved_device_idx = self._resolve_device_id(device)
2295
2296		# Create a temporary Pattern
2297		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx)
2298
2299		# Create a PatternBuilder
2300		builder = subsequence.pattern_builder.PatternBuilder(
2301			pattern=pattern,
2302			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
2303			drum_note_map=drum_note_map,
2304			section=self._form_state.get_section_info() if self._form_state else None,
2305			bar=self._builder_bar,
2306			conductor=self.conductor,
2307			rng=random.Random(),  # Fresh random state for each trigger
2308			tweaks={},
2309			default_grid=default_grid,
2310			data=self.data
2311		)
2312
2313		# Call the builder function
2314		try:
2315
2316			if chord and self._harmonic_state is not None:
2317				current_chord = self._harmonic_state.get_current_chord()
2318				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
2319				fn(builder, injected)
2320
2321			else:
2322				fn(builder)
2323
2324		except Exception:
2325			logger.exception("Error in trigger builder — pattern will be silent")
2326			return
2327
2328		# Calculate the start pulse based on quantize
2329		current_pulse = self._sequencer.pulse_count
2330		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
2331
2332		if quantize == 0:
2333			# Immediate: use current pulse
2334			start_pulse = current_pulse
2335
2336		else:
2337			# Quantize to the next multiple of (quantize * pulses_per_beat)
2338			quantize_pulses = int(quantize * pulses_per_beat)
2339			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
2340
2341		# Schedule the pattern for one-shot execution
2342		try:
2343			loop = asyncio.get_running_loop()
2344			# Already on the event loop
2345			asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse))
2346
2347		except RuntimeError:
2348			# Not on the event loop — schedule via call_soon_threadsafe
2349			if self._sequencer._event_loop is not None:
2350				asyncio.run_coroutine_threadsafe(
2351					self._sequencer.schedule_pattern(pattern, start_pulse),
2352					loop=self._sequencer._event_loop
2353				)
2354			else:
2355				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.
  • 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
2357	@property
2358	def is_clock_following (self) -> bool:
2359
2360		"""True if either the primary or any additional device is following external clock."""
2361
2362		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:
2365	def play (self) -> None:
2366
2367		"""
2368		Start the composition.
2369
2370		This call blocks until the program is interrupted (e.g., via Ctrl+C).
2371		It initializes the MIDI hardware, launches the background sequencer,
2372		and begins playback.
2373		"""
2374
2375		try:
2376			asyncio.run(self._run())
2377
2378		except KeyboardInterrupt:
2379			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:
2382	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
2383
2384		"""Render the composition to a MIDI file without real-time playback.
2385
2386		Runs the sequencer as fast as possible (no timing delays) and stops
2387		when the first active limit is reached.  The result is saved as a
2388		standard MIDI file that can be imported into any DAW.
2389
2390		All patterns, scheduled callbacks, and harmony logic run exactly as
2391		they would during live playback — BPM transitions, generative fills,
2392		and probabilistic gates all work in render mode.  The only difference
2393		is that time is simulated rather than wall-clock driven.
2394
2395		Parameters:
2396			bars: Number of bars to render, or ``None`` for no bar limit
2397			      (default ``None``).  When both *bars* and *max_minutes* are
2398			      active, playback stops at whichever limit is reached first.
2399			filename: Output MIDI filename (default ``"render.mid"``).
2400			max_minutes: Safety cap on the length of rendered MIDI in minutes
2401			             (default ``60.0``).  Pass ``None`` to disable the time
2402			             cap — you must then provide an explicit *bars* value.
2403
2404		Raises:
2405			ValueError: If both *bars* and *max_minutes* are ``None``, which
2406			            would produce an infinite render.
2407
2408		Examples:
2409			```python
2410			# Default: renders up to 60 minutes of MIDI content.
2411			composition.render()
2412
2413			# Render exactly 64 bars (time cap still active as backstop).
2414			composition.render(bars=64, filename="demo.mid")
2415
2416			# Render up to 5 minutes of an infinite generative composition.
2417			composition.render(max_minutes=5, filename="five_min.mid")
2418
2419			# Remove the time cap — must supply bars instead.
2420			composition.render(bars=128, max_minutes=None, filename="long.mid")
2421			```
2422		"""
2423
2424		if bars is None and max_minutes is None:
2425			raise ValueError(
2426				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
2427				"Passing both as None would produce an infinite render."
2428			)
2429
2430		self._sequencer.recording = True
2431		self._sequencer.record_filename = filename
2432		self._sequencer.render_mode = True
2433		self._sequencer.render_bars = bars if bars is not None else 0
2434		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
2435		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:
345def register_scale (
346	name: str,
347	intervals: typing.List[int],
348	qualities: typing.Optional[typing.List[str]] = None
349) -> None:
350
351	"""
352	Register a custom scale for use with ``p.quantize()`` and
353	``scale_pitch_classes()``.
354
355	Parameters:
356		name: Scale name (used in ``p.quantize(key, name)``).
357		intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]``
358			for Hirajōshi). Must start with 0 and contain values 0–11.
359		qualities: Optional chord quality per scale degree (e.g.
360			``["minor", "major", "minor", "major", "diminished"]``).
361			Required only if you want to use the scale with
362			``diatonic_chords()`` or ``diatonic_chord_sequence()``.
363
364	Example::
365
366		import subsequence
367
368		subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11])
369
370		@comp.pattern(channel=0, length=4)
371		def melody (p):
372			p.note(60, beat=0)
373			p.quantize("C", "raga_bhairav")
374	"""
375
376	if not intervals or intervals[0] != 0:
377		raise ValueError("intervals must start with 0")
378	if any(i < 0 or i > 11 for i in intervals):
379		raise ValueError("intervals must contain values between 0 and 11")
380	if qualities is not None and len(qualities) != len(intervals):
381		raise ValueError(
382			f"qualities length ({len(qualities)}) must match "
383			f"intervals length ({len(intervals)})"
384		)
385
386	INTERVAL_DEFINITIONS[name] = intervals
387	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]:
210def scale_notes (
211	key: str,
212	mode: str = "ionian",
213	low: int = 60,
214	high: int = 72,
215	count: typing.Optional[int] = None,
216) -> typing.List[int]:
217
218	"""Return MIDI note numbers for a scale within a pitch range.
219
220	Parameters:
221		key: Scale root as a note name (``"C"``, ``"F#"``, ``"Bb"``, etc.).
222		     This acts as a **pitch-class filter only** — it determines which
223		     semitone positions (0–11) are valid members of the scale, but does
224		     not affect which octave notes are drawn from. Notes are selected
225		     starting from ``low`` upward; ``key`` controls *which* notes are
226		     kept, not where the sequence starts. To guarantee the first
227		     returned note is the root, ``low`` must be a MIDI number whose
228		     pitch class matches ``key``. When starting from an arbitrary MIDI
229		     number, derive the key name with
230		     ``subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]``.
231		mode: Scale mode name. Supports all keys of :data:`SCALE_MODE_MAP`
232		      (e.g. ``"ionian"``, ``"dorian"``, ``"natural_minor"``,
233		      ``"major_pentatonic"``). Use :func:`register_scale` for custom scales.
234		low: Lowest MIDI note (inclusive). When ``count`` is set, this is
235		     the starting note from which the scale ascends. **If ``low`` is
236		     not a member of the scale defined by ``key``, it is silently
237		     skipped** and the first returned note will be the next in-scale
238		     pitch above ``low``.
239		high: Highest MIDI note (inclusive). Ignored when ``count`` is set.
240		count: Exact number of notes to return. Notes ascend from ``low``
241		       through successive scale degrees, cycling into higher octaves
242		       as needed. When ``None`` (default), all scale tones between
243		       ``low`` and ``high`` are returned.
244
245	Returns:
246		Sorted list of MIDI note numbers.
247
248	Examples:
249		```python
250		import subsequence
251		import subsequence.constants.midi_notes as notes
252
253		# C major: all tones from middle C to C5
254		subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5)
255		# → [60, 62, 64, 65, 67, 69, 71, 72]
256
257		# E natural minor (aeolian) across one octave
258		subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3)
259		# → [40, 42, 43, 45, 47, 48, 50, 52]
260
261		# 15 notes of A minor pentatonic ascending from A3
262		subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15)
263		# → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91]
264
265		# Misalignment: key="E" but low=C4 — first note is C, not E
266		subsequence.scale_notes("E", "minor", low=60, count=4)
267		# → [60, 62, 64, 67]  (C D E G — all in E natural minor, but starts on C)
268
269		# Fix: derive key name from root_pitch so low is always in the scale
270		root_pitch = 64  # E4
271		key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]  # → "E"
272		subsequence.scale_notes(key, "minor", low=root_pitch, count=4)
273		# → [64, 66, 67, 69]  (E F# G A — starts on the root)
274		```
275	"""
276
277	key_pc = subsequence.chords.key_name_to_pc(key)
278	pcs = set(scale_pitch_classes(key_pc, mode))
279
280	if count is not None:
281		if not pcs:
282			return []
283		result: typing.List[int] = []
284		pitch = low
285		while len(result) < count and pitch <= 127:
286			if pitch % 12 in pcs:
287				result.append(pitch)
288			pitch += 1
289		return result
290
291	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]:
110def bank_select (bank: int) -> typing.Tuple[int, int]:
111
112	"""
113	Convert a 14-bit MIDI bank number to (MSB, LSB) for use with
114	``p.program_change()``.
115
116	MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and
117	CC 32 (Bank LSB).  Together they encode a 14-bit bank number in the
118	range 0–16,383:
119
120	    MSB = bank // 128   (upper 7 bits, sent on CC 0)
121	    LSB = bank % 128    (lower 7 bits, sent on CC 32)
122
123	Args:
124		bank: Integer bank number, 0–16,383.  Values outside this range are
125		      clamped.
126
127	Returns:
128		``(msb, lsb)`` tuple, each value in 0–127.
129
130	Example:
131		```python
132		msb, lsb = subsequence.bank_select(128)   # → (1, 0)
133		p.program_change(48, bank_msb=msb, bank_lsb=lsb)
134		```
135	"""
136
137	bank = max(0, min(16383, bank))
138	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)