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.
  • Cognitive harmony engine. Chord progressions evolve via weighted transition graphs with adjustable gravity and Narmour-based melodic inertia. Eleven built-in palettes, automatic voice leading, and frozen progressions to lock some sections while others evolve freely.
  • 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, 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.
  • 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). 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=9, length=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, 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- **Cognitive harmony engine.** Chord progressions evolve via weighted
 29  transition graphs with adjustable gravity and Narmour-based melodic
 30  inertia. Eleven built-in palettes, automatic voice leading, and frozen
 31  progressions to lock some sections while others evolve freely.
 32- **Sub-microsecond clock.** Hybrid sleep+spin timing achieves typical
 33  pulse jitter of < 5 us on Linux, with zero long-term drift.
 34- **Turn anything into music.** ``composition.schedule()`` runs any
 35  Python function on a beat cycle - APIs, sensors, files. Anything
 36  Python can reach becomes a musical parameter.
 37- **Pure MIDI, zero sound engine.** No audio synthesis, no heavyweight
 38  dependencies. Route to hardware synths, drum machines, Eurorack, or
 39  software instruments.
 40
 41Composition tools:
 42
 43- **Rhythm and feel.** Euclidean and Bresenham generators, multi-voice
 44  weighted Bresenham distribution (``bresenham_poly()``), ghost note
 45  layers (``ghost_fill()``), position-aware note removal (``thin()`` -
 46  the musical inverse of ``ghost_fill``), evolving cellular-automaton
 47  rhythms (``cellular_1d()``, ``cellular_2d()``), smooth Perlin noise (``perlin_1d()``,
 48  ``perlin_2d()``, ``perlin_1d_sequence()``, ``perlin_2d_grid()``),
 49  deterministic chaos sequences (``logistic_map()``), pink 1/f noise
 50  (``pink_noise()``), L-system string rewriting (``p.lsystem()``),
 51  Markov-chain generation (``p.markov()``), aperiodic binary rhythms
 52  (``p.thue_morse()``), golden-ratio beat placement (``p.fibonacci()``),
 53  Gray-Scott reaction-diffusion patterns (``p.reaction_diffusion()``),
 54  Lorenz strange-attractor generation (``p.lorenz()``), exhaustive
 55  pitch-subsequence melodies (``p.de_bruijn()``), step-wise melodies
 56  with guaranteed pitch diversity (``p.self_avoiding_walk()``), drones
 57  and explicit note on/off events (``p.drone()``, ``p.drone_off()``,
 58  ``p.silence()``),
 59  groove templates (``Groove.swing()``, ``Groove.from_agr()``), swing via
 60  ``p.swing()`` (a shortcut for ``Groove.swing()``), randomize,
 61  velocity shaping, dropout, per-step probability, and polyrhythms
 62  via independent pattern lengths.
 63- **Melody generation.** ``p.melody()`` with ``MelodicState`` applies
 64  the Narmour Implication-Realization model to single-note lines:
 65  continuation after small steps, reversal after large leaps, chord-tone
 66  weighting, range gravity, and pitch-diversity penalty.  History persists
 67  across bar rebuilds for natural phrase continuity.
 68- **Expression.** CC messages/ramps, pitch bend, note-correlated
 69  bend/portamento/slide, program changes, SysEx, and OSC output - all
 70  from within patterns.
 71- **Form and structure.** Musical form as a weighted graph, ordered list,
 72  or generator. Patterns read ``p.section`` to adapt. Conductor signals
 73  (LFOs, ramps) shape intensity over time.
 74- **Mini-notation.** ``p.seq("x x [x x] x", pitch="kick")`` - concise
 75  string syntax for rhythms, subdivisions, and per-step probability.
 76- **Scales and quantization.** ``p.quantize()`` snaps notes to any
 77  scale. ``scale_notes()`` generates a list of MIDI note numbers from
 78  a key, mode, and range or note count - useful for arpeggios, Markov
 79  chains, and melodic walks. Built-in western and non-western modes,
 80  plus ``register_scale()`` for your own.
 81- **Randomness tools.** Weighted choice, no-repeat shuffle, random
 82  walk, probability gates. Deterministic seeding (``seed=42``) makes
 83  every decision repeatable.
 84- **Pattern transforms.** Legato, staccato, reverse, double/half-time,
 85  shift, transpose, invert, randomize, and conditional ``p.every()``.
 86
 87Integration:
 88
 89- **MIDI clock.** Master (``clock_output()``) or follower
 90  (``clock_follow=True``). Sync to a DAW or drive hardware.
 91- **Hardware control.** CC input mapping from knobs/faders to
 92  ``composition.data``; patterns read and write the same dict via
 93  ``p.data`` for both external data access and cross-pattern
 94  communication. OSC for bidirectional communication with mixers,
 95  lighting, visuals.
 96- **Live coding.** Hot-swap patterns, change tempo, mute/unmute, and
 97  tweak parameters during playback via a built-in TCP eval server.
 98- **Hotkeys.** Single keystrokes to jump sections, toggle mutes, or
 99  fire any action - with optional bar-boundary quantization.
100- **Real-time pattern triggering.** ``composition.trigger()`` generates
101  one-shot patterns in response to sensors, OSC, or any event.
102- **Terminal display.** Live status line (BPM, bar, section, chord).
103  Add ``grid=True`` for an ASCII pattern grid showing velocity and
104  sustain - makes legato and staccato visually distinct at a glance.
105  Add ``grid_scale=2`` to zoom in horizontally, revealing swing and
106  groove micro-timing.
107- **Web UI Dashboard (Beta).** Enable with ``composition.web_ui()`` to 
108  broadcast live composition metadata and visualize piano-roll pattern 
109  grids in a reactive HTTP/WebSocket browser dashboard.
110- **Ableton Link.** Industry-standard wireless tempo/phase sync
111  (``comp.link()``; requires ``pip install subsequence[link]``).
112  Any Link-enabled app on the same LAN — Ableton Live, iOS synths,
113  other Subsequence instances — stays in time automatically.
114- **Recording.** Record to standard MIDI file. Render to file without
115  waiting for real-time playback.
116
117Minimal example:
118
119    ```python
120    import subsequence
121    import subsequence.constants.instruments.gm_drums as gm_drums
122
123    comp = subsequence.Composition(bpm=120)
124
125    @comp.pattern(channel=9, length=4, drum_note_map=gm_drums.GM_DRUM_MAP)
126    def drums (p):
127        (p.hit_steps("kick_1",        [0, 4, 8, 12], velocity=100)
128          .hit_steps("snare_1",       [4, 12],        velocity=90)
129          .hit_steps("hi_hat_closed", range(16),      velocity=70))
130
131    comp.play()
132    ```
133
134Community and Feedback:
135
136- **Discussions:** Chat and ask questions at https://github.com/simonholliday/subsequence/discussions
137- **Issues:** Report bugs and request features at https://github.com/simonholliday/subsequence/issues
138
139Package-level exports: ``Composition``, ``Groove``, ``MelodicState``, ``register_scale``, ``scale_notes``, ``bank_select``.
140"""
141
142import subsequence.composition
143import subsequence.groove
144import subsequence.intervals
145import subsequence.melodic_state
146import subsequence.midi_utils
147
148
149Composition = subsequence.composition.Composition
150Groove = subsequence.groove.Groove
151MelodicState = subsequence.melodic_state.MelodicState
152register_scale = subsequence.intervals.register_scale
153scale_notes = subsequence.intervals.scale_notes
154bank_select = subsequence.midi_utils.bank_select
class Composition:
 551class Composition:
 552
 553	"""
 554	The top-level controller for a musical piece.
 555	
 556	The `Composition` object manages the global clock (Sequencer), the harmonic
 557	progression (HarmonicState), the song structure (subsequence.form_state.FormState), and all MIDI patterns.
 558	It serves as the main entry point for defining your music.
 559	
 560	Typical workflow:
 561	1. Initialize `Composition` with BPM and Key.
 562	2. Define harmony and form (optional).
 563	3. Register patterns using the `@composition.pattern` decorator.
 564	4. Call `composition.play()` to start the music.
 565	"""
 566
 567	def __init__ (
 568		self,
 569		output_device: typing.Optional[str] = None,
 570		bpm: float = 120,
 571		time_signature: typing.Tuple[int, int] = (4, 4),
 572		key: typing.Optional[str] = None,
 573		seed: typing.Optional[int] = None,
 574		record: bool = False,
 575		record_filename: typing.Optional[str] = None,
 576		zero_indexed_channels: bool = True
 577	) -> None:
 578
 579		"""
 580		Initialize a new composition.
 581
 582		Parameters:
 583			output_device: The name of the MIDI output port to use. If `None`, 
 584				Subsequence will attempt to find a device, prompting if necessary.
 585			bpm: Initial tempo in beats per minute (default 120).
 586			key: The root key of the piece (e.g., "C", "F#", "Bb").
 587				Required if you plan to use `harmony()`.
 588			seed: An optional integer for deterministic randomness. When set, 
 589				every random decision (chord choices, drum probability, etc.) 
 590				will be identical on every run.
 591			record: When True, record all MIDI events to a file.
 592			record_filename: Optional filename for the recording (defaults to timestamp).
 593			zero_indexed_channels: When False, MIDI channels use
 594				1-based numbering (1-16) matching instrument labelling.
 595				Channel 10 is drums, the way musicians and hardware panels
 596				show it. When True (default), channels use 0-based numbering (0-15)
 597				matching the raw MIDI protocol.
 598
 599		Example:
 600			```python
 601			comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
 602			```
 603		"""
 604
 605		self.output_device = output_device
 606		self.bpm = bpm
 607		self.time_signature = time_signature
 608		self.key = key
 609		self._seed: typing.Optional[int] = seed
 610		self._zero_indexed_channels: bool = zero_indexed_channels
 611
 612		self._sequencer = subsequence.sequencer.Sequencer(
 613			output_device_name = output_device,
 614			initial_bpm = bpm,
 615			time_signature = time_signature,
 616			record = record,
 617			record_filename = record_filename
 618		)
 619
 620		self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None
 621		self._harmony_cycle_beats: typing.Optional[int] = None
 622		self._harmony_reschedule_lookahead: float = 1
 623		self._section_progressions: typing.Dict[str, Progression] = {}
 624		self._pending_patterns: typing.List[_PendingPattern] = []
 625		self._pending_scheduled: typing.List[_PendingScheduled] = []
 626		self._form_state: typing.Optional[subsequence.form_state.FormState] = None
 627		self._builder_bar: int = 0
 628		self._display: typing.Optional[subsequence.display.Display] = None
 629		self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None
 630		self._is_live: bool = False
 631		self._running_patterns: typing.Dict[str, typing.Any] = {}
 632		self._input_device: typing.Optional[str] = None
 633		self._clock_follow: bool = False
 634		self._clock_output: bool = False
 635		self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = []
 636		self.data: typing.Dict[str, typing.Any] = {}
 637		self._osc_server: typing.Optional[subsequence.osc.OscServer] = None
 638		self.conductor = subsequence.conductor.Conductor()
 639		self._web_ui_enabled: bool = False
 640		self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None
 641		self._link_quantum: typing.Optional[float] = None
 642
 643		# Hotkey state — populated by hotkeys() and hotkey().
 644		self._hotkeys_enabled: bool = False
 645		self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {}
 646		self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = []
 647		self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None
 648
 649	def _resolve_channel (self, channel: int) -> int:
 650
 651		"""
 652		Convert a user-supplied MIDI channel to the 0-indexed value used internally.
 653
 654		When ``zero_indexed_channels`` is True, the channel is validated as
 655		0-15 and returned unchanged. When False (1-indexed), the channel is
 656		validated as 1-16 and decremented by one.
 657		"""
 658
 659		if self._zero_indexed_channels:
 660			if not 0 <= channel <= 15:
 661				raise ValueError(f"MIDI channel must be 0-15 (zero_indexed_channels=True), got {channel}")
 662			return channel
 663		else:
 664			if not 1 <= channel <= 16:
 665				raise ValueError(f"MIDI channel must be 1-16, got {channel}")
 666			return channel - 1
 667
 668	@property
 669	def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]:
 670		"""The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called."""
 671		return self._harmonic_state
 672
 673	@property
 674	def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]:
 675		"""The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called."""
 676		return self._form_state
 677
 678	@property
 679	def sequencer (self) -> subsequence.sequencer.Sequencer:
 680		"""The underlying ``Sequencer`` instance."""
 681		return self._sequencer
 682
 683	@property
 684	def running_patterns (self) -> typing.Dict[str, typing.Any]:
 685		"""The currently active patterns, keyed by name."""
 686		return self._running_patterns
 687
 688	@property
 689	def builder_bar (self) -> int:
 690		"""Current bar index used by pattern builders."""
 691		return self._builder_bar
 692
 693	def _require_harmonic_state (self) -> subsequence.harmonic_state.HarmonicState:
 694		"""Return the active HarmonicState, raising ValueError if none is configured."""
 695		if self._harmonic_state is None:
 696			raise ValueError(
 697				"harmony() must be called before this action — "
 698				"no harmonic state has been configured."
 699			)
 700		return self._harmonic_state
 701
 702	def harmony (
 703		self,
 704		style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major",
 705		cycle_beats: int = 4,
 706		dominant_7th: bool = True,
 707		gravity: float = 1.0,
 708		nir_strength: float = 0.5,
 709		minor_turnaround_weight: float = 0.0,
 710		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
 711		reschedule_lookahead: float = 1
 712	) -> None:
 713
 714		"""
 715		Configure the harmonic logic and chord change intervals.
 716
 717		Subsequence uses a weighted transition graph to choose the next chord.
 718		You can influence these choices using 'gravity' (favoring the tonic) and
 719		'NIR strength' (melodic inertia based on Narmour's model).
 720
 721		Parameters:
 722			style: The harmonic style to use. Built-in: "functional_major"
 723				(alias "diatonic_major"), "turnaround", "aeolian_minor",
 724				"phrygian_minor", "lydian_major", "dorian_minor",
 725				"chromatic_mediant", "suspended", "mixolydian", "whole_tone",
 726				"diminished". See README for full descriptions.
 727			cycle_beats: How many beats each chord lasts (default 4).
 728			dominant_7th: Whether to include V7 chords (default True).
 729			gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
 730			nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement
 731				expectations.
 732			minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
 733			root_diversity: Root-repetition damping (0.0 to 1.0). Each recent
 734				chord sharing a candidate's root reduces the weight to 40% at
 735				the default (0.4). Set to 1.0 to disable.
 736			reschedule_lookahead: How many beats in advance to calculate the
 737				next chord.
 738
 739		Example:
 740			```python
 741			# A moody minor progression that changes every 8 beats
 742			comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
 743			```
 744		"""
 745
 746		if self.key is None:
 747			raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor")
 748
 749		preserved_history: typing.List[subsequence.chords.Chord] = []
 750		preserved_current: typing.Optional[subsequence.chords.Chord] = None
 751
 752		if self._harmonic_state is not None:
 753			preserved_history = self._harmonic_state.history.copy()
 754			preserved_current = self._harmonic_state.current_chord
 755
 756		self._harmonic_state = subsequence.harmonic_state.HarmonicState(
 757			key_name = self.key,
 758			graph_style = style,
 759			include_dominant_7th = dominant_7th,
 760			key_gravity_blend = gravity,
 761			nir_strength = nir_strength,
 762			minor_turnaround_weight = minor_turnaround_weight,
 763			root_diversity = root_diversity
 764		)
 765
 766		if preserved_history:
 767			self._harmonic_state.history = preserved_history
 768		if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current):
 769			self._harmonic_state.current_chord = preserved_current
 770
 771		self._harmony_cycle_beats = cycle_beats
 772		self._harmony_reschedule_lookahead = reschedule_lookahead
 773
 774	def freeze (self, bars: int) -> "Progression":
 775
 776		"""Capture a chord progression from the live harmony engine.
 777
 778		Runs the harmony engine forward by *bars* chord changes, records each
 779		chord, and returns it as a :class:`Progression` that can be bound to a
 780		form section with :meth:`section_chords`.
 781
 782		The engine state **advances** — successive ``freeze()`` calls produce a
 783		continuing compositional journey so section progressions feel like parts
 784		of a whole rather than isolated islands.
 785
 786		Parameters:
 787			bars: Number of chords to capture (one per harmony cycle).
 788
 789		Returns:
 790			A :class:`Progression` with the captured chords and trailing
 791			history for NIR continuity.
 792
 793		Raises:
 794			ValueError: If :meth:`harmony` has not been called first.
 795
 796		Example::
 797
 798			composition.harmony(style="functional_major", cycle_beats=4)
 799			verse  = composition.freeze(8)   # 8 chords, engine advances
 800			chorus = composition.freeze(4)   # next 4 chords, continuing on
 801			composition.section_chords("verse",  verse)
 802			composition.section_chords("chorus", chorus)
 803		"""
 804
 805		hs = self._require_harmonic_state()
 806
 807		if bars < 1:
 808			raise ValueError("bars must be at least 1")
 809		collected: typing.List[subsequence.chords.Chord] = [hs.current_chord]
 810
 811		for _ in range(bars - 1):
 812			hs.step()
 813			collected.append(hs.current_chord)
 814
 815		# Advance past the last captured chord so the next freeze() call or
 816		# live playback does not duplicate it.
 817		hs.step()
 818
 819		return Progression(
 820			chords = tuple(collected),
 821			trailing_history = tuple(hs.history),
 822		)
 823
 824	def section_chords (self, section_name: str, progression: "Progression") -> None:
 825
 826		"""Bind a frozen :class:`Progression` to a named form section.
 827
 828		Every time *section_name* plays, the harmonic clock replays the
 829		progression's chords in order instead of calling the live engine.
 830		Sections without a bound progression continue generating live chords.
 831
 832		Parameters:
 833			section_name: Name of the section as defined in :meth:`form`.
 834			progression: The :class:`Progression` returned by :meth:`freeze`.
 835
 836		Raises:
 837			ValueError: If the form has been configured and *section_name* is
 838				not a known section name.
 839
 840		Example::
 841
 842			composition.section_chords("verse",  verse_progression)
 843			composition.section_chords("chorus", chorus_progression)
 844			# "bridge" is not bound — it generates live chords
 845		"""
 846
 847		if (
 848			self._form_state is not None
 849			and self._form_state._section_bars is not None
 850			and section_name not in self._form_state._section_bars
 851		):
 852			known = ", ".join(sorted(self._form_state._section_bars))
 853			raise ValueError(
 854				f"Section '{section_name}' not found in form. "
 855				f"Known sections: {known}"
 856			)
 857
 858		self._section_progressions[section_name] = progression
 859
 860	def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None:
 861
 862		"""
 863		Register a callback for a sequencer event (e.g., "bar", "start", "stop").
 864		"""
 865
 866		self._sequencer.on_event(event_name, callback)
 867
 868
 869	# -----------------------------------------------------------------------
 870	# Hotkey API
 871	# -----------------------------------------------------------------------
 872
 873	def hotkeys (self, enabled: bool = True) -> None:
 874
 875		"""Enable or disable the global hotkey listener.
 876
 877		Must be called **before** :meth:`play` to take effect.  When enabled, a
 878		background thread reads single keystrokes from stdin without requiring
 879		Enter.  The ``?`` key is always reserved and lists all active bindings.
 880
 881		Hotkeys have zero impact on playback when disabled — the listener
 882		thread is never started.
 883
 884		Args:
 885		    enabled: ``True`` (default) to enable hotkeys; ``False`` to disable.
 886
 887		Example::
 888
 889		    composition.hotkeys()
 890		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
 891		    composition.play()
 892		"""
 893
 894		self._hotkeys_enabled = enabled
 895
 896
 897	def hotkey (
 898		self,
 899		key:      str,
 900		action:   typing.Callable[[], None],
 901		quantize: int = 0,
 902		label:    typing.Optional[str] = None,
 903	) -> None:
 904
 905		"""Register a single-key shortcut that fires during playback.
 906
 907		The listener must be enabled first with :meth:`hotkeys`.
 908
 909		Most actions — form jumps, ``composition.data`` writes, and
 910		:meth:`tweak` calls — should use ``quantize=0`` (the default).  Their
 911		musical effect is naturally delayed to the next pattern rebuild cycle,
 912		which provides automatic musical quantization without extra configuration.
 913
 914		Use ``quantize=N`` for actions where you want an explicit bar-boundary
 915		guarantee, such as :meth:`mute` / :meth:`unmute`.
 916
 917		The ``?`` key is reserved and cannot be overridden.
 918
 919		Args:
 920		    key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``).
 921		    action: Zero-argument callable to execute.
 922		    quantize: ``0`` = execute immediately (default).  ``N`` = execute
 923		        on the next global bar number divisible by *N*.
 924		    label: Display name for the ``?`` help listing.  Auto-derived from
 925		        the function name or lambda body if omitted.
 926
 927		Raises:
 928		    ValueError: If ``key`` is the reserved ``?`` character, or if
 929		        ``key`` is not exactly one character.
 930
 931		Example::
 932
 933		    composition.hotkeys()
 934
 935		    # Immediate — musical effect happens at next pattern rebuild
 936		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
 937		    composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
 938
 939		    # Explicit 4-bar phrase boundary
 940		    composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
 941
 942		    # Named function — label is derived automatically
 943		    def drop_to_breakdown():
 944		        composition.form_jump("breakdown")
 945		        composition.mute("lead")
 946
 947		    composition.hotkey("d", drop_to_breakdown)
 948
 949		    composition.play()
 950		"""
 951
 952		if len(key) != 1:
 953			raise ValueError(f"hotkey key must be a single character, got {key!r}")
 954
 955		if key == _HOTKEY_RESERVED:
 956			raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.")
 957
 958		derived = label if label is not None else _derive_label(action)
 959
 960		self._hotkey_bindings[key] = HotkeyBinding(
 961			key      = key,
 962			action   = action,
 963			quantize = quantize,
 964			label    = derived,
 965		)
 966
 967
 968	def form_jump (self, section_name: str) -> None:
 969
 970		"""Jump the form to a named section immediately.
 971
 972		Delegates to :meth:`subsequence.form_state.FormState.jump_to`.  Only works when the
 973		composition uses graph-mode form (a dict passed to :meth:`form`).
 974
 975		The musical effect is heard at the *next pattern rebuild cycle* — already-
 976		queued MIDI notes are unaffected.  This natural delay means ``form_jump``
 977		is effective without needing explicit quantization.
 978
 979		Args:
 980		    section_name: The section to jump to.
 981
 982		Raises:
 983		    ValueError: If no form is configured, or the form is not in graph
 984		        mode, or *section_name* is unknown.
 985
 986		Example::
 987
 988		    composition.hotkey("c", lambda: composition.form_jump("chorus"))
 989		"""
 990
 991		if self._form_state is None:
 992			raise ValueError("form_jump() requires a form to be configured via composition.form().")
 993
 994		self._form_state.jump_to(section_name)
 995
 996
 997	def form_next (self, section_name: str) -> None:
 998
 999		"""Queue the next section — takes effect when the current section ends.
1000
1001		Unlike :meth:`form_jump`, this does not interrupt the current section.
1002		The queued section replaces the automatically pre-decided next section
1003		and takes effect at the natural section boundary.  The performer can
1004		change their mind by calling ``form_next`` again before the boundary.
1005
1006		Delegates to :meth:`subsequence.form_state.FormState.queue_next`.  Only works when the
1007		composition uses graph-mode form (a dict passed to :meth:`form`).
1008
1009		Args:
1010		    section_name: The section to queue.
1011
1012		Raises:
1013		    ValueError: If no form is configured, or the form is not in graph
1014		        mode, or *section_name* is unknown.
1015
1016		Example::
1017
1018		    composition.hotkey("c", lambda: composition.form_next("chorus"))
1019		"""
1020
1021		if self._form_state is None:
1022			raise ValueError("form_next() requires a form to be configured via composition.form().")
1023
1024		self._form_state.queue_next(section_name)
1025
1026
1027	def _list_hotkeys (self) -> None:
1028
1029		"""Log all active hotkey bindings (triggered by the ``?`` key).
1030
1031		Output appears via the standard logger so it scrolls cleanly above
1032		the :class:`~subsequence.display.Display` status line.
1033		"""
1034
1035		lines = ["Active hotkeys:"]
1036		for key in sorted(self._hotkey_bindings):
1037			b = self._hotkey_bindings[key]
1038			quant_str = "immediate" if b.quantize == 0 else f"quantize={b.quantize}"
1039			lines.append(f"  {key}  \u2192  {b.label}  ({quant_str})")
1040		lines.append(f"  ?  \u2192  list hotkeys")
1041		logger.info("\n".join(lines))
1042
1043
1044	def _process_hotkeys (self, bar: int) -> None:
1045
1046		"""Drain pending keystrokes and execute due actions.
1047
1048		Called on every ``"bar"`` event by the sequencer when hotkeys are
1049		enabled.  Handles both immediate (``quantize=0``) and quantized actions.
1050
1051		Immediate actions are executed directly from the keystroke listener
1052		thread (not here).  This method only processes quantized actions that
1053		were deferred to a bar boundary.
1054
1055		Args:
1056		    bar: The current global bar number from the sequencer.
1057		"""
1058
1059		if self._keystroke_listener is None:
1060			return
1061
1062		# Process newly arrived keys.
1063		for key in self._keystroke_listener.drain():
1064
1065			if key == _HOTKEY_RESERVED:
1066				self._list_hotkeys()
1067				continue
1068
1069			binding = self._hotkey_bindings.get(key)
1070			if binding is None:
1071				continue
1072
1073			if binding.quantize == 0:
1074				# Immediate — execute now (we're on the bar-event callback,
1075				# which is safe for all mutation methods).
1076				try:
1077					binding.action()
1078					logger.info(f"Hotkey '{key}' \u2192 {binding.label}")
1079				except Exception as exc:
1080					logger.warning(f"Hotkey '{key}' action raised: {exc}")
1081			else:
1082				# Defer until the next quantize boundary.
1083				self._pending_hotkey_actions.append(
1084					_PendingHotkeyAction(binding=binding, requested_bar=bar)
1085				)
1086
1087		# Fire any pending actions whose bar boundary has arrived.
1088		still_pending: typing.List[_PendingHotkeyAction] = []
1089
1090		for pending in self._pending_hotkey_actions:
1091			if bar % pending.binding.quantize == 0:
1092				try:
1093					pending.binding.action()
1094					logger.info(
1095						f"Hotkey '{pending.binding.key}' \u2192 {pending.binding.label} "
1096						f"(bar {bar})"
1097					)
1098				except Exception as exc:
1099					logger.warning(
1100						f"Hotkey '{pending.binding.key}' action raised: {exc}"
1101					)
1102			else:
1103				still_pending.append(pending)
1104
1105		self._pending_hotkey_actions = still_pending
1106
1107	def seed (self, value: int) -> None:
1108
1109		"""
1110		Set a random seed for deterministic, repeatable playback.
1111
1112		If a seed is set, Subsequence will produce the exact same sequence 
1113		every time you run the script. This is vital for finishing tracks or 
1114		reproducing a specific 'performance'.
1115
1116		Parameters:
1117			value: An integer seed.
1118
1119		Example:
1120			```python
1121			# Fix the randomness
1122			comp.seed(42)
1123			```
1124		"""
1125
1126		self._seed = value
1127
1128	def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
1129
1130		"""
1131		Enable or disable the live terminal dashboard.
1132
1133		When enabled, Subsequence uses a safe logging handler that allows a
1134		persistent status line (BPM, Key, Bar, Section, Chord) to stay at
1135		the bottom of the terminal while logs scroll above it.
1136
1137		Parameters:
1138			enabled: Whether to show the display (default True).
1139			grid: When True, render an ASCII grid visualisation of all
1140				running patterns above the status line. The grid updates
1141				once per bar, showing which steps have notes and at what
1142				velocity.
1143			grid_scale: Horizontal zoom factor for the grid (default
1144				``1.0``).  Higher values add visual columns between
1145				grid steps, revealing micro-timing from swing and groove.
1146				Snapped to the nearest integer internally for uniform
1147				marker spacing.
1148		"""
1149
1150		if enabled:
1151			self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale)
1152		else:
1153			self._display = None
1154
1155	def web_ui (self) -> None:
1156
1157		"""
1158		Enable the realtime Web UI Dashboard.
1159
1160		When enabled, Subsequence instantiates a WebSocket server that broadcasts 
1161		the current state, signals, and active patterns (with high-res timing and note data) 
1162		to any connected browser clients.
1163		"""
1164
1165		self._web_ui_enabled = True
1166
1167	def midi_input (self, device: str, clock_follow: bool = False) -> None:
1168
1169		"""
1170		Configure MIDI input for external sync and MIDI messages.
1171
1172		Parameters:
1173			device: The name of the MIDI input port.
1174			clock_follow: If True, Subsequence will slave its clock to incoming 
1175				MIDI Ticks. It will also follow MIDI Start/Stop/Continue 
1176				commands.
1177
1178		Example:
1179			```python
1180			# Slave Subsequence to an external hardware sequencer
1181			comp.midi_input("Scarlett 2i4", clock_follow=True)
1182			```
1183		"""
1184
1185		self._input_device = device
1186		self._clock_follow = clock_follow
1187
1188	def clock_output (self, enabled: bool = True) -> None:
1189
1190		"""
1191		Send MIDI timing clock to connected hardware.
1192
1193		When enabled, Subsequence acts as a MIDI clock master and sends
1194		standard clock messages on the output port: a Start message (0xFA)
1195		when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN),
1196		and a Stop message (0xFC) when playback ends.
1197
1198		This allows hardware synthesizers, drum machines, and effect units to
1199		slave their tempo to Subsequence automatically.
1200
1201		**Note:** Clock output is automatically disabled when ``midi_input()``
1202		is called with ``clock_follow=True``, to prevent a clock feedback loop.
1203
1204		Parameters:
1205			enabled: Whether to send MIDI clock (default True).
1206
1207		Example:
1208			```python
1209			comp = subsequence.Composition(bpm=120, output_device="...")
1210			comp.clock_output()   # hardware will follow Subsequence tempo
1211			```
1212		"""
1213
1214		self._clock_output = enabled
1215
1216
1217	def link (self, quantum: float = 4.0) -> "Composition":
1218
1219		"""
1220		Enable Ableton Link tempo and phase synchronisation.
1221
1222		When enabled, Subsequence joins the local Link session and slaves its
1223		clock to the shared network tempo and beat phase.  All other Link-enabled
1224		apps on the same LAN — Ableton Live, iOS synths, other Subsequence
1225		instances — will automatically stay in time.
1226
1227		Playback starts on the next bar boundary aligned to the Link quantum,
1228		so downbeats stay in sync across all participants.
1229
1230		Requires the ``link`` optional extra::
1231
1232		    pip install subsequence[link]
1233
1234		Parameters:
1235			quantum: Beat cycle length.  ``4.0`` (default) = one bar in 4/4 time.
1236			         Change this if your composition uses a different meter.
1237
1238		Example::
1239
1240		    comp = subsequence.Composition(bpm=120, key="C")
1241		    comp.link()          # join the Link session
1242		    comp.play()
1243
1244		    # On another machine / instance:
1245		    comp2 = subsequence.Composition(bpm=120)
1246		    comp2.link()         # tempo and phase will lock to comp
1247		    comp2.play()
1248
1249		Note:
1250		    ``set_bpm()`` proposes the new tempo to the Link network when Link
1251		    is active.  The network-authoritative tempo is applied on the next
1252		    pulse, so there may be a brief lag before the change is visible.
1253		"""
1254
1255		# Eagerly check that aalink is installed — fail early with a clear message.
1256		subsequence.link_clock._require_aalink()
1257
1258		self._link_quantum = quantum
1259		return self
1260
1261
1262	def cc_map (
1263		self,
1264		cc: int,
1265		key: str,
1266		channel: typing.Optional[int] = None,
1267		min_val: float = 0.0,
1268		max_val: float = 1.0
1269	) -> None:
1270
1271		"""
1272		Map an incoming MIDI CC to a ``composition.data`` key.
1273
1274		When the composition receives a CC message on the configured MIDI
1275		input port, the value is scaled from the CC range (0–127) to
1276		*[min_val, max_val]* and stored in ``composition.data[key]``.
1277
1278		This lets hardware knobs, faders, and expression pedals control live
1279		parameters without writing any callback code.
1280
1281		**Requires** ``midi_input()`` to be called first to open an input port.
1282
1283		Parameters:
1284			cc: MIDI Control Change number (0–127).
1285			key: The ``composition.data`` key to write.
1286			channel: If given, only respond to CC messages on this channel.
1287				Uses the same numbering convention as ``pattern()`` (0-15
1288				by default, or 1-16 with ``zero_indexed_channels=False``).
1289				``None`` matches any channel (default).
1290			min_val: Scaled minimum — written when CC value is 0 (default 0.0).
1291			max_val: Scaled maximum — written when CC value is 127 (default 1.0).
1292
1293		Example:
1294			```python
1295			comp.midi_input("Arturia KeyStep")
1296			comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
1297			comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader
1298			```
1299		"""
1300
1301		resolved_channel = self._resolve_channel(channel) if channel is not None else None
1302
1303		self._cc_mappings.append({
1304			'cc': cc,
1305			'key': key,
1306			'channel': resolved_channel,
1307			'min_val': min_val,
1308			'max_val': max_val,
1309		})
1310
1311
1312	def live (self, port: int = 5555) -> None:
1313
1314		"""
1315		Enable the live coding eval server.
1316
1317		This allows you to connect to a running composition using the 
1318		`subsequence.live_client` REPL and hot-swap pattern code or 
1319		modify variables in real-time.
1320
1321		Parameters:
1322			port: The TCP port to listen on (default 5555).
1323		"""
1324
1325		self._live_server = subsequence.live_server.LiveServer(self, port=port)
1326		self._is_live = True
1327
1328	def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None:
1329
1330		"""
1331		Enable bi-directional Open Sound Control (OSC).
1332
1333		Subsequence will listen for commands (like `/bpm` or `/mute`) and 
1334		broadcast its internal state (like `/chord` or `/bar`) over UDP.
1335
1336		Parameters:
1337			receive_port: Port to listen for incoming OSC messages (default 9000).
1338			send_port: Port to send state updates to (default 9001).
1339			send_host: The IP address to send updates to (default "127.0.0.1").
1340		"""
1341
1342		self._osc_server = subsequence.osc.OscServer(
1343			self,
1344			receive_port = receive_port,
1345			send_port = send_port,
1346			send_host = send_host
1347		)
1348
1349	def set_bpm (self, bpm: float) -> None:
1350
1351		"""
1352		Instantly change the tempo.
1353
1354		Parameters:
1355			bpm: The new tempo in beats per minute.
1356
1357		When Ableton Link is active, this proposes the new tempo to the Link
1358		network instead of applying it locally.  The network-authoritative tempo
1359		is picked up on the next pulse.
1360		"""
1361
1362		self._sequencer.set_bpm(bpm)
1363
1364		if not self._clock_follow and self._link_quantum is None:
1365			self.bpm = bpm
1366
1367	def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None:
1368
1369		"""
1370		Smoothly ramp the tempo to a target value over a number of bars.
1371
1372		Parameters:
1373			bpm: Target tempo in beats per minute.
1374			bars: Duration of the transition in bars.
1375			shape: Easing curve name.  Defaults to ``"linear"``.
1376			       ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural-
1377			       sounding tempo changes.  See :mod:`subsequence.easing` for all
1378			       available shapes.
1379
1380		Example:
1381			```python
1382			# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
1383			comp.target_bpm(140, bars=8, shape="ease_in_out")
1384			```
1385		"""
1386
1387		self._sequencer.set_target_bpm(bpm, bars, shape)
1388
1389	def live_info (self) -> typing.Dict[str, typing.Any]:
1390
1391		"""
1392		Return a dictionary containing the current state of the composition.
1393		
1394		Includes BPM, key, current bar, active section, current chord, 
1395		running patterns, and custom data.
1396		"""
1397
1398		section_info = None
1399		if self._form_state is not None:
1400			section = self._form_state.get_section_info()
1401			if section is not None:
1402				section_info = {
1403					"name": section.name,
1404					"bar": section.bar,
1405					"bars": section.bars,
1406					"progress": section.progress
1407				}
1408
1409		chord_name = None
1410		if self._harmonic_state is not None:
1411			chord = self._harmonic_state.get_current_chord()
1412			if chord is not None:
1413				chord_name = chord.name()
1414
1415		pattern_list = []
1416		channel_offset = 0 if self._zero_indexed_channels else 1
1417		for name, pat in self._running_patterns.items():
1418			pattern_list.append({
1419				"name": name,
1420				"channel": pat.channel + channel_offset,
1421				"length": pat.length,
1422				"cycle": pat._cycle_count,
1423				"muted": pat._muted,
1424				"tweaks": dict(pat._tweaks)
1425			})
1426
1427		return {
1428			"bpm": self._sequencer.current_bpm,
1429			"key": self.key,
1430			"bar": self._builder_bar,
1431			"section": section_info,
1432			"chord": chord_name,
1433			"patterns": pattern_list,
1434			"input_device": self._input_device,
1435			"clock_follow": self._clock_follow,
1436			"data": self.data
1437		}
1438
1439	def mute (self, name: str) -> None:
1440
1441		"""
1442		Mute a running pattern by name.
1443		
1444		The pattern continues to 'run' and increment its cycle count in 
1445		the background, but it will not produce any MIDI notes until unmuted.
1446
1447		Parameters:
1448			name: The function name of the pattern to mute.
1449		"""
1450
1451		if name not in self._running_patterns:
1452			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1453
1454		self._running_patterns[name]._muted = True
1455		logger.info(f"Muted pattern: {name}")
1456
1457	def unmute (self, name: str) -> None:
1458
1459		"""
1460		Unmute a previously muted pattern.
1461		"""
1462
1463		if name not in self._running_patterns:
1464			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1465
1466		self._running_patterns[name]._muted = False
1467		logger.info(f"Unmuted pattern: {name}")
1468
1469	def tweak (self, name: str, **kwargs: typing.Any) -> None:
1470
1471		"""Override parameters for a running pattern.
1472
1473		Values set here are available inside the pattern's builder
1474		function via ``p.param()``.  They persist across rebuilds
1475		until explicitly changed or cleared.  Changes take effect
1476		on the next rebuild cycle.
1477
1478		Parameters:
1479			name: The function name of the pattern.
1480			**kwargs: Parameter names and their new values.
1481
1482		Example (from the live REPL)::
1483
1484			composition.tweak("bass", pitches=[48, 52, 55, 60])
1485		"""
1486
1487		if name not in self._running_patterns:
1488			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1489
1490		self._running_patterns[name]._tweaks.update(kwargs)
1491		logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}")
1492
1493	def clear_tweak (self, name: str, *param_names: str) -> None:
1494
1495		"""Remove tweaked parameters from a running pattern.
1496
1497		If no parameter names are given, all tweaks for the pattern
1498		are cleared and every ``p.param()`` call reverts to its
1499		default.
1500
1501		Parameters:
1502			name: The function name of the pattern.
1503			*param_names: Specific parameter names to clear.  If
1504				omitted, all tweaks are removed.
1505		"""
1506
1507		if name not in self._running_patterns:
1508			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1509
1510		if not param_names:
1511			self._running_patterns[name]._tweaks.clear()
1512			logger.info(f"Cleared all tweaks for pattern '{name}'")
1513		else:
1514			for param_name in param_names:
1515				self._running_patterns[name]._tweaks.pop(param_name, None)
1516			logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}")
1517
1518	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
1519
1520		"""Return a copy of the current tweaks for a running pattern.
1521
1522		Parameters:
1523			name: The function name of the pattern.
1524		"""
1525
1526		if name not in self._running_patterns:
1527			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1528
1529		return dict(self._running_patterns[name]._tweaks)
1530
1531	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
1532
1533		"""
1534		Register a custom function to run on a repeating beat-based cycle.
1535
1536		Subsequence automatically runs synchronous functions in a thread pool
1537		so they don't block the timing-critical MIDI clock. Async functions
1538		are run directly on the event loop.
1539
1540		Parameters:
1541			fn: The function to call.
1542			cycle_beats: How often to call it (e.g., 4 = every bar).
1543			reschedule_lookahead: How far in advance to schedule the next call.
1544			wait_for_initial: If True, run the function once during startup
1545				and wait for it to complete before playback begins. This
1546				ensures ``composition.data`` is populated before patterns
1547				first build. Implies ``defer=True`` for the repeating
1548				schedule.
1549			defer: If True, skip the pulse-0 fire and defer the first
1550				repeating call to just before the second cycle boundary.
1551		"""
1552
1553		self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer))
1554
1555	def form (
1556		self,
1557		sections: typing.Union[
1558			typing.List[typing.Tuple[str, int]],
1559			typing.Iterator[typing.Tuple[str, int]],
1560			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
1561		],
1562		loop: bool = False,
1563		start: typing.Optional[str] = None
1564	) -> None:
1565
1566		"""
1567		Define the structure (sections) of the composition.
1568
1569		You can define form in three ways:
1570		1. **Graph (Dict)**: Dynamic transitions based on weights.
1571		2. **Sequence (List)**: A fixed order of sections.
1572		3. **Generator**: A Python generator that yields `(name, bars)` pairs.
1573
1574		Parameters:
1575			sections: The form definition (Dict, List, or Generator).
1576			loop: Whether to cycle back to the start (List mode only).
1577			start: The section to start with (Graph mode only).
1578
1579		Example:
1580			```python
1581			# A simple pop structure
1582			comp.form([
1583				("verse", 8),
1584				("chorus", 8),
1585				("verse", 8),
1586				("chorus", 16)
1587			])
1588			```
1589		"""
1590
1591		self._form_state = subsequence.form_state.FormState(sections, loop=loop, start=start)
1592
1593	def pattern (
1594		self,
1595		channel: int,
1596		length: float = 4,
1597		unit: typing.Optional[float] = None,
1598		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
1599		reschedule_lookahead: float = 1,
1600		voice_leading: bool = False
1601	) -> typing.Callable:
1602
1603		"""
1604		Register a function as a repeating MIDI pattern.
1605
1606		The decorated function will be called once per cycle to 'rebuild' its
1607		content. This allows for generative logic that evolves over time.
1608
1609		When ``unit`` is provided, ``length`` is a note count and the actual
1610		duration in beats is ``length * unit``.  The note count also becomes
1611		the default grid size for ``hit_steps()`` and ``sequence()``.
1612
1613		When ``unit`` is omitted, ``length`` is in beats (quarter notes) and
1614		the grid defaults to sixteenth-note resolution.
1615
1616		Parameters:
1617			channel: MIDI channel. By default uses 0-based numbering (0-15)
1618				matching the raw MIDI protocol. Set
1619				``zero_indexed_channels=False`` on the ``Composition`` to use
1620				1-based numbering (1-16) instead.
1621			length: Note count when ``unit`` is given, otherwise duration
1622				in beats (default 4).
1623			unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``).
1624				When set, ``length`` is treated as a count and the grid
1625				defaults to ``length``.
1626			drum_note_map: Optional mapping for drum instruments.
1627			reschedule_lookahead: Beats in advance to compute the next cycle.
1628			voice_leading: If True, chords in this pattern will automatically
1629				use inversions that minimize voice movement.
1630
1631		Example:
1632			```python
1633			@comp.pattern(channel=1, length=6, unit=dur.SIXTEENTH)
1634			def riff(p):
1635				p.sequence(steps=[0, 1, 3, 5], pitches=60)
1636			```
1637		"""
1638
1639		channel = self._resolve_channel(channel)
1640
1641		if unit is not None:
1642			beat_length = length * unit
1643			default_grid = int(length)
1644		else:
1645			beat_length = length
1646			default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH)
1647
1648		def decorator (fn: typing.Callable) -> typing.Callable:
1649
1650			"""
1651			Wrap the builder function and register it as a pending pattern.
1652			During live sessions, hot-swap an existing pattern's builder instead.
1653			"""
1654
1655			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
1656			if self._is_live and fn.__name__ in self._running_patterns:
1657				running = self._running_patterns[fn.__name__]
1658				running._builder_fn = fn
1659				running._wants_chord = _fn_has_parameter(fn, "chord")
1660				logger.info(f"Hot-swapped pattern: {fn.__name__}")
1661				return fn
1662
1663			pending = _PendingPattern(
1664				builder_fn = fn,
1665				channel = channel,  # already resolved to 0-indexed
1666				length = beat_length,
1667				default_grid = default_grid,
1668				drum_note_map = drum_note_map,
1669				reschedule_lookahead = reschedule_lookahead,
1670				voice_leading = voice_leading
1671			)
1672
1673			self._pending_patterns.append(pending)
1674
1675			return fn
1676
1677		return decorator
1678
1679	def layer (
1680		self,
1681		*builder_fns: typing.Callable,
1682		channel: int,
1683		length: float = 4,
1684		unit: typing.Optional[float] = None,
1685		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
1686		reschedule_lookahead: float = 1,
1687		voice_leading: bool = False
1688	) -> None:
1689
1690		"""
1691		Combine multiple functions into a single MIDI pattern.
1692
1693		This is useful for composing complex patterns out of reusable
1694		building blocks (e.g., a 'kick' function and a 'snare' function).
1695
1696		Parameters:
1697			builder_fns: One or more pattern builder functions.
1698			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
1699			length: Note count when ``unit`` is given, otherwise duration
1700				in beats (default 4).
1701			unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``).
1702				When set, ``length`` is treated as a count and the grid
1703				defaults to ``length``.
1704			drum_note_map: Optional mapping for drum instruments.
1705			reschedule_lookahead: Beats in advance to compute the next cycle.
1706			voice_leading: If True, chords use smooth voice leading.
1707		"""
1708
1709		if unit is not None:
1710			beat_length = length * unit
1711			default_grid = int(length)
1712		else:
1713			beat_length = length
1714			default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH)
1715
1716		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
1717
1718		if wants_chord:
1719
1720			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
1721
1722				for fn in builder_fns:
1723					if _fn_has_parameter(fn, "chord"):
1724						fn(p, chord)
1725					else:
1726						fn(p)
1727
1728		else:
1729
1730			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
1731
1732				for fn in builder_fns:
1733					fn(p)
1734
1735		resolved = self._resolve_channel(channel)
1736
1737		pending = _PendingPattern(
1738			builder_fn = merged_builder,
1739			channel = resolved,  # already resolved to 0-indexed
1740			length = beat_length,
1741			default_grid = default_grid,
1742			drum_note_map = drum_note_map,
1743			reschedule_lookahead = reschedule_lookahead,
1744			voice_leading = voice_leading
1745		)
1746
1747		self._pending_patterns.append(pending)
1748
1749	def trigger (
1750		self,
1751		fn: typing.Callable,
1752		channel: int,
1753		length: float = 1,
1754		quantize: float = 0,
1755		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
1756		chord: bool = False
1757	) -> None:
1758
1759		"""
1760		Trigger a one-shot pattern immediately or on a quantized boundary.
1761
1762		This is useful for real-time response to sensors, OSC messages, or other
1763		external events. The builder function is called immediately with a fresh
1764		PatternBuilder, and the generated events are injected into the queue at
1765		the specified quantize boundary.
1766
1767		The builder function has the same API as a ``@composition.pattern``
1768		decorated function and can use all PatternBuilder methods: ``p.note()``,
1769		``p.euclidean()``, ``p.arpeggio()``, and so on.
1770
1771		Parameters:
1772			fn: The pattern builder function (same signature as ``@comp.pattern``).
1773			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
1774			length: Duration in beats (default 1). This is the time window for the
1775				one-shot pattern.
1776			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
1777				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
1778				constants from ``subsequence.constants.durations``.
1779			drum_note_map: Optional drum name mapping for this pattern.
1780			chord: If ``True``, the builder function receives the current chord as
1781				a second parameter (same as ``@composition.pattern``).
1782
1783		Example:
1784			```python
1785			# Immediate single note
1786			composition.trigger(
1787				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
1788				channel=0
1789			)
1790
1791			# Quantized fill (next bar)
1792			import subsequence.constants.durations as dur
1793			composition.trigger(
1794				lambda p: p.euclidean("snare", pulses=7, velocity=90),
1795				channel=9,
1796				drum_note_map=gm_drums.GM_DRUM_MAP,
1797				quantize=dur.WHOLE
1798			)
1799
1800			# With chord context
1801			composition.trigger(
1802				lambda p: p.arpeggio(p.chord.tones(root=60), step=dur.SIXTEENTH),
1803				channel=0,
1804				quantize=dur.QUARTER,
1805				chord=True
1806			)
1807			```
1808		"""
1809
1810		# Resolve channel numbering
1811		resolved_channel = self._resolve_channel(channel)
1812
1813		# Create a temporary Pattern
1814		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=length)
1815
1816		# Create a PatternBuilder
1817		builder = subsequence.pattern_builder.PatternBuilder(
1818			pattern=pattern,
1819			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
1820			drum_note_map=drum_note_map,
1821			section=self._form_state.get_section_info() if self._form_state else None,
1822			bar=self._builder_bar,
1823			conductor=self.conductor,
1824			rng=random.Random(),  # Fresh random state for each trigger
1825			tweaks={},
1826			default_grid=round(length / subsequence.constants.durations.SIXTEENTH),
1827			data=self.data
1828		)
1829
1830		# Call the builder function
1831		try:
1832
1833			if chord and self._harmonic_state is not None:
1834				current_chord = self._harmonic_state.get_current_chord()
1835				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
1836				fn(builder, injected)
1837
1838			else:
1839				fn(builder)
1840
1841		except Exception:
1842			logger.exception("Error in trigger builder — pattern will be silent")
1843			return
1844
1845		# Calculate the start pulse based on quantize
1846		current_pulse = self._sequencer.pulse_count
1847		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
1848
1849		if quantize == 0:
1850			# Immediate: use current pulse
1851			start_pulse = current_pulse
1852
1853		else:
1854			# Quantize to the next multiple of (quantize * pulses_per_beat)
1855			quantize_pulses = int(quantize * pulses_per_beat)
1856			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
1857
1858		# Schedule the pattern for one-shot execution
1859		try:
1860			loop = asyncio.get_running_loop()
1861			# Already on the event loop
1862			asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse))
1863
1864		except RuntimeError:
1865			# Not on the event loop — schedule via call_soon_threadsafe
1866			if self._sequencer._event_loop is not None:
1867				asyncio.run_coroutine_threadsafe(
1868					self._sequencer.schedule_pattern(pattern, start_pulse),
1869					loop=self._sequencer._event_loop
1870				)
1871			else:
1872				logger.warning("trigger() called before playback started; pattern ignored")
1873
1874	def play (self) -> None:
1875
1876		"""
1877		Start the composition.
1878
1879		This call blocks until the program is interrupted (e.g., via Ctrl+C).
1880		It initializes the MIDI hardware, launches the background sequencer,
1881		and begins playback.
1882		"""
1883
1884		try:
1885			asyncio.run(self._run())
1886
1887		except KeyboardInterrupt:
1888			pass
1889
1890
1891	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
1892
1893		"""Render the composition to a MIDI file without real-time playback.
1894
1895		Runs the sequencer as fast as possible (no timing delays) and stops
1896		when the first active limit is reached.  The result is saved as a
1897		standard MIDI file that can be imported into any DAW.
1898
1899		All patterns, scheduled callbacks, and harmony logic run exactly as
1900		they would during live playback — BPM transitions, generative fills,
1901		and probabilistic gates all work in render mode.  The only difference
1902		is that time is simulated rather than wall-clock driven.
1903
1904		Parameters:
1905			bars: Number of bars to render, or ``None`` for no bar limit
1906			      (default ``None``).  When both *bars* and *max_minutes* are
1907			      active, playback stops at whichever limit is reached first.
1908			filename: Output MIDI filename (default ``"render.mid"``).
1909			max_minutes: Safety cap on the length of rendered MIDI in minutes
1910			             (default ``60.0``).  Pass ``None`` to disable the time
1911			             cap — you must then provide an explicit *bars* value.
1912
1913		Raises:
1914			ValueError: If both *bars* and *max_minutes* are ``None``, which
1915			            would produce an infinite render.
1916
1917		Examples:
1918			```python
1919			# Default: renders up to 60 minutes of MIDI content.
1920			composition.render()
1921
1922			# Render exactly 64 bars (time cap still active as backstop).
1923			composition.render(bars=64, filename="demo.mid")
1924
1925			# Render up to 5 minutes of an infinite generative composition.
1926			composition.render(max_minutes=5, filename="five_min.mid")
1927
1928			# Remove the time cap — must supply bars instead.
1929			composition.render(bars=128, max_minutes=None, filename="long.mid")
1930			```
1931		"""
1932
1933		if bars is None and max_minutes is None:
1934			raise ValueError(
1935				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
1936				"Passing both as None would produce an infinite render."
1937			)
1938
1939		self._sequencer.recording = True
1940		self._sequencer.record_filename = filename
1941		self._sequencer.render_mode = True
1942		self._sequencer.render_bars = bars if bars is not None else 0
1943		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
1944		asyncio.run(self._run())
1945
1946	async def _run (self) -> None:
1947
1948		"""
1949		Async entry point that schedules all patterns and runs the sequencer.
1950		"""
1951
1952		# Pass MIDI input configuration to the sequencer before start.
1953		if self._input_device is not None:
1954			self._sequencer.input_device_name = self._input_device
1955			self._sequencer.clock_follow = self._clock_follow
1956
1957		# Pass clock output flag (suppressed automatically when clock_follow=True).
1958		self._sequencer.clock_output = self._clock_output and not self._clock_follow
1959
1960		# Create Ableton Link clock if comp.link() was called.
1961		if self._link_quantum is not None:
1962			self._sequencer._link_clock = subsequence.link_clock.LinkClock(
1963				bpm = self.bpm,
1964				quantum = self._link_quantum,
1965				loop = asyncio.get_running_loop(),
1966			)
1967
1968		# Share CC input mappings and a reference to composition.data with the sequencer.
1969		self._sequencer.cc_mappings = self._cc_mappings
1970		self._sequencer._composition_data = self.data
1971
1972		# Derive child RNGs from the master seed so each component gets
1973		# an independent, deterministic stream.  When no seed is set,
1974		# each component creates its own unseeded RNG (existing behaviour).
1975		self._pattern_rngs: typing.List[random.Random] = []
1976
1977		if self._seed is not None:
1978			master = random.Random(self._seed)
1979
1980			if self._harmonic_state is not None:
1981				self._harmonic_state.rng = random.Random(master.randint(0, 2 ** 63))
1982
1983			if self._form_state is not None:
1984				self._form_state._rng = random.Random(master.randint(0, 2 ** 63))
1985
1986			for _ in self._pending_patterns:
1987				self._pattern_rngs.append(random.Random(master.randint(0, 2 ** 63)))
1988
1989		if self._harmonic_state is not None and self._harmony_cycle_beats is not None:
1990
1991			def _get_section_progression () -> typing.Optional[typing.Tuple[str, int, typing.Optional[Progression]]]:
1992				"""Return (section_name, section_index, Progression|None) for the current section, or None."""
1993				if self._form_state is None:
1994					return None
1995				info = self._form_state.get_section_info()
1996				if info is None:
1997					return None
1998				prog = self._section_progressions.get(info.name)
1999				return (info.name, info.index, prog)
2000
2001			await schedule_harmonic_clock(
2002				sequencer = self._sequencer,
2003				get_harmonic_state = lambda: self._harmonic_state,
2004				cycle_beats = self._harmony_cycle_beats,
2005				reschedule_lookahead = self._harmony_reschedule_lookahead,
2006				get_section_progression = _get_section_progression,
2007			)
2008
2009		if self._form_state is not None:
2010
2011			await schedule_form(
2012				sequencer = self._sequencer,
2013				form_state = self._form_state,
2014				reschedule_lookahead = 1
2015			)
2016
2017		# Bar counter - always active so p.bar is available to all builders.
2018		def _advance_builder_bar (pulse: int) -> None:
2019			self._builder_bar += 1
2020
2021		first_bar_pulse = int(self.time_signature[0] * self._sequencer.pulses_per_beat)
2022
2023		await self._sequencer.schedule_callback_repeating(
2024			callback = _advance_builder_bar,
2025			interval_beats = self.time_signature[0],
2026			start_pulse = first_bar_pulse,
2027			reschedule_lookahead = 1
2028		)
2029
2030		# Run wait_for_initial=True scheduled functions and block until all complete.
2031		# This ensures composition.data is populated before patterns build.
2032		initial_tasks = [t for t in self._pending_scheduled if t.wait_for_initial]
2033
2034		if initial_tasks:
2035
2036			names = ", ".join(t.fn.__name__ for t in initial_tasks)
2037			logger.info(f"Waiting for initial scheduled {'function' if len(initial_tasks) == 1 else 'functions'} before start: {names}")
2038
2039			async def _run_initial (fn: typing.Callable) -> None:
2040
2041				accepts_ctx = _fn_has_parameter(fn, "p")
2042				ctx = ScheduleContext(cycle=0)
2043
2044				try:
2045					if asyncio.iscoroutinefunction(fn):
2046						await (fn(ctx) if accepts_ctx else fn())
2047					else:
2048						loop = asyncio.get_running_loop()
2049						call = (lambda: fn(ctx)) if accepts_ctx else fn
2050						await loop.run_in_executor(None, call)
2051				except Exception as exc:
2052					logger.warning(f"Initial run of {fn.__name__!r} failed: {exc}")
2053
2054			await asyncio.gather(*[_run_initial(t.fn) for t in initial_tasks])
2055
2056		for pending_task in self._pending_scheduled:
2057
2058			accepts_ctx = _fn_has_parameter(pending_task.fn, "p")
2059			wrapped = _make_safe_callback(pending_task.fn, accepts_context=accepts_ctx)
2060
2061			# wait_for_initial=True implies defer — no point firing at pulse 0
2062			# after the blocking run just completed.  defer=True skips the
2063			# backshift fire so the first repeating call happens one full cycle
2064			# later.
2065			if pending_task.wait_for_initial or pending_task.defer:
2066				start_pulse = int(pending_task.cycle_beats * self._sequencer.pulses_per_beat)
2067			else:
2068				start_pulse = 0
2069
2070			await self._sequencer.schedule_callback_repeating(
2071				callback = wrapped,
2072				interval_beats = pending_task.cycle_beats,
2073				start_pulse = start_pulse,
2074				reschedule_lookahead = pending_task.reschedule_lookahead
2075			)
2076
2077		# Build Pattern objects from pending registrations.
2078		patterns: typing.List[subsequence.pattern.Pattern] = []
2079
2080		for i, pending in enumerate(self._pending_patterns):
2081
2082			pattern_rng = self._pattern_rngs[i] if i < len(self._pattern_rngs) else None
2083			pattern = self._build_pattern_from_pending(pending, pattern_rng)
2084			patterns.append(pattern)
2085
2086		await schedule_patterns(
2087			sequencer = self._sequencer,
2088			patterns = patterns,
2089			start_pulse = 0
2090		)
2091
2092		# Populate the running patterns dict for live hot-swap and mute/unmute.
2093		for i, pending in enumerate(self._pending_patterns):
2094			name = pending.builder_fn.__name__
2095			self._running_patterns[name] = patterns[i]
2096
2097		if self._display is not None and not self._sequencer.render_mode:
2098			self._display.start()
2099			self._sequencer.on_event("bar",  self._display.update)
2100			self._sequencer.on_event("beat", self._display.update)
2101
2102		if self._live_server is not None:
2103			await self._live_server.start()
2104
2105		if self._osc_server is not None:
2106			await self._osc_server.start()
2107			self._sequencer.osc_server = self._osc_server
2108
2109			def _send_osc_status (bar: int) -> None:
2110				if self._osc_server:
2111					self._osc_server.send("/bar", bar)
2112					self._osc_server.send("/bpm", self._sequencer.current_bpm)
2113					
2114					if self._harmonic_state:
2115						self._osc_server.send("/chord", self._harmonic_state.current_chord.name())
2116					
2117					if self._form_state:
2118						info = self._form_state.get_section_info()
2119						if info:
2120							self._osc_server.send("/section", info.name)
2121
2122			self._sequencer.on_event("bar", _send_osc_status)
2123
2124		# Start keystroke listener if hotkeys are enabled and not in render mode.
2125		if self._hotkeys_enabled and not self._sequencer.render_mode:
2126			self._keystroke_listener = subsequence.keystroke.KeystrokeListener()
2127			self._keystroke_listener.start()
2128
2129			if self._keystroke_listener.active:
2130				# Listener started successfully — register the bar handler
2131				# and show all bindings so the user knows what's available.
2132				self._sequencer.on_event("bar", self._process_hotkeys)
2133				self._list_hotkeys()
2134			# If not active, KeystrokeListener.start() already logged a warning.
2135
2136		if self._web_ui_enabled and not self._sequencer.render_mode:
2137			self._web_ui_server = subsequence.web_ui.WebUI(self)
2138			self._web_ui_server.start()
2139
2140		await run_until_stopped(self._sequencer)
2141
2142		if self._web_ui_server is not None:
2143			self._web_ui_server.stop()
2144
2145		if self._live_server is not None:
2146			await self._live_server.stop()
2147
2148		if self._osc_server is not None:
2149			await self._osc_server.stop()
2150			self._sequencer.osc_server = None
2151
2152		if self._display is not None:
2153			self._display.stop()
2154
2155		if self._keystroke_listener is not None:
2156			self._keystroke_listener.stop()
2157			self._keystroke_listener = None
2158
2159	def _build_pattern_from_pending (self, pending: _PendingPattern, rng: typing.Optional[random.Random] = None) -> subsequence.pattern.Pattern:
2160
2161		"""
2162		Create a Pattern from a pending registration using a temporary subclass.
2163		"""
2164
2165		composition_ref = self
2166
2167		class _DecoratorPattern (subsequence.pattern.Pattern):
2168
2169			"""
2170			Pattern subclass that delegates to a builder function on each reschedule.
2171			"""
2172
2173			def __init__ (self, pending: _PendingPattern, pattern_rng: typing.Optional[random.Random] = None) -> None:
2174
2175				"""
2176				Initialize the decorator pattern from pending registration details.
2177				"""
2178
2179				super().__init__(
2180					channel = pending.channel,
2181					length = pending.length,
2182					reschedule_lookahead = min(
2183						pending.reschedule_lookahead,
2184						composition_ref._harmony_reschedule_lookahead
2185					)
2186				)
2187
2188				self._builder_fn = pending.builder_fn
2189				self._drum_note_map = pending.drum_note_map
2190				self._default_grid: int = pending.default_grid
2191				self._wants_chord = _fn_has_parameter(pending.builder_fn, "chord")
2192				self._cycle_count = 0
2193				self._rng = pattern_rng
2194				self._muted = False
2195				self._voice_leading_state: typing.Optional[subsequence.voicings.VoiceLeadingState] = (
2196					subsequence.voicings.VoiceLeadingState() if pending.voice_leading else None
2197				)
2198				self._tweaks: typing.Dict[str, typing.Any] = {}
2199
2200				self._rebuild()
2201
2202			def _rebuild (self) -> None:
2203
2204				"""
2205				Clear steps and call the builder function to repopulate.
2206				"""
2207
2208				self.steps = {}
2209				self.cc_events = []
2210				self.osc_events = []
2211				current_cycle = self._cycle_count
2212				self._cycle_count += 1
2213
2214				if self._muted:
2215					return
2216
2217				builder = subsequence.pattern_builder.PatternBuilder(
2218					pattern = self,
2219					cycle = current_cycle,
2220					drum_note_map = self._drum_note_map,
2221					section = composition_ref._form_state.get_section_info() if composition_ref._form_state else None,
2222					bar = composition_ref._builder_bar,
2223					conductor = composition_ref.conductor,
2224					rng = self._rng,
2225					tweaks = self._tweaks,
2226					default_grid = self._default_grid,
2227					data = composition_ref.data
2228				)
2229
2230				try:
2231
2232					if self._wants_chord and composition_ref._harmonic_state is not None:
2233						chord = composition_ref._harmonic_state.get_current_chord()
2234						injected = _InjectedChord(chord, self._voice_leading_state)
2235						self._builder_fn(builder, injected)
2236
2237					else:
2238						self._builder_fn(builder)
2239
2240				except Exception:
2241					logger.exception("Error in pattern builder '%s' (cycle %d) - pattern will be silent this cycle", self._builder_fn.__name__, current_cycle)
2242
2243			def on_reschedule (self) -> None:
2244
2245				"""
2246				Rebuild the pattern from the builder function before the next cycle.
2247				"""
2248
2249				self._rebuild()
2250
2251		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 = True)
567	def __init__ (
568		self,
569		output_device: typing.Optional[str] = None,
570		bpm: float = 120,
571		time_signature: typing.Tuple[int, int] = (4, 4),
572		key: typing.Optional[str] = None,
573		seed: typing.Optional[int] = None,
574		record: bool = False,
575		record_filename: typing.Optional[str] = None,
576		zero_indexed_channels: bool = True
577	) -> None:
578
579		"""
580		Initialize a new composition.
581
582		Parameters:
583			output_device: The name of the MIDI output port to use. If `None`, 
584				Subsequence will attempt to find a device, prompting if necessary.
585			bpm: Initial tempo in beats per minute (default 120).
586			key: The root key of the piece (e.g., "C", "F#", "Bb").
587				Required if you plan to use `harmony()`.
588			seed: An optional integer for deterministic randomness. When set, 
589				every random decision (chord choices, drum probability, etc.) 
590				will be identical on every run.
591			record: When True, record all MIDI events to a file.
592			record_filename: Optional filename for the recording (defaults to timestamp).
593			zero_indexed_channels: When False, MIDI channels use
594				1-based numbering (1-16) matching instrument labelling.
595				Channel 10 is drums, the way musicians and hardware panels
596				show it. When True (default), channels use 0-based numbering (0-15)
597				matching the raw MIDI protocol.
598
599		Example:
600			```python
601			comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
602			```
603		"""
604
605		self.output_device = output_device
606		self.bpm = bpm
607		self.time_signature = time_signature
608		self.key = key
609		self._seed: typing.Optional[int] = seed
610		self._zero_indexed_channels: bool = zero_indexed_channels
611
612		self._sequencer = subsequence.sequencer.Sequencer(
613			output_device_name = output_device,
614			initial_bpm = bpm,
615			time_signature = time_signature,
616			record = record,
617			record_filename = record_filename
618		)
619
620		self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None
621		self._harmony_cycle_beats: typing.Optional[int] = None
622		self._harmony_reschedule_lookahead: float = 1
623		self._section_progressions: typing.Dict[str, Progression] = {}
624		self._pending_patterns: typing.List[_PendingPattern] = []
625		self._pending_scheduled: typing.List[_PendingScheduled] = []
626		self._form_state: typing.Optional[subsequence.form_state.FormState] = None
627		self._builder_bar: int = 0
628		self._display: typing.Optional[subsequence.display.Display] = None
629		self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None
630		self._is_live: bool = False
631		self._running_patterns: typing.Dict[str, typing.Any] = {}
632		self._input_device: typing.Optional[str] = None
633		self._clock_follow: bool = False
634		self._clock_output: bool = False
635		self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = []
636		self.data: typing.Dict[str, typing.Any] = {}
637		self._osc_server: typing.Optional[subsequence.osc.OscServer] = None
638		self.conductor = subsequence.conductor.Conductor()
639		self._web_ui_enabled: bool = False
640		self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None
641		self._link_quantum: typing.Optional[float] = None
642
643		# Hotkey state — populated by hotkeys() and hotkey().
644		self._hotkeys_enabled: bool = False
645		self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {}
646		self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = []
647		self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None

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, 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 (default), 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]
668	@property
669	def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]:
670		"""The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called."""
671		return self._harmonic_state

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

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

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

sequencer: subsequence.sequencer.Sequencer
678	@property
679	def sequencer (self) -> subsequence.sequencer.Sequencer:
680		"""The underlying ``Sequencer`` instance."""
681		return self._sequencer

The underlying Sequencer instance.

running_patterns: Dict[str, Any]
683	@property
684	def running_patterns (self) -> typing.Dict[str, typing.Any]:
685		"""The currently active patterns, keyed by name."""
686		return self._running_patterns

The currently active patterns, keyed by name.

builder_bar: int
688	@property
689	def builder_bar (self) -> int:
690		"""Current bar index used by pattern builders."""
691		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:
702	def harmony (
703		self,
704		style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major",
705		cycle_beats: int = 4,
706		dominant_7th: bool = True,
707		gravity: float = 1.0,
708		nir_strength: float = 0.5,
709		minor_turnaround_weight: float = 0.0,
710		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
711		reschedule_lookahead: float = 1
712	) -> None:
713
714		"""
715		Configure the harmonic logic and chord change intervals.
716
717		Subsequence uses a weighted transition graph to choose the next chord.
718		You can influence these choices using 'gravity' (favoring the tonic) and
719		'NIR strength' (melodic inertia based on Narmour's model).
720
721		Parameters:
722			style: The harmonic style to use. Built-in: "functional_major"
723				(alias "diatonic_major"), "turnaround", "aeolian_minor",
724				"phrygian_minor", "lydian_major", "dorian_minor",
725				"chromatic_mediant", "suspended", "mixolydian", "whole_tone",
726				"diminished". See README for full descriptions.
727			cycle_beats: How many beats each chord lasts (default 4).
728			dominant_7th: Whether to include V7 chords (default True).
729			gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
730			nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement
731				expectations.
732			minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
733			root_diversity: Root-repetition damping (0.0 to 1.0). Each recent
734				chord sharing a candidate's root reduces the weight to 40% at
735				the default (0.4). Set to 1.0 to disable.
736			reschedule_lookahead: How many beats in advance to calculate the
737				next chord.
738
739		Example:
740			```python
741			# A moody minor progression that changes every 8 beats
742			comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
743			```
744		"""
745
746		if self.key is None:
747			raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor")
748
749		preserved_history: typing.List[subsequence.chords.Chord] = []
750		preserved_current: typing.Optional[subsequence.chords.Chord] = None
751
752		if self._harmonic_state is not None:
753			preserved_history = self._harmonic_state.history.copy()
754			preserved_current = self._harmonic_state.current_chord
755
756		self._harmonic_state = subsequence.harmonic_state.HarmonicState(
757			key_name = self.key,
758			graph_style = style,
759			include_dominant_7th = dominant_7th,
760			key_gravity_blend = gravity,
761			nir_strength = nir_strength,
762			minor_turnaround_weight = minor_turnaround_weight,
763			root_diversity = root_diversity
764		)
765
766		if preserved_history:
767			self._harmonic_state.history = preserved_history
768		if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current):
769			self._harmonic_state.current_chord = preserved_current
770
771		self._harmony_cycle_beats = cycle_beats
772		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:
774	def freeze (self, bars: int) -> "Progression":
775
776		"""Capture a chord progression from the live harmony engine.
777
778		Runs the harmony engine forward by *bars* chord changes, records each
779		chord, and returns it as a :class:`Progression` that can be bound to a
780		form section with :meth:`section_chords`.
781
782		The engine state **advances** — successive ``freeze()`` calls produce a
783		continuing compositional journey so section progressions feel like parts
784		of a whole rather than isolated islands.
785
786		Parameters:
787			bars: Number of chords to capture (one per harmony cycle).
788
789		Returns:
790			A :class:`Progression` with the captured chords and trailing
791			history for NIR continuity.
792
793		Raises:
794			ValueError: If :meth:`harmony` has not been called first.
795
796		Example::
797
798			composition.harmony(style="functional_major", cycle_beats=4)
799			verse  = composition.freeze(8)   # 8 chords, engine advances
800			chorus = composition.freeze(4)   # next 4 chords, continuing on
801			composition.section_chords("verse",  verse)
802			composition.section_chords("chorus", chorus)
803		"""
804
805		hs = self._require_harmonic_state()
806
807		if bars < 1:
808			raise ValueError("bars must be at least 1")
809		collected: typing.List[subsequence.chords.Chord] = [hs.current_chord]
810
811		for _ in range(bars - 1):
812			hs.step()
813			collected.append(hs.current_chord)
814
815		# Advance past the last captured chord so the next freeze() call or
816		# live playback does not duplicate it.
817		hs.step()
818
819		return Progression(
820			chords = tuple(collected),
821			trailing_history = tuple(hs.history),
822		)

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:
824	def section_chords (self, section_name: str, progression: "Progression") -> None:
825
826		"""Bind a frozen :class:`Progression` to a named form section.
827
828		Every time *section_name* plays, the harmonic clock replays the
829		progression's chords in order instead of calling the live engine.
830		Sections without a bound progression continue generating live chords.
831
832		Parameters:
833			section_name: Name of the section as defined in :meth:`form`.
834			progression: The :class:`Progression` returned by :meth:`freeze`.
835
836		Raises:
837			ValueError: If the form has been configured and *section_name* is
838				not a known section name.
839
840		Example::
841
842			composition.section_chords("verse",  verse_progression)
843			composition.section_chords("chorus", chorus_progression)
844			# "bridge" is not bound — it generates live chords
845		"""
846
847		if (
848			self._form_state is not None
849			and self._form_state._section_bars is not None
850			and section_name not in self._form_state._section_bars
851		):
852			known = ", ".join(sorted(self._form_state._section_bars))
853			raise ValueError(
854				f"Section '{section_name}' not found in form. "
855				f"Known sections: {known}"
856			)
857
858		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:
860	def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None:
861
862		"""
863		Register a callback for a sequencer event (e.g., "bar", "start", "stop").
864		"""
865
866		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:
873	def hotkeys (self, enabled: bool = True) -> None:
874
875		"""Enable or disable the global hotkey listener.
876
877		Must be called **before** :meth:`play` to take effect.  When enabled, a
878		background thread reads single keystrokes from stdin without requiring
879		Enter.  The ``?`` key is always reserved and lists all active bindings.
880
881		Hotkeys have zero impact on playback when disabled — the listener
882		thread is never started.
883
884		Args:
885		    enabled: ``True`` (default) to enable hotkeys; ``False`` to disable.
886
887		Example::
888
889		    composition.hotkeys()
890		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
891		    composition.play()
892		"""
893
894		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:
897	def hotkey (
898		self,
899		key:      str,
900		action:   typing.Callable[[], None],
901		quantize: int = 0,
902		label:    typing.Optional[str] = None,
903	) -> None:
904
905		"""Register a single-key shortcut that fires during playback.
906
907		The listener must be enabled first with :meth:`hotkeys`.
908
909		Most actions — form jumps, ``composition.data`` writes, and
910		:meth:`tweak` calls — should use ``quantize=0`` (the default).  Their
911		musical effect is naturally delayed to the next pattern rebuild cycle,
912		which provides automatic musical quantization without extra configuration.
913
914		Use ``quantize=N`` for actions where you want an explicit bar-boundary
915		guarantee, such as :meth:`mute` / :meth:`unmute`.
916
917		The ``?`` key is reserved and cannot be overridden.
918
919		Args:
920		    key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``).
921		    action: Zero-argument callable to execute.
922		    quantize: ``0`` = execute immediately (default).  ``N`` = execute
923		        on the next global bar number divisible by *N*.
924		    label: Display name for the ``?`` help listing.  Auto-derived from
925		        the function name or lambda body if omitted.
926
927		Raises:
928		    ValueError: If ``key`` is the reserved ``?`` character, or if
929		        ``key`` is not exactly one character.
930
931		Example::
932
933		    composition.hotkeys()
934
935		    # Immediate — musical effect happens at next pattern rebuild
936		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
937		    composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
938
939		    # Explicit 4-bar phrase boundary
940		    composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
941
942		    # Named function — label is derived automatically
943		    def drop_to_breakdown():
944		        composition.form_jump("breakdown")
945		        composition.mute("lead")
946
947		    composition.hotkey("d", drop_to_breakdown)
948
949		    composition.play()
950		"""
951
952		if len(key) != 1:
953			raise ValueError(f"hotkey key must be a single character, got {key!r}")
954
955		if key == _HOTKEY_RESERVED:
956			raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.")
957
958		derived = label if label is not None else _derive_label(action)
959
960		self._hotkey_bindings[key] = HotkeyBinding(
961			key      = key,
962			action   = action,
963			quantize = quantize,
964			label    = derived,
965		)

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:
968	def form_jump (self, section_name: str) -> None:
969
970		"""Jump the form to a named section immediately.
971
972		Delegates to :meth:`subsequence.form_state.FormState.jump_to`.  Only works when the
973		composition uses graph-mode form (a dict passed to :meth:`form`).
974
975		The musical effect is heard at the *next pattern rebuild cycle* — already-
976		queued MIDI notes are unaffected.  This natural delay means ``form_jump``
977		is effective without needing explicit quantization.
978
979		Args:
980		    section_name: The section to jump to.
981
982		Raises:
983		    ValueError: If no form is configured, or the form is not in graph
984		        mode, or *section_name* is unknown.
985
986		Example::
987
988		    composition.hotkey("c", lambda: composition.form_jump("chorus"))
989		"""
990
991		if self._form_state is None:
992			raise ValueError("form_jump() requires a form to be configured via composition.form().")
993
994		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:
 997	def form_next (self, section_name: str) -> None:
 998
 999		"""Queue the next section — takes effect when the current section ends.
1000
1001		Unlike :meth:`form_jump`, this does not interrupt the current section.
1002		The queued section replaces the automatically pre-decided next section
1003		and takes effect at the natural section boundary.  The performer can
1004		change their mind by calling ``form_next`` again before the boundary.
1005
1006		Delegates to :meth:`subsequence.form_state.FormState.queue_next`.  Only works when the
1007		composition uses graph-mode form (a dict passed to :meth:`form`).
1008
1009		Args:
1010		    section_name: The section to queue.
1011
1012		Raises:
1013		    ValueError: If no form is configured, or the form is not in graph
1014		        mode, or *section_name* is unknown.
1015
1016		Example::
1017
1018		    composition.hotkey("c", lambda: composition.form_next("chorus"))
1019		"""
1020
1021		if self._form_state is None:
1022			raise ValueError("form_next() requires a form to be configured via composition.form().")
1023
1024		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:
1107	def seed (self, value: int) -> None:
1108
1109		"""
1110		Set a random seed for deterministic, repeatable playback.
1111
1112		If a seed is set, Subsequence will produce the exact same sequence 
1113		every time you run the script. This is vital for finishing tracks or 
1114		reproducing a specific 'performance'.
1115
1116		Parameters:
1117			value: An integer seed.
1118
1119		Example:
1120			```python
1121			# Fix the randomness
1122			comp.seed(42)
1123			```
1124		"""
1125
1126		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 display( self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
1128	def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
1129
1130		"""
1131		Enable or disable the live terminal dashboard.
1132
1133		When enabled, Subsequence uses a safe logging handler that allows a
1134		persistent status line (BPM, Key, Bar, Section, Chord) to stay at
1135		the bottom of the terminal while logs scroll above it.
1136
1137		Parameters:
1138			enabled: Whether to show the display (default True).
1139			grid: When True, render an ASCII grid visualisation of all
1140				running patterns above the status line. The grid updates
1141				once per bar, showing which steps have notes and at what
1142				velocity.
1143			grid_scale: Horizontal zoom factor for the grid (default
1144				``1.0``).  Higher values add visual columns between
1145				grid steps, revealing micro-timing from swing and groove.
1146				Snapped to the nearest integer internally for uniform
1147				marker spacing.
1148		"""
1149
1150		if enabled:
1151			self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale)
1152		else:
1153			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:
1155	def web_ui (self) -> None:
1156
1157		"""
1158		Enable the realtime Web UI Dashboard.
1159
1160		When enabled, Subsequence instantiates a WebSocket server that broadcasts 
1161		the current state, signals, and active patterns (with high-res timing and note data) 
1162		to any connected browser clients.
1163		"""
1164
1165		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) -> None:
1167	def midi_input (self, device: str, clock_follow: bool = False) -> None:
1168
1169		"""
1170		Configure MIDI input for external sync and MIDI messages.
1171
1172		Parameters:
1173			device: The name of the MIDI input port.
1174			clock_follow: If True, Subsequence will slave its clock to incoming 
1175				MIDI Ticks. It will also follow MIDI Start/Stop/Continue 
1176				commands.
1177
1178		Example:
1179			```python
1180			# Slave Subsequence to an external hardware sequencer
1181			comp.midi_input("Scarlett 2i4", clock_follow=True)
1182			```
1183		"""
1184
1185		self._input_device = device
1186		self._clock_follow = clock_follow

Configure MIDI input for external sync and MIDI messages.

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.
Example:
# Slave Subsequence to an external hardware sequencer
comp.midi_input("Scarlett 2i4", clock_follow=True)
def clock_output(self, enabled: bool = True) -> None:
1188	def clock_output (self, enabled: bool = True) -> None:
1189
1190		"""
1191		Send MIDI timing clock to connected hardware.
1192
1193		When enabled, Subsequence acts as a MIDI clock master and sends
1194		standard clock messages on the output port: a Start message (0xFA)
1195		when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN),
1196		and a Stop message (0xFC) when playback ends.
1197
1198		This allows hardware synthesizers, drum machines, and effect units to
1199		slave their tempo to Subsequence automatically.
1200
1201		**Note:** Clock output is automatically disabled when ``midi_input()``
1202		is called with ``clock_follow=True``, to prevent a clock feedback loop.
1203
1204		Parameters:
1205			enabled: Whether to send MIDI clock (default True).
1206
1207		Example:
1208			```python
1209			comp = subsequence.Composition(bpm=120, output_device="...")
1210			comp.clock_output()   # hardware will follow Subsequence tempo
1211			```
1212		"""
1213
1214		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) -> None:
1262	def cc_map (
1263		self,
1264		cc: int,
1265		key: str,
1266		channel: typing.Optional[int] = None,
1267		min_val: float = 0.0,
1268		max_val: float = 1.0
1269	) -> None:
1270
1271		"""
1272		Map an incoming MIDI CC to a ``composition.data`` key.
1273
1274		When the composition receives a CC message on the configured MIDI
1275		input port, the value is scaled from the CC range (0–127) to
1276		*[min_val, max_val]* and stored in ``composition.data[key]``.
1277
1278		This lets hardware knobs, faders, and expression pedals control live
1279		parameters without writing any callback code.
1280
1281		**Requires** ``midi_input()`` to be called first to open an input port.
1282
1283		Parameters:
1284			cc: MIDI Control Change number (0–127).
1285			key: The ``composition.data`` key to write.
1286			channel: If given, only respond to CC messages on this channel.
1287				Uses the same numbering convention as ``pattern()`` (0-15
1288				by default, or 1-16 with ``zero_indexed_channels=False``).
1289				``None`` matches any channel (default).
1290			min_val: Scaled minimum — written when CC value is 0 (default 0.0).
1291			max_val: Scaled maximum — written when CC value is 127 (default 1.0).
1292
1293		Example:
1294			```python
1295			comp.midi_input("Arturia KeyStep")
1296			comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
1297			comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader
1298			```
1299		"""
1300
1301		resolved_channel = self._resolve_channel(channel) if channel is not None else None
1302
1303		self._cc_mappings.append({
1304			'cc': cc,
1305			'key': key,
1306			'channel': resolved_channel,
1307			'min_val': min_val,
1308			'max_val': max_val,
1309		})

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).
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
def live(self, port: int = 5555) -> None:
1312	def live (self, port: int = 5555) -> None:
1313
1314		"""
1315		Enable the live coding eval server.
1316
1317		This allows you to connect to a running composition using the 
1318		`subsequence.live_client` REPL and hot-swap pattern code or 
1319		modify variables in real-time.
1320
1321		Parameters:
1322			port: The TCP port to listen on (default 5555).
1323		"""
1324
1325		self._live_server = subsequence.live_server.LiveServer(self, port=port)
1326		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:
1328	def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None:
1329
1330		"""
1331		Enable bi-directional Open Sound Control (OSC).
1332
1333		Subsequence will listen for commands (like `/bpm` or `/mute`) and 
1334		broadcast its internal state (like `/chord` or `/bar`) over UDP.
1335
1336		Parameters:
1337			receive_port: Port to listen for incoming OSC messages (default 9000).
1338			send_port: Port to send state updates to (default 9001).
1339			send_host: The IP address to send updates to (default "127.0.0.1").
1340		"""
1341
1342		self._osc_server = subsequence.osc.OscServer(
1343			self,
1344			receive_port = receive_port,
1345			send_port = send_port,
1346			send_host = send_host
1347		)

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 set_bpm(self, bpm: float) -> None:
1349	def set_bpm (self, bpm: float) -> None:
1350
1351		"""
1352		Instantly change the tempo.
1353
1354		Parameters:
1355			bpm: The new tempo in beats per minute.
1356
1357		When Ableton Link is active, this proposes the new tempo to the Link
1358		network instead of applying it locally.  The network-authoritative tempo
1359		is picked up on the next pulse.
1360		"""
1361
1362		self._sequencer.set_bpm(bpm)
1363
1364		if not self._clock_follow and self._link_quantum is None:
1365			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:
1367	def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None:
1368
1369		"""
1370		Smoothly ramp the tempo to a target value over a number of bars.
1371
1372		Parameters:
1373			bpm: Target tempo in beats per minute.
1374			bars: Duration of the transition in bars.
1375			shape: Easing curve name.  Defaults to ``"linear"``.
1376			       ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural-
1377			       sounding tempo changes.  See :mod:`subsequence.easing` for all
1378			       available shapes.
1379
1380		Example:
1381			```python
1382			# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
1383			comp.target_bpm(140, bars=8, shape="ease_in_out")
1384			```
1385		"""
1386
1387		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]:
1389	def live_info (self) -> typing.Dict[str, typing.Any]:
1390
1391		"""
1392		Return a dictionary containing the current state of the composition.
1393		
1394		Includes BPM, key, current bar, active section, current chord, 
1395		running patterns, and custom data.
1396		"""
1397
1398		section_info = None
1399		if self._form_state is not None:
1400			section = self._form_state.get_section_info()
1401			if section is not None:
1402				section_info = {
1403					"name": section.name,
1404					"bar": section.bar,
1405					"bars": section.bars,
1406					"progress": section.progress
1407				}
1408
1409		chord_name = None
1410		if self._harmonic_state is not None:
1411			chord = self._harmonic_state.get_current_chord()
1412			if chord is not None:
1413				chord_name = chord.name()
1414
1415		pattern_list = []
1416		channel_offset = 0 if self._zero_indexed_channels else 1
1417		for name, pat in self._running_patterns.items():
1418			pattern_list.append({
1419				"name": name,
1420				"channel": pat.channel + channel_offset,
1421				"length": pat.length,
1422				"cycle": pat._cycle_count,
1423				"muted": pat._muted,
1424				"tweaks": dict(pat._tweaks)
1425			})
1426
1427		return {
1428			"bpm": self._sequencer.current_bpm,
1429			"key": self.key,
1430			"bar": self._builder_bar,
1431			"section": section_info,
1432			"chord": chord_name,
1433			"patterns": pattern_list,
1434			"input_device": self._input_device,
1435			"clock_follow": self._clock_follow,
1436			"data": self.data
1437		}

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:
1439	def mute (self, name: str) -> None:
1440
1441		"""
1442		Mute a running pattern by name.
1443		
1444		The pattern continues to 'run' and increment its cycle count in 
1445		the background, but it will not produce any MIDI notes until unmuted.
1446
1447		Parameters:
1448			name: The function name of the pattern to mute.
1449		"""
1450
1451		if name not in self._running_patterns:
1452			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1453
1454		self._running_patterns[name]._muted = True
1455		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:
1457	def unmute (self, name: str) -> None:
1458
1459		"""
1460		Unmute a previously muted pattern.
1461		"""
1462
1463		if name not in self._running_patterns:
1464			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1465
1466		self._running_patterns[name]._muted = False
1467		logger.info(f"Unmuted pattern: {name}")

Unmute a previously muted pattern.

def tweak(self, name: str, **kwargs: Any) -> None:
1469	def tweak (self, name: str, **kwargs: typing.Any) -> None:
1470
1471		"""Override parameters for a running pattern.
1472
1473		Values set here are available inside the pattern's builder
1474		function via ``p.param()``.  They persist across rebuilds
1475		until explicitly changed or cleared.  Changes take effect
1476		on the next rebuild cycle.
1477
1478		Parameters:
1479			name: The function name of the pattern.
1480			**kwargs: Parameter names and their new values.
1481
1482		Example (from the live REPL)::
1483
1484			composition.tweak("bass", pitches=[48, 52, 55, 60])
1485		"""
1486
1487		if name not in self._running_patterns:
1488			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1489
1490		self._running_patterns[name]._tweaks.update(kwargs)
1491		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:
1493	def clear_tweak (self, name: str, *param_names: str) -> None:
1494
1495		"""Remove tweaked parameters from a running pattern.
1496
1497		If no parameter names are given, all tweaks for the pattern
1498		are cleared and every ``p.param()`` call reverts to its
1499		default.
1500
1501		Parameters:
1502			name: The function name of the pattern.
1503			*param_names: Specific parameter names to clear.  If
1504				omitted, all tweaks are removed.
1505		"""
1506
1507		if name not in self._running_patterns:
1508			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1509
1510		if not param_names:
1511			self._running_patterns[name]._tweaks.clear()
1512			logger.info(f"Cleared all tweaks for pattern '{name}'")
1513		else:
1514			for param_name in param_names:
1515				self._running_patterns[name]._tweaks.pop(param_name, None)
1516			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]:
1518	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
1519
1520		"""Return a copy of the current tweaks for a running pattern.
1521
1522		Parameters:
1523			name: The function name of the pattern.
1524		"""
1525
1526		if name not in self._running_patterns:
1527			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
1528
1529		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:
1531	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
1532
1533		"""
1534		Register a custom function to run on a repeating beat-based cycle.
1535
1536		Subsequence automatically runs synchronous functions in a thread pool
1537		so they don't block the timing-critical MIDI clock. Async functions
1538		are run directly on the event loop.
1539
1540		Parameters:
1541			fn: The function to call.
1542			cycle_beats: How often to call it (e.g., 4 = every bar).
1543			reschedule_lookahead: How far in advance to schedule the next call.
1544			wait_for_initial: If True, run the function once during startup
1545				and wait for it to complete before playback begins. This
1546				ensures ``composition.data`` is populated before patterns
1547				first build. Implies ``defer=True`` for the repeating
1548				schedule.
1549			defer: If True, skip the pulse-0 fire and defer the first
1550				repeating call to just before the second cycle boundary.
1551		"""
1552
1553		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:
1555	def form (
1556		self,
1557		sections: typing.Union[
1558			typing.List[typing.Tuple[str, int]],
1559			typing.Iterator[typing.Tuple[str, int]],
1560			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
1561		],
1562		loop: bool = False,
1563		start: typing.Optional[str] = None
1564	) -> None:
1565
1566		"""
1567		Define the structure (sections) of the composition.
1568
1569		You can define form in three ways:
1570		1. **Graph (Dict)**: Dynamic transitions based on weights.
1571		2. **Sequence (List)**: A fixed order of sections.
1572		3. **Generator**: A Python generator that yields `(name, bars)` pairs.
1573
1574		Parameters:
1575			sections: The form definition (Dict, List, or Generator).
1576			loop: Whether to cycle back to the start (List mode only).
1577			start: The section to start with (Graph mode only).
1578
1579		Example:
1580			```python
1581			# A simple pop structure
1582			comp.form([
1583				("verse", 8),
1584				("chorus", 8),
1585				("verse", 8),
1586				("chorus", 16)
1587			])
1588			```
1589		"""
1590
1591		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, length: float = 4, unit: Optional[float] = None, drum_note_map: Optional[Dict[str, int]] = None, reschedule_lookahead: float = 1, voice_leading: bool = False) -> Callable:
1593	def pattern (
1594		self,
1595		channel: int,
1596		length: float = 4,
1597		unit: typing.Optional[float] = None,
1598		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
1599		reschedule_lookahead: float = 1,
1600		voice_leading: bool = False
1601	) -> typing.Callable:
1602
1603		"""
1604		Register a function as a repeating MIDI pattern.
1605
1606		The decorated function will be called once per cycle to 'rebuild' its
1607		content. This allows for generative logic that evolves over time.
1608
1609		When ``unit`` is provided, ``length`` is a note count and the actual
1610		duration in beats is ``length * unit``.  The note count also becomes
1611		the default grid size for ``hit_steps()`` and ``sequence()``.
1612
1613		When ``unit`` is omitted, ``length`` is in beats (quarter notes) and
1614		the grid defaults to sixteenth-note resolution.
1615
1616		Parameters:
1617			channel: MIDI channel. By default uses 0-based numbering (0-15)
1618				matching the raw MIDI protocol. Set
1619				``zero_indexed_channels=False`` on the ``Composition`` to use
1620				1-based numbering (1-16) instead.
1621			length: Note count when ``unit`` is given, otherwise duration
1622				in beats (default 4).
1623			unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``).
1624				When set, ``length`` is treated as a count and the grid
1625				defaults to ``length``.
1626			drum_note_map: Optional mapping for drum instruments.
1627			reschedule_lookahead: Beats in advance to compute the next cycle.
1628			voice_leading: If True, chords in this pattern will automatically
1629				use inversions that minimize voice movement.
1630
1631		Example:
1632			```python
1633			@comp.pattern(channel=1, length=6, unit=dur.SIXTEENTH)
1634			def riff(p):
1635				p.sequence(steps=[0, 1, 3, 5], pitches=60)
1636			```
1637		"""
1638
1639		channel = self._resolve_channel(channel)
1640
1641		if unit is not None:
1642			beat_length = length * unit
1643			default_grid = int(length)
1644		else:
1645			beat_length = length
1646			default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH)
1647
1648		def decorator (fn: typing.Callable) -> typing.Callable:
1649
1650			"""
1651			Wrap the builder function and register it as a pending pattern.
1652			During live sessions, hot-swap an existing pattern's builder instead.
1653			"""
1654
1655			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
1656			if self._is_live and fn.__name__ in self._running_patterns:
1657				running = self._running_patterns[fn.__name__]
1658				running._builder_fn = fn
1659				running._wants_chord = _fn_has_parameter(fn, "chord")
1660				logger.info(f"Hot-swapped pattern: {fn.__name__}")
1661				return fn
1662
1663			pending = _PendingPattern(
1664				builder_fn = fn,
1665				channel = channel,  # already resolved to 0-indexed
1666				length = beat_length,
1667				default_grid = default_grid,
1668				drum_note_map = drum_note_map,
1669				reschedule_lookahead = reschedule_lookahead,
1670				voice_leading = voice_leading
1671			)
1672
1673			self._pending_patterns.append(pending)
1674
1675			return fn
1676
1677		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.

When unit is provided, length is a note count and the actual duration in beats is length * unit. The note count also becomes the default grid size for hit_steps() and sequence().

When unit is omitted, length is in beats (quarter notes) and the grid defaults to sixteenth-note resolution.

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.
  • length: Note count when unit is given, otherwise duration in beats (default 4).
  • unit: Duration of one note in beats (e.g. dur.SIXTEENTH). When set, length is treated as a count and the grid defaults to length.
  • 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, length=6, unit=dur.SIXTEENTH)
def riff(p):
        p.sequence(steps=[0, 1, 3, 5], pitches=60)
def layer( self, *builder_fns: Callable, channel: int, length: float = 4, unit: Optional[float] = None, drum_note_map: Optional[Dict[str, int]] = None, reschedule_lookahead: float = 1, voice_leading: bool = False) -> None:
1679	def layer (
1680		self,
1681		*builder_fns: typing.Callable,
1682		channel: int,
1683		length: float = 4,
1684		unit: typing.Optional[float] = None,
1685		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
1686		reschedule_lookahead: float = 1,
1687		voice_leading: bool = False
1688	) -> None:
1689
1690		"""
1691		Combine multiple functions into a single MIDI pattern.
1692
1693		This is useful for composing complex patterns out of reusable
1694		building blocks (e.g., a 'kick' function and a 'snare' function).
1695
1696		Parameters:
1697			builder_fns: One or more pattern builder functions.
1698			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
1699			length: Note count when ``unit`` is given, otherwise duration
1700				in beats (default 4).
1701			unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``).
1702				When set, ``length`` is treated as a count and the grid
1703				defaults to ``length``.
1704			drum_note_map: Optional mapping for drum instruments.
1705			reschedule_lookahead: Beats in advance to compute the next cycle.
1706			voice_leading: If True, chords use smooth voice leading.
1707		"""
1708
1709		if unit is not None:
1710			beat_length = length * unit
1711			default_grid = int(length)
1712		else:
1713			beat_length = length
1714			default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH)
1715
1716		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
1717
1718		if wants_chord:
1719
1720			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
1721
1722				for fn in builder_fns:
1723					if _fn_has_parameter(fn, "chord"):
1724						fn(p, chord)
1725					else:
1726						fn(p)
1727
1728		else:
1729
1730			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
1731
1732				for fn in builder_fns:
1733					fn(p)
1734
1735		resolved = self._resolve_channel(channel)
1736
1737		pending = _PendingPattern(
1738			builder_fn = merged_builder,
1739			channel = resolved,  # already resolved to 0-indexed
1740			length = beat_length,
1741			default_grid = default_grid,
1742			drum_note_map = drum_note_map,
1743			reschedule_lookahead = reschedule_lookahead,
1744			voice_leading = voice_leading
1745		)
1746
1747		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).

Arguments:
  • builder_fns: One or more pattern builder functions.
  • channel: MIDI channel (0-15, or 1-16 with zero_indexed_channels=False).
  • length: Note count when unit is given, otherwise duration in beats (default 4).
  • unit: Duration of one note in beats (e.g. dur.SIXTEENTH). When set, length is treated as a count and the grid defaults to length.
  • 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, length: float = 1, quantize: float = 0, drum_note_map: Optional[Dict[str, int]] = None, chord: bool = False) -> None:
1749	def trigger (
1750		self,
1751		fn: typing.Callable,
1752		channel: int,
1753		length: float = 1,
1754		quantize: float = 0,
1755		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
1756		chord: bool = False
1757	) -> None:
1758
1759		"""
1760		Trigger a one-shot pattern immediately or on a quantized boundary.
1761
1762		This is useful for real-time response to sensors, OSC messages, or other
1763		external events. The builder function is called immediately with a fresh
1764		PatternBuilder, and the generated events are injected into the queue at
1765		the specified quantize boundary.
1766
1767		The builder function has the same API as a ``@composition.pattern``
1768		decorated function and can use all PatternBuilder methods: ``p.note()``,
1769		``p.euclidean()``, ``p.arpeggio()``, and so on.
1770
1771		Parameters:
1772			fn: The pattern builder function (same signature as ``@comp.pattern``).
1773			channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``).
1774			length: Duration in beats (default 1). This is the time window for the
1775				one-shot pattern.
1776			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
1777				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
1778				constants from ``subsequence.constants.durations``.
1779			drum_note_map: Optional drum name mapping for this pattern.
1780			chord: If ``True``, the builder function receives the current chord as
1781				a second parameter (same as ``@composition.pattern``).
1782
1783		Example:
1784			```python
1785			# Immediate single note
1786			composition.trigger(
1787				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
1788				channel=0
1789			)
1790
1791			# Quantized fill (next bar)
1792			import subsequence.constants.durations as dur
1793			composition.trigger(
1794				lambda p: p.euclidean("snare", pulses=7, velocity=90),
1795				channel=9,
1796				drum_note_map=gm_drums.GM_DRUM_MAP,
1797				quantize=dur.WHOLE
1798			)
1799
1800			# With chord context
1801			composition.trigger(
1802				lambda p: p.arpeggio(p.chord.tones(root=60), step=dur.SIXTEENTH),
1803				channel=0,
1804				quantize=dur.QUARTER,
1805				chord=True
1806			)
1807			```
1808		"""
1809
1810		# Resolve channel numbering
1811		resolved_channel = self._resolve_channel(channel)
1812
1813		# Create a temporary Pattern
1814		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=length)
1815
1816		# Create a PatternBuilder
1817		builder = subsequence.pattern_builder.PatternBuilder(
1818			pattern=pattern,
1819			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
1820			drum_note_map=drum_note_map,
1821			section=self._form_state.get_section_info() if self._form_state else None,
1822			bar=self._builder_bar,
1823			conductor=self.conductor,
1824			rng=random.Random(),  # Fresh random state for each trigger
1825			tweaks={},
1826			default_grid=round(length / subsequence.constants.durations.SIXTEENTH),
1827			data=self.data
1828		)
1829
1830		# Call the builder function
1831		try:
1832
1833			if chord and self._harmonic_state is not None:
1834				current_chord = self._harmonic_state.get_current_chord()
1835				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
1836				fn(builder, injected)
1837
1838			else:
1839				fn(builder)
1840
1841		except Exception:
1842			logger.exception("Error in trigger builder — pattern will be silent")
1843			return
1844
1845		# Calculate the start pulse based on quantize
1846		current_pulse = self._sequencer.pulse_count
1847		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
1848
1849		if quantize == 0:
1850			# Immediate: use current pulse
1851			start_pulse = current_pulse
1852
1853		else:
1854			# Quantize to the next multiple of (quantize * pulses_per_beat)
1855			quantize_pulses = int(quantize * pulses_per_beat)
1856			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
1857
1858		# Schedule the pattern for one-shot execution
1859		try:
1860			loop = asyncio.get_running_loop()
1861			# Already on the event loop
1862			asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse))
1863
1864		except RuntimeError:
1865			# Not on the event loop — schedule via call_soon_threadsafe
1866			if self._sequencer._event_loop is not None:
1867				asyncio.run_coroutine_threadsafe(
1868					self._sequencer.schedule_pattern(pattern, start_pulse),
1869					loop=self._sequencer._event_loop
1870				)
1871			else:
1872				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.

Arguments:
  • fn: The pattern builder function (same signature as @comp.pattern).
  • channel: MIDI channel (0-15, or 1-16 with zero_indexed_channels=False).
  • length: Duration in beats (default 1). This is the time window for the one-shot pattern.
  • 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), step=dur.SIXTEENTH),
        channel=0,
        quantize=dur.QUARTER,
        chord=True
)
def play(self) -> None:
1874	def play (self) -> None:
1875
1876		"""
1877		Start the composition.
1878
1879		This call blocks until the program is interrupted (e.g., via Ctrl+C).
1880		It initializes the MIDI hardware, launches the background sequencer,
1881		and begins playback.
1882		"""
1883
1884		try:
1885			asyncio.run(self._run())
1886
1887		except KeyboardInterrupt:
1888			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:
1891	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
1892
1893		"""Render the composition to a MIDI file without real-time playback.
1894
1895		Runs the sequencer as fast as possible (no timing delays) and stops
1896		when the first active limit is reached.  The result is saved as a
1897		standard MIDI file that can be imported into any DAW.
1898
1899		All patterns, scheduled callbacks, and harmony logic run exactly as
1900		they would during live playback — BPM transitions, generative fills,
1901		and probabilistic gates all work in render mode.  The only difference
1902		is that time is simulated rather than wall-clock driven.
1903
1904		Parameters:
1905			bars: Number of bars to render, or ``None`` for no bar limit
1906			      (default ``None``).  When both *bars* and *max_minutes* are
1907			      active, playback stops at whichever limit is reached first.
1908			filename: Output MIDI filename (default ``"render.mid"``).
1909			max_minutes: Safety cap on the length of rendered MIDI in minutes
1910			             (default ``60.0``).  Pass ``None`` to disable the time
1911			             cap — you must then provide an explicit *bars* value.
1912
1913		Raises:
1914			ValueError: If both *bars* and *max_minutes* are ``None``, which
1915			            would produce an infinite render.
1916
1917		Examples:
1918			```python
1919			# Default: renders up to 60 minutes of MIDI content.
1920			composition.render()
1921
1922			# Render exactly 64 bars (time cap still active as backstop).
1923			composition.render(bars=64, filename="demo.mid")
1924
1925			# Render up to 5 minutes of an infinite generative composition.
1926			composition.render(max_minutes=5, filename="five_min.mid")
1927
1928			# Remove the time cap — must supply bars instead.
1929			composition.render(bars=128, max_minutes=None, filename="long.mid")
1930			```
1931		"""
1932
1933		if bars is None and max_minutes is None:
1934			raise ValueError(
1935				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
1936				"Passing both as None would produce an infinite render."
1937			)
1938
1939		self._sequencer.recording = True
1940		self._sequencer.record_filename = filename
1941		self._sequencer.render_mode = True
1942		self._sequencer.render_bars = bars if bars is not None else 0
1943		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
1944		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.

def register_scale( name: str, intervals: List[int], qualities: Optional[List[str]] = None) -> None:
323def register_scale (
324	name: str,
325	intervals: typing.List[int],
326	qualities: typing.Optional[typing.List[str]] = None
327) -> None:
328
329	"""
330	Register a custom scale for use with ``p.quantize()`` and
331	``scale_pitch_classes()``.
332
333	Parameters:
334		name: Scale name (used in ``p.quantize(key, name)``).
335		intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]``
336			for Hirajōshi). Must start with 0 and contain values 0–11.
337		qualities: Optional chord quality per scale degree (e.g.
338			``["minor", "major", "minor", "major", "diminished"]``).
339			Required only if you want to use the scale with
340			``diatonic_chords()`` or ``diatonic_chord_sequence()``.
341
342	Example::
343
344		import subsequence
345
346		subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11])
347
348		@comp.pattern(channel=0, length=4)
349		def melody (p):
350			p.note(60, beat=0)
351			p.quantize("C", "raga_bhairav")
352	"""
353
354	if not intervals or intervals[0] != 0:
355		raise ValueError("intervals must start with 0")
356	if any(i < 0 or i > 11 for i in intervals):
357		raise ValueError("intervals must contain values between 0 and 11")
358	if qualities is not None and len(qualities) != len(intervals):
359		raise ValueError(
360			f"qualities length ({len(qualities)}) must match "
361			f"intervals length ({len(intervals)})"
362		)
363
364	INTERVAL_DEFINITIONS[name] = intervals
365	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: Root note name (``"C"``, ``"F#"``, ``"Bb"``, etc.).
222		mode: Scale mode name. Supports all keys of :data:`SCALE_MODE_MAP`
223		      (e.g. ``"ionian"``, ``"dorian"``, ``"natural_minor"``,
224		      ``"major_pentatonic"``). Use :func:`register_scale` for custom scales.
225		low: Lowest MIDI note (inclusive). When ``count`` is set, this is
226		     the starting note from which the scale ascends.
227		high: Highest MIDI note (inclusive). Ignored when ``count`` is set.
228		count: Exact number of notes to return. Notes ascend from ``low``
229		       through successive scale degrees, cycling into higher octaves
230		       as needed. When ``None`` (default), all scale tones between
231		       ``low`` and ``high`` are returned.
232
233	Returns:
234		Sorted list of MIDI note numbers.
235
236	Examples:
237		```python
238		import subsequence
239		import subsequence.constants.midi_notes as notes
240
241		# C major: all tones from middle C to C5
242		subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5)
243		# → [60, 62, 64, 65, 67, 69, 71, 72]
244
245		# E natural minor (aeolian) across one octave
246		subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3)
247		# → [40, 42, 43, 45, 47, 48, 50, 52]
248
249		# 15 notes of A minor pentatonic ascending from A3
250		subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15)
251		# → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91]
252		```
253	"""
254
255	key_pc = subsequence.chords.key_name_to_pc(key)
256	pcs = set(scale_pitch_classes(key_pc, mode))
257
258	if count is not None:
259		if not pcs:
260			return []
261		result: typing.List[int] = []
262		pitch = low
263		while len(result) < count and pitch <= 127:
264			if pitch % 12 in pcs:
265				result.append(pitch)
266			pitch += 1
267		return result
268
269	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: Root note name ("C", "F#", "Bb", etc.).
  • 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.
  • 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]
def bank_select(bank: int) -> Tuple[int, int]:
10def bank_select (bank: int) -> typing.Tuple[int, int]:
11
12	"""
13	Convert a 14-bit MIDI bank number to (MSB, LSB) for use with
14	``p.program_change()``.
15
16	MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and
17	CC 32 (Bank LSB).  Together they encode a 14-bit bank number in the
18	range 0–16,383:
19
20	    MSB = bank // 128   (upper 7 bits, sent on CC 0)
21	    LSB = bank % 128    (lower 7 bits, sent on CC 32)
22
23	Args:
24		bank: Integer bank number, 0–16,383.  Values outside this range are
25		      clamped.
26
27	Returns:
28		``(msb, lsb)`` tuple, each value in 0–127.
29
30	Example:
31		```python
32		msb, lsb = subsequence.bank_select(128)   # → (1, 0)
33		p.program_change(48, bank_msb=msb, bank_lsb=lsb)
34		```
35	"""
36
37	bank = max(0, min(16383, bank))
38	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)