subsequence

Subsequence - an algorithmic composition framework for Python.

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

What makes it different:

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

Composition tools:

  • Rhythm and feel. Euclidean and Bresenham generators, multi-voice weighted Bresenham distribution (bresenham_poly()), ghost note layers (ghost_fill()), position-aware note removal (thin() - the musical inverse of ghost_fill), evolving cellular-automaton rhythms (cellular_1d(), cellular_2d()), smooth Perlin noise (perlin_1d(), perlin_2d(), perlin_1d_sequence(), perlin_2d_grid()), deterministic chaos sequences (logistic_map()), pink 1/f noise (pink_noise()), L-system string rewriting (p.lsystem()), Markov-chain generation (p.markov()), aperiodic binary rhythms (p.thue_morse()), golden-ratio beat placement (p.golden()), Fibonacci cycles whose length the pitch pool chooses (p.fibonacci()), never-repeating two-voice wedges from Recamán's sequence (p.recaman()), Gray-Scott reaction-diffusion patterns (p.reaction_diffusion()), Lorenz strange-attractor generation (p.lorenz()), exhaustive pitch-subsequence melodies (p.de_bruijn()), step-wise melodies with guaranteed pitch diversity (p.self_avoiding_walk()), drones and explicit note on/off events (p.drone(), p.drone_off(), p.silence()), groove templates (Groove.swing(), Groove.from_agr()), swing via p.swing() (a shortcut for Groove.swing()), randomize, velocity shaping and ramps (p.build_velocity_ramp()), dropout, per-step probability, and polyrhythms via independent pattern lengths.
  • Melody generation. p.melody() with MelodicState applies the Narmour Implication-Realization model to single-note lines: continuation after small steps, reversal after large leaps, chord-tone weighting, range gravity, and pitch-diversity penalty. History persists across bar rebuilds for natural phrase continuity.
  • Chord parts. comp.chords() and p.progression() play a chord progression — generated from a chord-graph style or given explicitly — at a declared harmonic rhythm: a fixed length, a shaped [WHOLE, HALF, HALF] sequence, or between(WHOLE, 3 * WHOLE, step=WHOLE) for chords of varying, quantized length. Voicing density, detached articulation, and a seed for a fixed phrase are all declarative.
  • 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.
  • Sequences as lists. p.hit_steps("kick", [0, 4, 8, 12]) and p.sequence(steps=..., pitches=..., velocities=...) place rhythms and lines from plain Python lists - the vocabulary the generator and density helpers in sequence_utils all speak.
  • Scales. p.snap_to_scale() snaps notes to any scale. scale_notes() generates a list of MIDI note numbers from a key, mode, and range or note count - useful for arpeggios, Markov chains, and melodic walks. Built-in western and non-western modes, plus register_scale() for your own.
  • Microtonal tuning. composition.tuning() applies a tuning system globally; p.apply_tuning() overrides per-pattern. Supports Scala .scl files, explicit cent lists, frequency ratios, and N-TET equal temperaments. Polyphonic parts use explicit channel rotation so simultaneous notes can carry independent pitch bends without MPE. Compatible with any standard MIDI synthesiser.
  • Randomness tools. Weighted choice, no-repeat shuffle, random walk, probability gates. Deterministic seeding makes every decision repeatable: set it composition-wide (seed=42) or per generator (seed= on any generator, with rng= for an explicit instance — precedence rng > seed > the pattern's p.rng). See the README "Conventions" section for the API's shared vocabulary.
  • Pattern transforms. Legato, detached, fixed gate (p.duration()), reverse, time-stretch, rotate, transpose, invert, randomize, and conditional p.every().

Integration:

  • MIDI clock. Master (clock_output()) or follower (clock_follow=True). When multiple inputs are connected, only one may be designated as the master clock source; messages from other inputs are filtered to prevent sync interference. Sync to a DAW or drive hardware.
  • Latency compensation. Declare each output device's physical latency (latency_ms=); Subsequence delays the faster devices so a mix of hardware and slower software instruments sound together.
  • MIDI mirroring with per-device drum maps. Fan a pattern out to extra (device, channel) destinations; an entry can carry its own drum_note_map so one named drum hit re-resolves to the right voice on each device — a DRM1 and a General MIDI sampler alike.
  • Shared project definitions. load_definitions("project.yaml") reads a small per-project YAML file naming notes, CCs, channels, programs, and NRPNs — the same file the Subsample sampler reads — so both tools use identical names and a renumber is a single edit.
  • 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 held-note arpeggiator. composition.note_input() tracks the notes you hold on a keyboard; a pattern reads them with p.held_notes() and arpeggiates them (p.arpeggio(p.held_notes())), with release_ms debounce and latch. A performance layer over the deterministic composition - empty when rendering headlessly.
  • 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, detached, and staccato articulations visually distinct at a glance. Add grid_scale=2 to zoom in horizontally, revealing swing and groove micro-timing.
  • Web UI Dashboard (Beta). Enable with composition.web_ui() to broadcast live composition metadata and visualize piano-roll pattern grids in a reactive HTTP/WebSocket browser dashboard.
  • Ableton Link. Industry-standard wireless tempo/phase sync (comp.link(); requires pip install subsequence[link]). Any Link-enabled app on the same LAN — Ableton Live, iOS synths, other Subsequence instances — stays in time automatically.
  • Recording. Record to standard MIDI file. Render to file without waiting for real-time playback.
Minimal example:
import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums

comp = subsequence.Composition(bpm=120)

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

comp.play()

Community and Feedback:

Package-level exports: Composition, Chord, Groove, MelodicState, Tuning, Motif, Phrase, motif, sentence, period, Degree, ChordTone, Approach, MotifEvent, ControlEvent, Progression, ChordSpan, PitchSet, progression, Cadence, Section, Form, roles, sieve, residual_class, between, parse_chord, register_chord_quality, register_scale, scale_notes, bank_select, Definitions, load_definitions.

  1"""
  2Subsequence - an algorithmic composition framework for Python.
  3
  4Subsequence gives you a palette of mathematical building blocks -
  5Euclidean rhythms, cellular automata, L-systems, Markov chains,
  6cognitive melody generation - and a stateful engine that lets them
  7interact and evolve over time. Unlike tools that loop a fixed pattern
  8forever, Subsequence rebuilds every pattern fresh before each cycle
  9with full context, so algorithms feed into each other and compositions
 10emerge that no single technique could produce alone. It generates pure
 11MIDI (no audio engine) to control hardware synths, modular systems,
 12drum machines, or software VSTs/DAWs.
 13
 14What makes it different:
 15
 16- **A rich algorithmic palette.** Euclidean and Bresenham rhythm
 17  generators, cellular automata (1D and 2D), L-system string rewriting,
 18  Markov chains, cognitive melody via the Narmour model, probability-
 19  weighted ghost notes, position-aware thinning, drones and continuous
 20  notes, Perlin and pink noise, logistic chaos maps - plus groove
 21  templates, velocity shaping, and pitch-bend automation to shape
 22  how they sound.
 23- **Stateful patterns that evolve.** Each pattern is a Python function
 24  rebuilt fresh every cycle with full context - current chord, section,
 25  cycle count, shared data from other patterns. A Euclidean rhythm can
 26  thin itself as tension builds, a cellular automaton can seed from the
 27  harmony, and a Markov chain can shift behaviour between sections.
 28- **Optional chord graph.** Define weighted chord and key transitions
 29  via probability graphs, with gravity and automatic voice leading.
 30  A dozen built-in palettes and frozen progressions to lock some sections
 31  while others evolve freely. Layer on cognitive harmony for
 32  Narmour-based melodic inertia.
 33- **Sub-microsecond clock.** Hybrid sleep+spin timing achieves typical
 34  pulse jitter of < 5 us on Linux, with zero long-term drift.
 35- **Turn anything into music.** ``composition.schedule()`` runs any
 36  Python function on a beat cycle - APIs, sensors, files. Anything
 37  Python can reach becomes a musical parameter.
 38- **Pure MIDI, zero sound engine.** No audio synthesis, no heavyweight
 39  dependencies. Route to hardware synths, drum machines, Eurorack, or
 40  software instruments.
 41
 42Composition tools:
 43
 44- **Rhythm and feel.** Euclidean and Bresenham generators, multi-voice
 45  weighted Bresenham distribution (``bresenham_poly()``), ghost note
 46  layers (``ghost_fill()``), position-aware note removal (``thin()`` -
 47  the musical inverse of ``ghost_fill``), evolving cellular-automaton
 48  rhythms (``cellular_1d()``, ``cellular_2d()``), smooth Perlin noise (``perlin_1d()``,
 49  ``perlin_2d()``, ``perlin_1d_sequence()``, ``perlin_2d_grid()``),
 50  deterministic chaos sequences (``logistic_map()``), pink 1/f noise
 51  (``pink_noise()``), L-system string rewriting (``p.lsystem()``),
 52  Markov-chain generation (``p.markov()``), aperiodic binary rhythms
 53  (``p.thue_morse()``), golden-ratio beat placement (``p.golden()``),
 54  Fibonacci cycles whose length the pitch pool chooses (``p.fibonacci()``),
 55  never-repeating two-voice wedges from Recamán's sequence (``p.recaman()``),
 56  Gray-Scott reaction-diffusion patterns (``p.reaction_diffusion()``),
 57  Lorenz strange-attractor generation (``p.lorenz()``), exhaustive
 58  pitch-subsequence melodies (``p.de_bruijn()``), step-wise melodies
 59  with guaranteed pitch diversity (``p.self_avoiding_walk()``), drones
 60  and explicit note on/off events (``p.drone()``, ``p.drone_off()``,
 61  ``p.silence()``),
 62  groove templates (``Groove.swing()``, ``Groove.from_agr()``), swing via
 63  ``p.swing()`` (a shortcut for ``Groove.swing()``), randomize,
 64  velocity shaping and ramps (``p.build_velocity_ramp()``), dropout, per-step
 65  probability, and polyrhythms via independent pattern lengths.
 66- **Melody generation.** ``p.melody()`` with ``MelodicState`` applies
 67  the Narmour Implication-Realization model to single-note lines:
 68  continuation after small steps, reversal after large leaps, chord-tone
 69  weighting, range gravity, and pitch-diversity penalty.  History persists
 70  across bar rebuilds for natural phrase continuity.
 71- **Chord parts.** ``comp.chords()`` and ``p.progression()`` play a chord
 72  progression — generated from a chord-graph style or given explicitly — at a
 73  declared *harmonic rhythm*: a fixed length, a shaped ``[WHOLE, HALF, HALF]``
 74  sequence, or ``between(WHOLE, 3 * WHOLE, step=WHOLE)`` for chords of varying,
 75  quantized length.  Voicing density, ``detached`` articulation, and a seed for
 76  a fixed phrase are all declarative.
 77- **Expression.** CC messages/ramps, pitch bend, note-correlated
 78  bend/portamento/slide, program changes, SysEx, and OSC output - all
 79  from within patterns.
 80- **Form and structure.** Musical form as a weighted graph, ordered list,
 81  or generator. Patterns read ``p.section`` to adapt. Conductor signals
 82  (LFOs, ramps) shape intensity over time.
 83- **Sequences as lists.** ``p.hit_steps("kick", [0, 4, 8, 12])`` and
 84  ``p.sequence(steps=..., pitches=..., velocities=...)`` place rhythms
 85  and lines from plain Python lists - the vocabulary the generator and
 86  density helpers in ``sequence_utils`` all speak.
 87- **Scales.** ``p.snap_to_scale()`` snaps notes to any
 88  scale. ``scale_notes()`` generates a list of MIDI note numbers from
 89  a key, mode, and range or note count - useful for arpeggios, Markov
 90  chains, and melodic walks. Built-in western and non-western modes,
 91  plus ``register_scale()`` for your own.
 92- **Microtonal tuning.** ``composition.tuning()`` applies a tuning
 93  system globally; ``p.apply_tuning()`` overrides per-pattern.
 94  Supports Scala ``.scl`` files, explicit cent lists, frequency ratios,
 95  and N-TET equal temperaments. Polyphonic parts use explicit channel
 96  rotation so simultaneous notes can carry independent pitch bends
 97  without MPE. Compatible with any standard MIDI synthesiser.
 98- **Randomness tools.** Weighted choice, no-repeat shuffle, random
 99  walk, probability gates. Deterministic seeding makes every decision
100  repeatable: set it composition-wide (``seed=42``) or per generator
101  (``seed=`` on any generator, with ``rng=`` for an explicit instance —
102  precedence ``rng`` > ``seed`` > the pattern's ``p.rng``). See the
103  README "Conventions" section for the API's shared vocabulary.
104- **Pattern transforms.** Legato, detached, fixed gate (``p.duration()``),
105  reverse, time-stretch, rotate, transpose, invert, randomize, and
106  conditional ``p.every()``.
107
108Integration:
109
110- **MIDI clock.** Master (``clock_output()``) or follower
111  (``clock_follow=True``). When multiple inputs are connected, only
112  one may be designated as the master clock source; messages from
113  other inputs are filtered to prevent sync interference. Sync to a
114  DAW or drive hardware.
115- **Latency compensation.** Declare each output device's physical
116  latency (``latency_ms=``); Subsequence delays the faster devices so
117  a mix of hardware and slower software instruments sound together.
118- **MIDI mirroring with per-device drum maps.** Fan a pattern out to
119  extra ``(device, channel)`` destinations; an entry can carry its own
120  ``drum_note_map`` so one named drum hit re-resolves to the right voice
121  on each device — a DRM1 and a General MIDI sampler alike.
122- **Shared project definitions.** ``load_definitions("project.yaml")``
123  reads a small per-project YAML file naming notes, CCs, channels,
124  programs, and NRPNs — the same file the Subsample sampler reads — so
125  both tools use identical names and a renumber is a single edit.
126- **Hardware control.** CC input mapping from knobs/faders to
127  ``composition.data``; patterns read and write the same dict via
128  ``p.data`` for both external data access and cross-pattern
129  communication. OSC for bidirectional communication with mixers,
130  lighting, visuals.
131- **Live held-note arpeggiator.** ``composition.note_input()`` tracks the
132  notes you hold on a keyboard; a pattern reads them with ``p.held_notes()``
133  and arpeggiates them (``p.arpeggio(p.held_notes())``), with ``release_ms``
134  debounce and ``latch``. A performance layer over the deterministic
135  composition - empty when rendering headlessly.
136- **Live coding.** Hot-swap patterns, change tempo, mute/unmute, and
137  tweak parameters during playback via a built-in TCP eval server.
138- **Hotkeys.** Single keystrokes to jump sections, toggle mutes, or
139  fire any action - with optional bar-boundary quantization.
140- **Real-time pattern triggering.** ``composition.trigger()`` generates
141  one-shot patterns in response to sensors, OSC, or any event.
142- **Terminal display.** Live status line (BPM, bar, section, chord).
143  Add ``grid=True`` for an ASCII pattern grid showing velocity and
144  sustain - makes legato, detached, and staccato articulations visually
145  distinct at a glance.
146  Add ``grid_scale=2`` to zoom in horizontally, revealing swing and
147  groove micro-timing.
148- **Web UI Dashboard (Beta).** Enable with ``composition.web_ui()`` to 
149  broadcast live composition metadata and visualize piano-roll pattern 
150  grids in a reactive HTTP/WebSocket browser dashboard.
151- **Ableton Link.** Industry-standard wireless tempo/phase sync
152  (``comp.link()``; requires ``pip install subsequence[link]``).
153  Any Link-enabled app on the same LAN — Ableton Live, iOS synths,
154  other Subsequence instances — stays in time automatically.
155- **Recording.** Record to standard MIDI file. Render to file without
156  waiting for real-time playback.
157
158Minimal example:
159
160    ```python
161    import subsequence
162    import subsequence.constants.instruments.gm_drums as gm_drums
163
164    comp = subsequence.Composition(bpm=120)
165
166    @comp.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
167    def drums (p):
168        (p.hit_steps("kick_1",        [0, 4, 8, 12], velocity=100)
169          .hit_steps("snare_1",       [4, 12],        velocity=90)
170          .hit_steps("hi_hat_closed", range(16),      velocity=70))
171
172    comp.play()
173    ```
174
175Community and Feedback:
176
177- **Discussions:** Chat and ask questions at https://github.com/simonholliday/subsequence/discussions
178- **Issues:** Report bugs and request features at https://github.com/simonholliday/subsequence/issues
179
180Package-level exports: ``Composition``, ``Chord``, ``Groove``, ``MelodicState``, ``Tuning``, ``Motif``, ``Phrase``, ``motif``, ``sentence``, ``period``, ``Degree``, ``ChordTone``, ``Approach``, ``MotifEvent``, ``ControlEvent``, ``Progression``, ``ChordSpan``, ``PitchSet``, ``progression``, ``Cadence``, ``Section``, ``Form``, ``roles``, ``sieve``, ``residual_class``, ``between``, ``parse_chord``, ``register_chord_quality``, ``register_scale``, ``scale_notes``, ``bank_select``, ``Definitions``, ``load_definitions``.
181"""
182
183import subsequence.cadences
184import subsequence.chords
185import subsequence.forms
186import subsequence.roles
187import subsequence.composition
188import subsequence.definitions
189import subsequence.groove
190import subsequence.harmonic_rhythm
191import subsequence.intervals
192import subsequence.melodic_state
193import subsequence.midi_utils
194import subsequence.motifs
195import subsequence.progressions
196import subsequence.sequence_utils
197import subsequence.tuning
198
199
200Composition = subsequence.composition.Composition
201Motif = subsequence.motifs.Motif
202Phrase = subsequence.motifs.Phrase
203motif = subsequence.motifs.motif
204sentence = subsequence.motifs.sentence
205period = subsequence.motifs.period
206Cadence = subsequence.cadences.Cadence
207Section = subsequence.forms.Section
208Form = subsequence.forms.Form
209Degree = subsequence.motifs.Degree
210ChordTone = subsequence.motifs.ChordTone
211Approach = subsequence.motifs.Approach
212MotifEvent = subsequence.motifs.MotifEvent
213ControlEvent = subsequence.motifs.ControlEvent
214Progression = subsequence.progressions.Progression
215ChordSpan = subsequence.progressions.ChordSpan
216PitchSet = subsequence.progressions.PitchSet
217progression = subsequence.progressions.progression
218Chord = subsequence.chords.Chord
219Groove = subsequence.groove.Groove
220MelodicState = subsequence.melodic_state.MelodicState
221Tuning = subsequence.tuning.Tuning
222between = subsequence.harmonic_rhythm.between
223parse_chord = subsequence.chords.parse_chord
224register_chord_quality = subsequence.chords.register_chord_quality
225register_scale = subsequence.intervals.register_scale
226scale_notes = subsequence.intervals.scale_notes
227bank_select = subsequence.midi_utils.bank_select
228Definitions = subsequence.definitions.Definitions
229load_definitions = subsequence.definitions.load_definitions
230roles = subsequence.roles
231sieve = subsequence.sequence_utils.sieve
232residual_class = subsequence.sequence_utils.residual_class
class Composition:
1264class Composition:
1265
1266	"""
1267	The top-level controller for a musical piece.
1268	
1269	The ``Composition`` object manages the global clock (Sequencer), the harmonic
1270	progression (HarmonicState), the song structure (subsequence.form_state.FormState), and all MIDI patterns.
1271	It serves as the main entry point for defining your music.
1272	
1273	Typical workflow:
1274
1275	1. Initialize ``Composition`` with BPM and Key.
1276	2. Define harmony and form (optional).
1277	3. Register patterns using the ``@composition.pattern`` decorator.
1278	4. Call ``composition.play()`` to start the music.
1279	"""
1280
1281	def __init__ (
1282		self,
1283		output_device: typing.Optional[str] = None,
1284		bpm: float = 120,
1285		time_signature: typing.Tuple[int, int] = (4, 4),
1286		key: typing.Optional[str] = None,
1287		scale: typing.Optional[str] = None,
1288		seed: typing.Optional[int] = None,
1289		record: bool = False,
1290		record_filename: typing.Optional[str] = None,
1291		zero_indexed_channels: bool = False,
1292		latency_ms: float = 0.0
1293	) -> None:
1294
1295		"""
1296		Initialize a new composition.
1297
1298		Parameters:
1299			output_device: Which MIDI output port to use, matched against
1300				``mido.get_output_names()``.  The name is treated as a
1301				pattern: ``*`` stands for any run of characters and ``?``
1302				for exactly one, matching is case-insensitive, and a name
1303				with no wildcards is simply a substring — so a plain
1304				``"Scarlett"`` finds the port without typing the rest.
1305				An exact name always wins outright.
1306
1307				Wildcards matter on Linux/ALSA, where names carry the
1308				client and port ids (e.g.
1309				``"Scarlett 2i4 USB:Scarlett 2i4 USB MIDI 1 16:0"``).  The
1310				client id — ``16`` here — is handed out in connection order
1311				and moves between reboots or when a virtual port is
1312				recreated, while the port index after it (``0``) stays put.
1313				Wildcard the one that moves and keep the one that does not::
1314
1315				    "*Scarlett 2i4 USB *:0"
1316
1317				Keep that trailing port index.  A multi-port interface
1318				reports one name per port, so ``"*U6MIDI Pro*"`` matches
1319				all three ports of a 3-port unit and asks which you meant
1320				at every launch, while ``"*U6MIDI Pro *:0"`` names one for
1321				good.  Prefer ``*`` to ``?`` — ``?`` matches a single
1322				character, so a pattern written for ``16:0`` quietly stops
1323				matching once ids reach three digits.  To look up the
1324				current names::
1325
1326				    import mido
1327				    for n in mido.get_output_names(): print(n)
1328
1329				If ``None``, Subsequence auto-discovers — uses the only
1330				available device, or prompts to choose if several exist.
1331			bpm: Initial tempo in beats per minute (default 120).
1332			time_signature: The metre as ``(beats, unit)``, default ``(4, 4)``.
1333				Sets the bar length everywhere bars matter: ``bars=`` pattern
1334				lengths, ``p.bar``/``p.signal()``, form advancement and
1335				transitions, and pinned-chord bar numbers.
1336			key: The root key of the piece (e.g., "C", "F#", "Bb").
1337				Required if you plan to use ``harmony()``.
1338			scale: The scale/mode of the piece (e.g. "minor", "dorian",
1339				or any registered scale name).  Used to resolve scale
1340				degrees in motifs; defaults to major (ionian) when unset.
1341			seed: An optional integer for deterministic randomness. When set,
1342				every random decision (chord choices, drum probability, etc.)
1343				will be identical on every run.
1344			record: When True, record all MIDI events to a file.
1345			record_filename: Optional filename for the recording (defaults to timestamp).
1346			zero_indexed_channels: When False (default), MIDI channels use
1347				1-based numbering (1-16) matching instrument labelling.
1348				Channel 10 is drums, the way musicians and hardware panels
1349				show it. When True, channels use 0-based numbering (0-15)
1350				matching the raw MIDI protocol.
1351			latency_ms: Physical output latency of the primary device in
1352				milliseconds, for delay compensation (default 0.0, must be
1353				non-negative). Set this when the primary output sounds late
1354				(e.g. a software sampler) so Subsequence delays faster
1355				devices to line everything up. See ``midi_output()`` for
1356				additional devices.
1357
1358		Example:
1359			```python
1360			comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
1361			```
1362		"""
1363
1364		if latency_ms < 0:
1365			raise ValueError(f"latency_ms must be non-negative — got {latency_ms}")
1366
1367		self.output_device = output_device
1368		self.bpm = bpm
1369		self.time_signature = time_signature
1370		self.key = key
1371		self.scale = scale
1372		self._seed: typing.Optional[int] = seed
1373		self._zero_indexed_channels: bool = zero_indexed_channels
1374		self._output_latency_ms: float = latency_ms
1375
1376		# Determinism plumbing: named-stream derivation state.  Build-time
1377		# consumers draw per-call-salted streams (freeze:1, harmony:2, ...) so
1378		# adding one call never shifts another's stream; play-time pattern
1379		# streams are name-keyed in _build_pattern_from_pending.
1380		self._freeze_count: int = 0
1381		self._harmony_count: int = 0
1382		self._form_count: int = 0
1383		self._reroll_nonces: typing.Dict[str, int] = {}
1384		self._locked_names: typing.Set[str] = set()
1385
1386		self._sequencer = subsequence.sequencer.Sequencer(
1387			output_device_name = output_device,
1388			initial_bpm = bpm,
1389			time_signature = time_signature,
1390			record = record,
1391			record_filename = record_filename
1392		)
1393
1394		self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None
1395		self._harmony_cycle_beats: typing.Optional[int] = None
1396		self._harmony_style: typing.Optional[str] = None
1397		# The style (name or ChordGraph) from the most recent style-configuring
1398		# harmony() call — reused by parameter-only re-calls.
1399		self._last_harmony_style: typing.Optional[typing.Union[str, subsequence.chord_graphs.ChordGraph]] = None
1400		self._harmony_reschedule_lookahead: float = 1
1401		self._section_progressions: typing.Dict[str, Progression] = {}
1402		self._bound_progression: typing.Optional[Progression] = None
1403		self._pinned_chords: typing.Dict[int, typing.Any] = {}
1404		self._cadence_requests: typing.Dict[int, str] = {}
1405		self._section_cadences: typing.Dict[str, str] = {}
1406		self._harmony_horizon = _HarmonyHorizon()
1407		# True once the span-walking clock is registered for this playback —
1408		# lets a first mid-playback harmony() call start it exactly once.
1409		self._harmonic_clock_started: bool = False
1410		self._section_motifs: typing.Dict[typing.Tuple[str, typing.Optional[str]], typing.Any] = {}
1411		self._energy_map: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]] = {}
1412		self._form_has_payload: bool = False
1413		self._form_key: typing.Optional[str] = None
1414		self._form_scale: typing.Optional[str] = None
1415		# Cache of section progressions resolved against an effective key/scale
1416		# (key-relative section harmony re-keys per occurrence; resolution is a
1417		# pure function of (content, key, scale), so this is just memoisation).
1418		self._resolved_section_cache: typing.Dict[typing.Tuple[str, typing.Optional[str], typing.Optional[str]], Progression] = {}
1419		self._transitions: typing.List[_Transition] = []
1420		self._transition_muted: typing.Set[str] = set()
1421		self._pending_patterns: typing.List[_PendingPattern] = []
1422		# Names of patterns declared by the most recent live-reload exec (added by
1423		# pattern()/layer() as they run); the deletion diff in _apply_source_async
1424		# compares this against the same source's PREVIOUS exec.
1425		self._declared_names: typing.Set[str] = set()
1426		# Per-source declaration history: source label/path → the names it
1427		# declared last time it was exec'd.  The deletion diff unregisters only
1428		# names a source used to declare and no longer does — never patterns
1429		# registered by the wrapper script or by another watched source.
1430		self._source_declared: typing.Dict[str, typing.Set[str]] = {}
1431		self._pending_scheduled: typing.List[_PendingScheduled] = []
1432		self._form_state: typing.Optional[subsequence.form_state.FormState] = None
1433		self._builder_bar: int = 0
1434		self._display: typing.Optional[subsequence.display.Display] = None
1435		self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None
1436		self._live_reloader: typing.Optional[subsequence.live_reloader.LiveReloader] = None
1437		self._is_live: bool = False
1438		self._running_patterns: typing.Dict[str, typing.Any] = {}
1439		self._input_device: typing.Optional[str] = None
1440		self._input_device_alias: typing.Optional[str] = None
1441		self._clock_follow: bool = False
1442		self._clock_output: bool = False
1443		self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = []
1444		self._cc_forwards: typing.List[typing.Dict[str, typing.Any]] = []
1445		# Held-note input config from note_input() (None = not declared).
1446		self._note_input: typing.Optional[typing.Dict[str, typing.Any]] = None
1447		# Additional output devices registered with midi_output() after construction.
1448		self._additional_outputs: typing.List[_AdditionalOutput] = []
1449		# Additional input devices: (device_name: str, alias: Optional[str], clock_follow: bool)
1450		self._additional_inputs: typing.List[typing.Tuple[str, typing.Optional[str], bool]] = []
1451		# Maps alias/name → output device index (populated in _run after all devices are opened).
1452		self._output_device_names: typing.Dict[str, int] = {}
1453		# Maps alias/name → input device index (populated in _run after all input devices are opened).
1454		self._input_device_names: typing.Dict[str, int] = {}
1455		self.data: typing.Dict[str, typing.Any] = {}
1456		self._osc_server: typing.Optional[subsequence.osc.OscServer] = None
1457		self.conductor = subsequence.conductor.Conductor()
1458		self._web_ui_enabled: bool = False
1459		self._web_ui_http_host: str = "127.0.0.1"
1460		self._web_ui_ws_host: str = "127.0.0.1"
1461		self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None
1462		self._link_quantum: typing.Optional[float] = None
1463
1464		# Hotkey state — populated by hotkeys() and hotkey().
1465		self._hotkeys_enabled: bool = False
1466		self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {}
1467		self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = []
1468		self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None
1469
1470		# Tuning state — populated by tuning().
1471		self._tuning: typing.Optional[typing.Any] = None       # subsequence.tuning.Tuning
1472		self._tuning_bend_range: float = 2.0
1473		self._tuning_channels: typing.Optional[typing.List[int]] = None
1474		self._tuning_reference_note: int = 60
1475		self._tuning_exclude_drums: bool = True
1476
1477	def _resolve_device_id (self, device: subsequence.midi_utils.DeviceId) -> int:
1478		"""Resolve an output device id (None/int/str) to an integer index.
1479
1480		``None`` → 0 (primary device).  ``int`` → returned as-is.
1481		``str`` → looked up in ``_output_device_names``; logs a warning and
1482		returns 0 if the name is unknown (called after all devices are opened
1483		in ``_run()``).
1484		"""
1485		if device is None:
1486			return 0
1487		if isinstance(device, int):
1488			return device
1489		idx = self._output_device_names.get(device)
1490		if idx is None:
1491			logger.warning(
1492				f"Unknown output device name '{device}' — routing to device 0. "
1493				f"Available names: {list(self._output_device_names.keys())}"
1494			)
1495			return 0
1496		return idx
1497
1498	def _resolve_input_device_id (self, device: subsequence.midi_utils.DeviceId) -> typing.Optional[int]:
1499		"""Resolve an input device id (None/int/str) to an integer index.
1500
1501		``None`` → ``None`` (matches any input device — existing behaviour).
1502		``int`` → returned as-is.  ``str`` → looked up in ``_input_device_names``;
1503		logs a warning and returns ``-1`` if the name is unknown — an index no
1504		real device carries, so the mapping matches NOTHING (returning None
1505		here would silently fail OPEN and listen to every device).
1506		Called after all input devices are opened in ``_run()``.
1507		"""
1508		if device is None:
1509			return None
1510		if isinstance(device, int):
1511			return device
1512		idx = self._input_device_names.get(device)
1513		if idx is None:
1514			logger.warning(
1515				f"Unknown input device name '{device}' — mapping will be ignored. "
1516				f"Available names: {list(self._input_device_names.keys())}"
1517			)
1518			return -1
1519		return idx
1520
1521	def _resolve_pending_devices (self) -> None:
1522		"""Resolve name-based device ids on pending patterns now that all output devices are open."""
1523		for pending in self._pending_patterns:
1524			if isinstance(pending.raw_device, str):
1525				pending.device = self._resolve_device_id(pending.raw_device)
1526
1527	async def _activate_new_pending_patterns (self) -> None:
1528
1529		"""Build and schedule any pending patterns whose names are not yet running.
1530
1531		Used by ``LiveReloader._reload_async`` to bring NEW patterns added
1532		in a live reload into rotation mid-flight.  Existing patterns
1533		hot-swap via the decorator (their ``_builder_fn`` is replaced in
1534		place); only patterns whose names are not yet in ``_running_patterns``
1535		need this graduation step.
1536
1537		Newly-scheduled patterns start at the current sequencer pulse —
1538		they'll generate events from now onward, and the next reschedule
1539		will fire at the same offset as their primary cycle.
1540		"""
1541
1542		# Resolve any deferred string-device names against the now-open
1543		# device registry (no-op for int/None devices).
1544		self._resolve_pending_devices()
1545
1546		# Dedupe by name, last declaration wins — re-declaring a pattern in a
1547		# reloaded source must not schedule two copies.
1548		new_by_name: typing.Dict[str, _PendingPattern] = {}
1549
1550		for pending in self._pending_patterns:
1551			if pending.builder_fn.__name__ not in self._running_patterns:
1552				new_by_name[pending.builder_fn.__name__] = pending
1553
1554		new_pending = list(new_by_name.values())
1555
1556		if not new_pending:
1557			return
1558
1559		current_pulse = self._sequencer.pulse_count
1560
1561		for pending in new_pending:
1562
1563			pattern = self._build_pattern_from_pending(pending, start_pulse = current_pulse)
1564			await self._sequencer.schedule_pattern_repeating(pattern, start_pulse = current_pulse)
1565			self._running_patterns[pending.builder_fn.__name__] = pattern
1566
1567			logger.info(f"Live-reload: scheduled new pattern '{pending.builder_fn.__name__}'")
1568
1569		# Prune graduated (and stale duplicate) declarations: leaving them in
1570		# _pending_patterns resurrected deleted patterns on every later reload.
1571		self._pending_patterns = [
1572			pending for pending in self._pending_patterns
1573			if pending.builder_fn.__name__ not in self._running_patterns
1574		]
1575
1576	def _resolve_channel (self, channel: int) -> int:
1577
1578		"""
1579		Convert a user-supplied MIDI channel to the 0-indexed value used internally.
1580
1581		When ``zero_indexed_channels`` is False (default), the channel is
1582		validated as 1-16 and decremented by one. When True (0-indexed), the
1583		channel is validated as 0-15 and returned unchanged.
1584		"""
1585
1586		if self._zero_indexed_channels:
1587			if not 0 <= channel <= 15:
1588				raise ValueError(f"MIDI channel must be 0-15 (zero_indexed_channels=True), got {channel}")
1589			return channel
1590		else:
1591			if not 1 <= channel <= 16:
1592				raise ValueError(f"MIDI channel must be 1-16, got {channel}")
1593			return channel - 1
1594
1595	def _resolve_mirrors (
1596		self,
1597		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]],
1598		primary: typing.Optional[typing.Tuple[int, int]] = None,
1599	) -> typing.List[subsequence.pattern.MirrorSpec]:
1600
1601		"""
1602		Validate and normalise a list of mirror destinations.
1603
1604		Each entry is a 2- or 3-element sequence — ``(device_idx, channel)`` or
1605		``(device_idx, channel, drum_note_map)`` — as a tuple, list, or any such
1606		iterable.  ``channel`` is expressed in the user's channel-numbering
1607		convention (1-16 by default, 0-15 when ``zero_indexed_channels=True``);
1608		this method converts it to canonical 0-indexed form and rejects
1609		malformed entries.  The optional ``drum_note_map`` is preserved verbatim
1610		so the sequencer can re-resolve mirrored drum names per device.
1611
1612		String device names are NOT supported here; users wanting a named
1613		device should pass the integer index returned from ``midi_output()``.
1614
1615		If ``primary=(device, channel)`` is supplied (canonical 0-indexed
1616		form), a mirror entry whose ``(device, channel)`` matches it triggers a
1617		``logger.warning`` — this is almost always a user error (every event
1618		would double-fire on the same destination).  The optional map is ignored
1619		for this comparison.  Skipped when ``primary`` is ``None``, since the
1620		runtime API call site supplies its own check.
1621		"""
1622
1623		if mirrors is None:
1624			return []
1625
1626		resolved: typing.List[subsequence.pattern.MirrorSpec] = []
1627
1628		for entry in mirrors:
1629
1630			# Accept any 2- or 3-element iterable (tuple, list, etc.) — config
1631			# files and JSON sources naturally produce lists.  Validate shape at
1632			# decoration time so bad inputs surface here instead of producing
1633			# inscrutable failures inside the sequencer.
1634			try:
1635				items = list(entry)
1636			except TypeError:
1637				raise ValueError(f"Mirror entry must be a (device, channel[, drum_note_map]) tuple — got {entry!r}")
1638
1639			if len(items) not in (2, 3):
1640				raise ValueError(f"Mirror entry must have 2 or 3 elements (device, channel[, drum_note_map]) — got {entry!r}")
1641
1642			device = items[0]
1643			channel = items[1]
1644			drum_map = items[2] if len(items) == 3 else None
1645
1646			if not isinstance(device, int) or isinstance(device, bool):
1647				raise ValueError(f"Mirror device must be an integer index — got {type(device).__name__} ({device!r})")
1648
1649			if not isinstance(channel, int) or isinstance(channel, bool):
1650				raise ValueError(f"Mirror channel must be an integer — got {type(channel).__name__} ({channel!r})")
1651
1652			if drum_map is not None and not isinstance(drum_map, dict):
1653				raise ValueError(f"Mirror drum_note_map must be a dict or None — got {type(drum_map).__name__} ({drum_map!r})")
1654
1655			resolved_channel = self._resolve_channel(channel)
1656
1657			if primary is not None and (device, resolved_channel) == primary:
1658				logger.warning(
1659					f"Mirror destination {(device, resolved_channel)} matches the pattern's primary destination "
1660					f"— every event will double-fire on this (device, channel).  This is almost "
1661					f"certainly unintended."
1662				)
1663
1664			resolved_entry: subsequence.pattern.MirrorSpec = (
1665				(device, resolved_channel)
1666				if drum_map is None
1667				else (device, resolved_channel, drum_map)
1668			)
1669			resolved.append(resolved_entry)
1670
1671		return resolved
1672
1673	@property
1674	def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]:
1675		"""The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called."""
1676		return self._harmonic_state
1677
1678	def current_chord (self) -> typing.Optional[typing.Any]:
1679
1680		"""The chord sounding at the playhead, or ``None`` without harmony.
1681
1682		Reads the harmony window at the current pulse, so it stays accurate
1683		under variable harmonic rhythm and clock lookahead (the engine's
1684		``current_chord`` flips *lookahead* beats early — this does not).
1685		Falls back to the engine's chord before playback starts.  The chord
1686		may be a decorated wrapper (``Am9``, ``C/G``) when the sounding span
1687		is spiced; it duck-types the ``Chord`` voicing protocol either way.
1688		"""
1689
1690		if not self._harmony_horizon.is_empty:
1691			beat = self._sequencer.pulse_count / self._sequencer.pulses_per_beat
1692			chord = self._harmony_horizon.chord_at(beat)
1693			if chord is not None:
1694				return chord
1695
1696		if self._harmonic_state is not None:
1697			return self._harmonic_state.get_current_chord()
1698
1699		return None
1700
1701	def _effective_key_scale (
1702		self,
1703		section_info: typing.Optional["subsequence.form_state.SectionInfo"],
1704	) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
1705
1706		"""Resolve the key and scale in force, by the key-source precedence.
1707
1708		The layered chain, key and scale resolved **independently** so a
1709		section can move the tonic, the mode, or both:
1710		``Section.key`` > form key (``form(key=)`` / ``Form(key=)``) >
1711		``Composition.key``, and likewise for scale.  This is the one place
1712		the tier order lives; every placement site routes through it so the
1713		section key reaches every compositional element uniformly (the
1714		three-intent model: only *key-relative* content reads this — absolute
1715		content ignores it, chord-relative content tracks the chord).
1716		"""
1717
1718		key: typing.Optional[str] = None
1719		scale: typing.Optional[str] = None
1720
1721		if section_info is not None:
1722			key = section_info.key
1723			scale = section_info.scale
1724
1725		if key is None:
1726			key = self._form_key
1727		if key is None:
1728			key = self.key
1729
1730		if scale is None:
1731			scale = self._form_scale
1732		if scale is None:
1733			scale = self.scale
1734
1735		return key, scale
1736
1737	def _resolve_section_progression (
1738		self,
1739		info: "subsequence.form_state.SectionInfo",
1740	) -> typing.Optional[Progression]:
1741
1742		"""Resolve a section's bound progression against its effective key/scale.
1743
1744		Concrete progressions (names, ``PitchSet``, frozen captures) are
1745		returned unchanged.  Key-relative ones resolve against the section's
1746		effective key+scale — memoised per ``(name, key, scale)`` so a stable
1747		section reuses one realisation and span identity is stable across
1748		ticks.  If no key is resolvable at this moment the section is skipped
1749		(returns ``None`` → falls through to the bound/live source) with a
1750		warning; the authoritative check runs at :meth:`play`/:meth:`render`.
1751		"""
1752
1753		raw = self._section_progressions.get(info.name)
1754
1755		if raw is None or raw.is_concrete:
1756			return raw
1757
1758		key, scale = self._effective_key_scale(info)
1759
1760		if key is None:
1761			logger.warning(
1762				"section_chords(%r) is key-relative but no key resolves for this section — "
1763				"skipping (the chords fall through to the live/bound source)",
1764				info.name,
1765			)
1766			return None
1767
1768		cache_key = (info.name, key, scale)
1769		cached = self._resolved_section_cache.get(cache_key)
1770
1771		if cached is not None:
1772			return cached
1773
1774		try:
1775			resolved = raw.resolve(key, scale or "ionian")
1776		except ValueError as error:
1777			# A degree out of range for the effective scale, or an unknown
1778			# scale — never let it escape the clock callback (that would kill
1779			# harmony for the rest of playback).  Skip the section; the _run
1780			# pre-flight catches the common case far earlier.
1781			logger.warning(
1782				"section_chords(%r) cannot resolve against %s %s (%s) — skipping; "
1783				"the chords fall through to the live/bound source",
1784				info.name, key, scale or "ionian", error,
1785			)
1786			return None
1787
1788		self._resolved_section_cache[cache_key] = resolved
1789		return resolved
1790
1791	@property
1792	def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]:
1793		"""The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called."""
1794		return self._form_state
1795
1796	@property
1797	def sequencer (self) -> subsequence.sequencer.Sequencer:
1798		"""The underlying ``Sequencer`` instance."""
1799		return self._sequencer
1800
1801	@property
1802	def running_patterns (self) -> typing.Dict[str, typing.Any]:
1803		"""The currently active patterns, keyed by name."""
1804		return self._running_patterns
1805
1806	@property
1807	def builder_bar (self) -> int:
1808		"""Current bar index used by pattern builders."""
1809		return self._builder_bar
1810
1811	def _require_harmonic_state (self) -> subsequence.harmonic_state.HarmonicState:
1812		"""Return the active HarmonicState, raising ValueError if none is configured."""
1813		if self._harmonic_state is None:
1814			raise ValueError(
1815				"harmony() must be called before this action — "
1816				"no harmonic state has been configured."
1817			)
1818		return self._harmonic_state
1819
1820	def _coerce_progression (self, source: typing.Any, what: str) -> Progression:
1821
1822		"""Coerce a Progression / element list / preset name and resolve it against the key.
1823
1824		Binding freezes one realisation (the value type's identity), so
1825		key-relative content resolves here, at bind time, against the
1826		composition's key and scale.  Used by the *global* bound progression
1827		(``harmony(progression=)``) — which is not section-scoped, so it has
1828		nothing to re-key against.
1829		"""
1830
1831		value = source if isinstance(source, Progression) else subsequence.progressions.progression(source)
1832
1833		if not value.is_concrete:
1834			if self.key is None:
1835				raise ValueError(
1836					f"{what} contains key-relative chords (degrees/romans) — "
1837					"set key= on the Composition so they can resolve"
1838				)
1839			value = value.resolve(self.key, self.scale or "ionian")
1840
1841		return value
1842
1843	def _coerce_section_progression (self, source: typing.Any) -> Progression:
1844
1845		"""Coerce a section progression, keeping key-relative content UNRESOLVED.
1846
1847		Section harmony re-keys per occurrence (the section/form/composition
1848		key in force when the section plays), so a key-relative progression is
1849		stored relative and resolved late, in the clock, against the section's
1850		effective key+scale — unlike the global bound progression, which
1851		freezes at bind.  Concrete content (chord names, frozen captures,
1852		``PitchSet``) is already absolute and never moves.
1853		"""
1854
1855		return source if isinstance(source, Progression) else subsequence.progressions.progression(source)
1856
1857	def harmony (
1858		self,
1859		style: typing.Optional[typing.Union[str, subsequence.chord_graphs.ChordGraph]] = None,
1860		cycle_beats: int = 4,
1861		dominant_7th: bool = True,
1862		gravity: float = 1.0,
1863		nir_strength: float = 0.5,
1864		minor_turnaround_weight: float = 0.0,
1865		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1866		reschedule_lookahead: float = 1,
1867		progression: typing.Optional[typing.Any] = None,
1868	) -> None:
1869
1870		"""
1871		Configure the harmonic logic and chord change intervals.
1872
1873		Two sources, combinable: a **bound progression** (``progression=`` — a
1874		:class:`Progression` value, an element list like ``[1, 6, 3, "bVII7"]``,
1875		or chord names) walked span by span on the global clock; and/or a
1876		**graph style** stepping live chords.  With only a progression bound,
1877		it loops on exhaustion; with a style configured too, exhaustion falls
1878		through to live stepping (the frozen-replay bridge).  Calling with
1879		neither argument keeps today's default live engine
1880		(``style="functional_major"``).
1881
1882		Parameters:
1883			style: The harmonic style to use. Built-in: "functional_major"
1884				(alias "diatonic_major"), "hooktheory_major" (alias
1885				"pop_major"), "turnaround", "aeolian_minor",
1886				"phrygian_minor", "lydian_major", "dorian_minor",
1887				"chromatic_mediant", "suspended", "mixolydian", "whole_tone",
1888				"diminished". See README for full descriptions.
1889			cycle_beats: How many beats each live chord lasts (default 4).
1890				Bound progressions carry their own harmonic rhythm in their
1891				spans, so this applies to live stepping only.  A re-call
1892				during playback takes effect from the next chord boundary;
1893				a FIRST harmony() call mid-playback starts the clock itself.
1894			dominant_7th: Whether to include V7 chords (default True).
1895			gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
1896			nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement
1897				expectations.
1898			minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
1899			root_diversity: Root-repetition damping (0.0 to 1.0). Each recent
1900				chord sharing a candidate's root reduces the weight to 40% at
1901				the default (0.4). Set to 1.0 to disable.
1902			reschedule_lookahead: How many beats in advance to calculate the
1903				next chord.
1904			progression: A progression to bind to the global clock.  Key-
1905				relative content resolves now, against the composition key
1906				and scale (binding freezes one realisation).
1907
1908		Example:
1909			```python
1910			# A moody minor progression that changes every 8 beats
1911			comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
1912
1913			# Manual harmony driving everything — loops forever
1914			comp.harmony(progression=subsequence.progression([1, 6, 3, 7]))
1915			```
1916		"""
1917
1918		if style is None and progression is None:
1919			# A parameter-only re-call (gravity=, cycle_beats=, ...) keeps the
1920			# configured style — defaulting unconditionally here would silently
1921			# replace e.g. aeolian_minor with functional_major.
1922			style = self._last_harmony_style if self._last_harmony_style is not None else "functional_major"
1923
1924		if style is not None:
1925
1926			if self.key is None:
1927				raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor")
1928
1929			preserved_history: typing.List[subsequence.chords.Chord] = []
1930			preserved_current: typing.Optional[subsequence.chords.Chord] = None
1931
1932			if self._harmonic_state is not None:
1933				preserved_history = self._harmonic_state.history.copy()
1934				preserved_current = self._harmonic_state.current_chord
1935
1936			# Per-call salted build stream (harmony:1, harmony:2, ...): a re-call
1937			# gets its own deterministic stream while history and current chord
1938			# are preserved above, and adding a re-call never shifts any other
1939			# consumer's stream.
1940			self._harmony_count += 1
1941
1942			self._harmonic_state = subsequence.harmonic_state.HarmonicState(
1943				key_name = self.key,
1944				graph_style = style,
1945				include_dominant_7th = dominant_7th,
1946				key_gravity_blend = gravity,
1947				nir_strength = nir_strength,
1948				minor_turnaround_weight = minor_turnaround_weight,
1949				root_diversity = root_diversity,
1950				rng = self._stream(f"harmony:{self._harmony_count}")
1951			)
1952
1953			if preserved_history:
1954				self._harmonic_state.history = preserved_history
1955			if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current):
1956				self._harmonic_state.current_chord = preserved_current
1957
1958			self._harmony_style = style if isinstance(style, str) else None
1959			self._last_harmony_style = style
1960
1961		if progression is not None:
1962			self._bound_progression = self._coerce_progression(progression, "harmony(progression=)")
1963
1964		self._harmony_cycle_beats = cycle_beats
1965		self._harmony_reschedule_lookahead = reschedule_lookahead
1966
1967		# A re-call invalidates whatever the horizon had planned.
1968		self._harmony_horizon.invalidate_future()
1969
1970		# A FIRST harmony() call mid-playback must start the clock itself —
1971		# _run() only schedules clocks for sources it can see at play() time.
1972		# (Re-calls need nothing here: the clock reads its sources through
1973		# getters on every tick.)
1974		loop = self._sequencer._event_loop
1975
1976		if loop is not None and loop.is_running() and not self._harmonic_clock_started:
1977			try:
1978				on_loop = asyncio.get_running_loop() is loop
1979			except RuntimeError:
1980				on_loop = False
1981
1982			if on_loop:
1983				loop.create_task(self._start_harmonic_clock())
1984			else:
1985				asyncio.run_coroutine_threadsafe(self._start_harmonic_clock(), loop)
1986
1987	async def _start_harmonic_clock (self, bar_beats: typing.Optional[float] = None, clock_lookahead: typing.Optional[float] = None) -> None:
1988
1989		"""Register the span-walking harmonic clock (idempotent per playback).
1990
1991		Called from ``_run()`` when a harmony source exists at play time, and
1992		from ``harmony()`` when the FIRST source arrives mid-playback.
1993		``bar_beats``/``clock_lookahead`` default to a fresh computation for
1994		the mid-playback path; ``_run()`` passes the values it validated.
1995		"""
1996
1997		if self._harmonic_clock_started:
1998			return
1999
2000		self._harmonic_clock_started = True
2001
2002		if bar_beats is None:
2003			bar_beats = float(self.time_signature[0])
2004
2005		if clock_lookahead is None:
2006			lookaheads = [pattern.reschedule_lookahead for pattern in self._running_patterns.values()]
2007			clock_lookahead = min(bar_beats, max(1.0, float(self._harmony_reschedule_lookahead), float(max(lookaheads, default = 1))))
2008
2009		def _get_section_progression () -> typing.Optional[typing.Tuple[str, int, int, typing.Optional[Progression]]]:
2010			"""Return (section_name, section_index, bars, Progression|None) for the current section, or None.
2011
2012			The progression is resolved against the section's effective
2013			key/scale here (key-relative section harmony re-keys per
2014			occurrence); concrete content passes through unchanged.
2015			"""
2016			if self._form_state is None:
2017				return None
2018			info = self._form_state.get_section_info()
2019			if info is None:
2020				return None
2021			prog = self._resolve_section_progression(info)
2022			return (info.name, info.index, info.bars, prog)
2023
2024		def _resolve_cadence_formula (name: str) -> typing.List[subsequence.chords.Chord]:
2025			"""Resolve a cadence formula against the composition key and scale, at plan time."""
2026			hs = self._harmonic_state
2027			key_pc = subsequence.chords.key_name_to_pc(self.key) if self.key is not None else (hs.key_root_pc if hs is not None else 0)
2028			spec = subsequence.cadences.cadence_formula(name)
2029			return [
2030				subsequence.progressions.resolve_constraint(element, key_pc, self._constraint_scale(), f"cadence {name!r}")
2031				for element in spec.formula
2032			]
2033
2034		await schedule_harmonic_clock(
2035			sequencer = self._sequencer,
2036			get_harmonic_state = lambda: self._harmonic_state,
2037			horizon = self._harmony_horizon,
2038			bar_beats = bar_beats,
2039			cycle_beats = self._harmony_cycle_beats or 4,
2040			get_cycle_beats = lambda: self._harmony_cycle_beats or 4,
2041			get_bound_progression = lambda: self._bound_progression,
2042			get_section_progression = _get_section_progression,
2043			get_pinned = self._resolve_pin,
2044			cadence_requests = self._cadence_requests,
2045			resolve_cadence = _resolve_cadence_formula,
2046			get_section_cadence = self._section_cadences.get,
2047			reschedule_lookahead = clock_lookahead,
2048		)
2049
2050	def _constraint_scale (self) -> str:
2051
2052		"""The scale that hybrid-constraint ints resolve against.
2053
2054		The composition's own scale when set; otherwise inferred from the
2055		harmony style (``aeolian_minor`` → minor, matching
2056		:meth:`Progression.generate`'s documented inference), falling back
2057		to ionian.  Roman strings carry their quality and never need it.
2058		"""
2059
2060		if self.scale is not None:
2061			return self.scale
2062
2063		return subsequence.progressions._STYLE_SCALES.get(self._harmony_style or "", "ionian")
2064
2065	def freeze (
2066		self,
2067		bars: int,
2068		end: typing.Optional[typing.Any] = None,
2069		pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
2070		avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
2071		cadence: typing.Optional[str] = None,
2072	) -> "Progression":
2073
2074		"""Capture a chord progression from the live harmony engine.
2075
2076		Runs the harmony engine forward by *bars* chord changes, records each
2077		chord, and returns it as a :class:`Progression` that can be bound to a
2078		form section with :meth:`section_chords`.
2079
2080		The engine state **advances** — successive ``freeze()`` calls produce a
2081		continuing compositional journey so section progressions feel like parts
2082		of a whole rather than isolated islands.
2083
2084		The hybrid constraints compile into the walk: ``end=`` fixes the last
2085		bar ("end on V at bar 8"), ``pins=`` fix any 1-based bar, ``avoid=``
2086		excludes chords throughout.  Specs follow the progression-element
2087		grammar (ints where diatonic, roman/name strings where chromatic) and
2088		resolve against the composition key and scale.  A backward
2089		feasibility pass guarantees satisfiability before any chord is drawn;
2090		the forward walk keeps the engine's real history-dependent weighting.
2091		Bar 1 is always the engine's current chord — the journey continues —
2092		so ``pins={1: ...}`` may only name it redundantly.
2093
2094		Parameters:
2095			bars: Number of chords to capture (one per harmony cycle).
2096			end: The chord at the final bar — ``end="V"`` is the cadential
2097				major dominant in minor.
2098			pins: ``{bar: chord}`` — 1-based fiat positions.
2099			avoid: Chords excluded from the walk.
2100			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
2101				``"fakeout"``, theory aliases accepted) — its formula pins
2102				the final bars, so the walk approaches the close.
2103				Conflicts with ``end=`` or pins on those bars.
2104
2105		Returns:
2106			A :class:`Progression` with the captured chords and trailing
2107			history for NIR continuity.
2108
2109		Raises:
2110			ValueError: If :meth:`harmony` has not been called first, or the
2111				constraints are contradictory or unsatisfiable.
2112
2113		Example::
2114
2115			composition.harmony(style="functional_major", cycle_beats=4)
2116			verse  = composition.freeze(8, end="V")   # the verse sets up the chorus
2117			chorus = composition.freeze(4)            # next 4 chords, continuing on
2118			composition.section_chords("verse",  verse)
2119			composition.section_chords("chorus", chorus)
2120		"""
2121
2122		hs = self._require_harmonic_state()
2123
2124		if bars < 1:
2125			raise ValueError("bars must be at least 1")
2126
2127		if cadence is not None:
2128			pins = subsequence.progressions.cadence_pins(cadence, bars, pins, end)
2129			end = None
2130
2131		scale = self._constraint_scale()
2132		key_pc = subsequence.chords.key_name_to_pc(self.key) if self.key is not None else hs.key_root_pc
2133
2134		resolved_pins = {
2135			position: subsequence.progressions.resolve_constraint(spec, key_pc, scale, f"pins[{position}]")
2136			for position, spec in (pins or {}).items()
2137		}
2138		resolved_end = subsequence.progressions.resolve_constraint(end, key_pc, scale, "end") if end is not None else None
2139		resolved_avoid = [subsequence.progressions.resolve_constraint(spec, key_pc, scale, "avoid") for spec in (avoid or [])]
2140
2141		if 1 in resolved_pins and resolved_pins[1] != hs.current_chord:
2142			raise ValueError(
2143				f"pins[1]={resolved_pins[1].name()} conflicts with the engine's current chord "
2144				f"({hs.current_chord.name()}) — bar 1 of a freeze continues the journey; "
2145				"pin a later bar, or use pin_chord() for playback fiat"
2146			)
2147
2148		# Per-call salted stream (freeze:1, freeze:2, ...): each call's draws
2149		# are independent of every other consumer, so frozen progressions are
2150		# reproducible WITHOUT play() and adding a call cannot shift a
2151		# neighbour's output.  Engine state still advances normally — chord
2152		# continuity comes from current_chord/history, randomness from the
2153		# salted stream (swap-and-restore keeps hs.rng for play untouched).
2154		self._freeze_count += 1
2155		stream = self._stream(f"freeze:{self._freeze_count}")
2156		saved_rng = hs.rng
2157
2158		if stream is not None:
2159			hs.rng = stream
2160
2161		try:
2162			# The kernel with the engine's own hooks is draw-for-draw the old
2163			# step() loop when unconstrained — one walk path for both.
2164			def _commit (chosen: subsequence.chords.Chord) -> None:
2165				hs.current_chord = chosen
2166
2167			collected = subsequence.sequence_utils.constrained_walk(
2168				hs.graph,
2169				hs.current_chord,
2170				bars,
2171				rng = hs.rng,
2172				pins = resolved_pins,
2173				end = resolved_end,
2174				avoid = resolved_avoid,
2175				weight_modifier = hs._transition_weight,
2176				before_choice = hs._record_transition_source,
2177				after_choice = _commit,
2178			)
2179
2180			# Advance past the last captured chord so the next freeze() call or
2181			# live playback does not duplicate it.
2182			hs.step()
2183
2184		finally:
2185			hs.rng = saved_rng
2186
2187		span_beats = float(self._harmony_cycle_beats or 4)
2188
2189		return Progression(
2190			spans = tuple(
2191				subsequence.progressions.ChordSpan(chord = chord, beats = span_beats)
2192				for chord in collected
2193			),
2194			trailing_history = tuple(hs.history),
2195		)
2196
2197	def section_chords (self, section_name: str, progression: typing.Any) -> None:
2198
2199		"""Bind a :class:`Progression` to a named form section.
2200
2201		Every time *section_name* plays, the harmonic clock walks the
2202		progression's spans instead of calling the live engine.  Sections
2203		without a bound progression continue generating live chords.
2204
2205		Accepts a :class:`Progression` value (from :meth:`freeze`, the
2206		``progression()`` factory, or hand-built) or anything the factory
2207		accepts — an element list like ``[1, 6, 3, "bVII7"]`` or chord
2208		names.
2209
2210		**Key-relative content re-keys per occurrence.**  A progression
2211		written in degrees or romans is *key-relative* content: it resolves
2212		late, each time the section plays, against that section's effective
2213		key and scale (``Section.key`` > form key > composition key, with
2214		mode following the same chain).  So a ``Section(key="A")`` plays the
2215		same numbered progression a tone higher — its chords and its degrees
2216		share one tonic.  *Absolute* content — chord names (``"Am"``),
2217		:class:`~subsequence.progressions.PitchSet`, and frozen captures from
2218		:meth:`freeze` — names exact chords and is never transposed by a key.
2219
2220		On exhaustion mid-section the progression loops when no graph style
2221		is configured (and always when it contains a ``PitchSet``); with a
2222		live engine, exhaustion **falls through to live stepping in the
2223		COMPOSITION key** — the live graph engine does not transpose for a
2224		section (a stateful walk does not modulate mid-stream), so a
2225		re-keyed section that runs out of written chords hands off to
2226		composition-key harmony.  Bind a full-length progression (or set
2227		``at_end``/loop intent) if you need the whole section in its key.
2228
2229		Parameters:
2230			section_name: Name of the section as defined in :meth:`form`.
2231			progression: The progression to bind.
2232
2233		Raises:
2234			ValueError: If a graph-based form has been configured and
2235				*section_name* is not one of its sections.  List and generator
2236				forms yield names lazily, so they cannot be validated here.
2237				(A key-relative progression with no resolvable key for the
2238				section is caught at :meth:`play`/:meth:`render`, once the
2239				form's keys are known.)
2240
2241		Example::
2242
2243			composition.section_chords("verse",  verse_progression)
2244			composition.section_chords("chorus", [1, 6, 3, 7])
2245			# "bridge" is not bound — it generates live chords
2246		"""
2247
2248		if (
2249			self._form_state is not None
2250			and self._form_state._section_bars is not None
2251			and section_name not in self._form_state._section_bars
2252		):
2253			known = ", ".join(sorted(self._form_state._section_bars))
2254			raise ValueError(
2255				f"Section '{section_name}' not found in form. "
2256				f"Known sections: {known}"
2257			)
2258
2259		self._section_progressions[section_name] = self._coerce_section_progression(progression)
2260		self._resolved_section_cache = {}
2261		self._harmony_horizon.invalidate_future()
2262
2263	def pin_chord (self, bar: int, chord: typing.Optional[typing.Any]) -> None:
2264
2265		"""Force the chord sounding at a bar — fiat over live generation.
2266
2267		Whatever the harmonic source (live walk, bound progression, section
2268		progression) produces for *bar*, the pinned chord overrides it.
2269		Pass ``None`` to remove a pin.
2270
2271		Parameters:
2272			bar: 1-based bar number (the musician count).
2273			chord: A chord name, int degree, roman string, ``Chord``,
2274				``PitchSet``, or ``None`` to unpin.  A **key-relative** spec
2275				(int degree, roman) re-keys like section harmony: it resolves
2276				late, against the effective key of the section sounding at
2277				that bar (so ``pin_chord(8, "V")`` is the dominant of
2278				wherever bar 8 lands).  A **concrete** spec (name, ``Chord``,
2279				``PitchSet``) is absolute and never moves.
2280
2281		Example::
2282
2283			composition.pin_chord(8, "E7")    # the turnaround lands on E7
2284			composition.pin_chord(8, "V")     # the dominant of bar 8's section
2285			composition.pin_chord(8, None)    # let it walk again
2286		"""
2287
2288		if not isinstance(bar, int) or isinstance(bar, bool) or bar < 1:
2289			raise ValueError(f"bars are 1-based ints, got {bar!r}")
2290
2291		if chord is None:
2292			self._pinned_chords.pop(bar, None)
2293		else:
2294			# Store the parsed span — relative pins resolve late (per section)
2295			# at the clock; concrete pins are absolute.
2296			span = subsequence.progressions.parse_element(chord, beats = float(self.time_signature[0]))
2297
2298			if not span.is_concrete:
2299				# Raise early only when no key is resolvable for this bar — the
2300				# bar's own section (sequence forms) may supply one even with no
2301				# composition/form key.
2302				probe_info = self._form_state.section_info_at_bar(bar) if self._form_state is not None else None
2303				probe_key, _ = self._effective_key_scale(probe_info)
2304				if probe_key is None:
2305					raise ValueError(
2306						"pin_chord with a key-relative spec (degree/roman) needs a key — set key= on "
2307						"the Composition, a form key, or a Section.key for that bar (the pin re-keys "
2308						"to the section's effective key)"
2309					)
2310
2311			self._pinned_chords[bar] = span
2312
2313		self._harmony_horizon.invalidate_future()
2314
2315	def _resolve_pin (self, bar: int) -> typing.Optional[typing.Any]:
2316
2317		"""Resolve a stored pin to a chord-like, re-keying relative pins per section.
2318
2319		Concrete pins return their chord directly; key-relative pins resolve
2320		against the effective key of the section sounding now (the clock
2321		reads this as it reaches each bar).  Returns ``None`` (no pin / a
2322		relative pin with no resolvable key, warned) so the clock falls
2323		through to its normal source.
2324		"""
2325
2326		span = self._pinned_chords.get(bar)
2327
2328		if span is None:
2329			return None
2330
2331		if span.is_concrete:
2332			return _span_chord(span)
2333
2334		# Key the pin to the section that OWNS this bar (the clock's lookahead
2335		# can project a pin into a later, differently-keyed section while the
2336		# playhead is still earlier) — fall back to the playhead section where
2337		# a per-bar section is not computable (graph/generator forms).
2338		info: typing.Optional["subsequence.form_state.SectionInfo"] = None
2339		if self._form_state is not None:
2340			info = self._form_state.section_info_at_bar(bar)
2341			if info is None:
2342				info = self._form_state.get_section_info()
2343
2344		key, scale = self._effective_key_scale(info)
2345
2346		if key is None:
2347			logger.warning(
2348				"pin_chord(%d, ...) is key-relative but no key resolves for that bar — ignoring the pin",
2349				bar,
2350			)
2351			return None
2352
2353		return _span_chord(span.resolve(subsequence.chords.key_name_to_pc(key), scale or "ionian"))
2354
2355	def request_cadence (self, cadence: str = "strong", bar: typing.Optional[int] = None) -> None:
2356
2357		"""Ask the live engine to approach a cadence arriving at a bar.
2358
2359		The request hook: where :meth:`pin_chord` is fiat, this is a
2360		*steered approach* — at the next chord boundary the clock plans the
2361		remaining changes up to *bar* as a constrained walk through the
2362		engine's real weights, pinned to the cadence formula at the tail
2363		(``"strong"`` arrives V→I, ``"soft"`` IV→I, ``"open"`` IV→V,
2364		``"fakeout"`` V→vi; theory aliases accepted).  The chords still
2365		commit one boundary at a time, so the journey continues through the
2366		close.
2367
2368		One-shot: the request is consumed when planned.  Live harmony only —
2369		bound/section progressions are data and cannot be steered; a request
2370		whose bar passes unserved expires with a warning.  If the formula is
2371		not walkable from where the harmony stands, the arrival lands by
2372		fiat (loudly).  Ask at least a pattern-lookahead ahead: patterns may
2373		already have rendered against the previously planned chord.
2374
2375		Parameters:
2376			cadence: The cadence name.
2377			bar: The 1-based bar the cadence's final chord arrives at
2378				(required; in practice ≥ 2 — bar 1 cannot be approached).
2379
2380		Example::
2381
2382			composition.request_cadence("open", bar=16)    # hang on V at bar 16
2383		"""
2384
2385		spec = subsequence.cadences.cadence_formula(cadence)
2386
2387		if bar is None or not isinstance(bar, int) or isinstance(bar, bool) or bar < 1:
2388			raise ValueError(f"request_cadence needs bar= — the 1-based bar the cadence arrives at (got {bar!r})")
2389
2390		self._cadence_requests[bar] = spec.name
2391		self._harmony_horizon.invalidate_future()
2392
2393	def section_cadence (self, section_name: str, cadence: typing.Optional[str] = "strong") -> None:
2394
2395		"""Close every pass of a section with a cadence — the standing request.
2396
2397		Each time *section_name* is entered, the clock registers a
2398		:meth:`request_cadence` arriving at the section's final bar, so the
2399		harmony approaches the close as the section ends.  Live harmony
2400		only: a section with bound chords (:meth:`section_chords`) is data
2401		and ignores the registration — its closes are written, not steered.
2402		Pass ``None`` to unregister.
2403
2404		Example::
2405
2406			composition.form([("verse", 8), ("chorus", 8)])
2407			composition.section_cadence("verse", "open")     # every verse hangs on V
2408			composition.section_cadence("chorus", "strong")  # every chorus lands home
2409		"""
2410
2411		if cadence is None:
2412			self._section_cadences.pop(section_name, None)
2413			return
2414
2415		spec = subsequence.cadences.cadence_formula(cadence)
2416		self._section_cadences[section_name] = spec.name
2417
2418	def section_motifs (self, section_name: str, value: typing.Any, part: typing.Optional[str] = None) -> None:
2419
2420		"""Bind a Motif or Phrase to a named form section (per optional part).
2421
2422		Patterns read the binding back with ``p.section_motif(part)`` (or use
2423		the one-call :meth:`phrase_part`); a section with no binding for the
2424		part is silent for that part — bind material or don't, no fallback
2425		guessing.  Re-binding is idempotent, so the call is safe in a live
2426		file: re-executing on save is the desired rebind.
2427
2428		Parameters:
2429			section_name: Name of the section as defined in :meth:`form`.
2430			value: A ``Motif`` or ``Phrase`` (anything exposing
2431				``.length``/``.slice`` places).
2432			part: Optional part label, so one section can carry several
2433				bindings (``"lead"``, ``"bass"``, ...).
2434
2435		Raises:
2436			ValueError: If a graph-based form has been configured and
2437				*section_name* is not one of its sections.
2438
2439		Example::
2440
2441			composition.section_motifs("verse",  verse_line,  part="lead")
2442			composition.section_motifs("chorus", chorus_line, part="lead")
2443		"""
2444
2445		if not hasattr(value, "length") or not hasattr(value, "slice"):
2446			raise TypeError(
2447				f"section_motifs() binds Motif/Phrase values (.length/.slice) — got {type(value).__name__}"
2448			)
2449
2450		if (
2451			self._form_state is not None
2452			and self._form_state._section_bars is not None
2453			and section_name not in self._form_state._section_bars
2454		):
2455			known = ", ".join(sorted(self._form_state._section_bars))
2456			raise ValueError(
2457				f"Section '{section_name}' not found in form. "
2458				f"Known sections: {known}"
2459			)
2460
2461		self._section_motifs[(section_name, part)] = value
2462
2463	def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None:
2464
2465		"""
2466		Register a callback for a sequencer event (e.g., "bar", "start", "stop").
2467		"""
2468
2469		self._sequencer.on_event(event_name, callback)
2470
2471
2472	# -----------------------------------------------------------------------
2473	# Hotkey API
2474	# -----------------------------------------------------------------------
2475
2476	def hotkeys (self, enabled: bool = True) -> None:
2477
2478		"""Enable or disable the global hotkey listener.
2479
2480		Must be called **before** :meth:`play` to take effect.  When enabled, a
2481		background thread reads single keystrokes from stdin without requiring
2482		Enter.  The ``?`` key is always reserved and lists all active bindings.
2483
2484		Hotkeys have zero impact on playback when disabled — the listener
2485		thread is never started.
2486
2487		Args:
2488		    enabled: ``True`` (default) to enable hotkeys; ``False`` to disable.
2489
2490		Example::
2491
2492		    composition.hotkeys()
2493		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
2494		    composition.play()
2495		"""
2496
2497		self._hotkeys_enabled = enabled
2498
2499
2500	def hotkey (
2501		self,
2502		key:      str,
2503		action:   typing.Callable[[], None],
2504		quantize: int = 0,
2505		label:    typing.Optional[str] = None,
2506	) -> None:
2507
2508		"""Register a single-key shortcut that fires during playback.
2509
2510		The listener must be enabled first with :meth:`hotkeys`.
2511
2512		Most actions — form jumps, ``composition.data`` writes, and
2513		:meth:`tweak` calls — should use ``quantize=0`` (the default).  Their
2514		musical effect is naturally delayed to the next pattern rebuild cycle,
2515		which provides automatic musical quantization without extra configuration.
2516
2517		Use ``quantize=N`` for actions where you want an explicit bar-boundary
2518		guarantee, such as :meth:`mute` / :meth:`unmute`.
2519
2520		The ``?`` key is reserved and cannot be overridden.
2521
2522		Args:
2523		    key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``).
2524		    action: Zero-argument callable to execute.
2525		    quantize: ``0`` = execute immediately (default).  ``N`` = execute
2526		        on the next global bar number divisible by *N*.
2527		    label: Display name for the ``?`` help listing.  Auto-derived from
2528		        the function name or lambda body if omitted.
2529
2530		Raises:
2531		    ValueError: If ``key`` is the reserved ``?`` character, or if
2532		        ``key`` is not exactly one character.
2533
2534		Example::
2535
2536		    composition.hotkeys()
2537
2538		    # Immediate — musical effect happens at next pattern rebuild
2539		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
2540		    composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
2541
2542		    # Explicit 4-bar phrase boundary
2543		    composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
2544
2545		    # Named function — label is derived automatically
2546		    def drop_to_breakdown ():
2547		        composition.form_jump("breakdown")
2548		        composition.mute("lead")
2549
2550		    composition.hotkey("d", drop_to_breakdown)
2551
2552		    composition.play()
2553		"""
2554
2555		if len(key) != 1:
2556			raise ValueError(f"hotkey key must be a single character, got {key!r}")
2557
2558		if key == _HOTKEY_RESERVED:
2559			raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.")
2560
2561		derived = label if label is not None else _derive_label(action)
2562
2563		self._hotkey_bindings[key] = HotkeyBinding(
2564			key      = key,
2565			action   = action,
2566			quantize = quantize,
2567			label    = derived,
2568		)
2569
2570
2571	def form_jump (self, section_name: str) -> None:
2572
2573		"""Jump the form to a named section immediately.
2574
2575		Delegates to :meth:`subsequence.form_state.FormState.jump_to`.  Only works when the
2576		composition uses graph-mode form (a dict passed to :meth:`form`).
2577
2578		The musical effect is heard at the *next pattern rebuild cycle* — already-
2579		queued MIDI notes are unaffected.  This natural delay means ``form_jump``
2580		is effective without needing explicit quantization.
2581
2582		Args:
2583		    section_name: The section to jump to.
2584
2585		Raises:
2586		    ValueError: If no form is configured, or the form is not in graph
2587		        mode, or *section_name* is unknown.
2588
2589		Example::
2590
2591		    composition.hotkey("c", lambda: composition.form_jump("chorus"))
2592		"""
2593
2594		if self._form_state is None:
2595			raise ValueError("form_jump() requires a form to be configured via composition.form().")
2596
2597		self._form_state.jump_to(section_name)
2598
2599		# The harmony horizon planned against the old section — revoke it.
2600		self._harmony_horizon.invalidate_future()
2601
2602
2603	def form_next (self, section_name: str) -> None:
2604
2605		"""Queue the next section — takes effect when the current section ends.
2606
2607		Unlike :meth:`form_jump`, this does not interrupt the current section.
2608		The queued section replaces the automatically pre-decided next section
2609		and takes effect at the natural section boundary.  The performer can
2610		change their mind by calling ``form_next`` again before the boundary.
2611
2612		Delegates to :meth:`subsequence.form_state.FormState.queue_next`.  Only works when the
2613		composition uses graph-mode form (a dict passed to :meth:`form`).
2614
2615		Args:
2616		    section_name: The section to queue.
2617
2618		Raises:
2619		    ValueError: If no form is configured, or the form is not in graph
2620		        mode, or *section_name* is unknown.
2621
2622		Example::
2623
2624		    composition.hotkey("c", lambda: composition.form_next("chorus"))
2625		"""
2626
2627		if self._form_state is None:
2628			raise ValueError("form_next() requires a form to be configured via composition.form().")
2629
2630		self._form_state.queue_next(section_name)
2631
2632		# The harmony horizon planned against the old continuation — revoke it.
2633		self._harmony_horizon.invalidate_future()
2634
2635
2636	def _list_hotkeys (self) -> None:
2637
2638		"""Log all active hotkey bindings (triggered by the ``?`` key).
2639
2640		Output appears via the standard logger so it scrolls cleanly above
2641		the :class:`~subsequence.display.Display` status line.
2642		"""
2643
2644		lines = ["Active hotkeys:"]
2645		for key in sorted(self._hotkey_bindings):
2646			b = self._hotkey_bindings[key]
2647			quant_str = "immediate" if b.quantize == 0 else f"quantize={b.quantize}"
2648			lines.append(f"  {key}  \u2192  {b.label}  ({quant_str})")
2649		lines.append(f"  ?  \u2192  list hotkeys")
2650		logger.info("\n".join(lines))
2651
2652
2653	def _process_hotkeys (self, bar: int) -> None:
2654
2655		"""Drain pending keystrokes and execute due actions.
2656
2657		Called on every ``"bar"`` event by the sequencer when hotkeys are
2658		enabled.  Handles both immediate (``quantize=0``) and quantized actions.
2659
2660		Both kinds run here, on the bar-event callback (the event loop): the
2661		keystroke listener thread only enqueues keypresses (``drain()``), it
2662		never executes actions.  Immediate (``quantize=0``) bindings fire as soon
2663		as the key is drained; quantized ones wait for their next boundary.
2664
2665		Args:
2666		    bar: The current global bar number from the sequencer.
2667		"""
2668
2669		if self._keystroke_listener is None:
2670			return
2671
2672		# Process newly arrived keys.
2673		for key in self._keystroke_listener.drain():
2674
2675			if key == _HOTKEY_RESERVED:
2676				self._list_hotkeys()
2677				continue
2678
2679			binding = self._hotkey_bindings.get(key)
2680			if binding is None:
2681				continue
2682
2683			if binding.quantize == 0:
2684				# Immediate — execute now (we're on the bar-event callback,
2685				# which is safe for all mutation methods).
2686				try:
2687					binding.action()
2688					logger.info(f"Hotkey '{key}' \u2192 {binding.label}")
2689				except Exception as exc:
2690					logger.warning(f"Hotkey '{key}' action raised: {exc}")
2691			else:
2692				# Defer until the next quantize boundary.
2693				self._pending_hotkey_actions.append(
2694					_PendingHotkeyAction(binding=binding)
2695				)
2696
2697		# Fire any pending actions whose bar boundary has arrived.
2698		still_pending: typing.List[_PendingHotkeyAction] = []
2699
2700		for pending in self._pending_hotkey_actions:
2701			if bar % pending.binding.quantize == 0:
2702				try:
2703					pending.binding.action()
2704					logger.info(
2705						f"Hotkey '{pending.binding.key}' \u2192 {pending.binding.label} "
2706						f"(bar {bar})"
2707					)
2708				except Exception as exc:
2709					logger.warning(
2710						f"Hotkey '{pending.binding.key}' action raised: {exc}"
2711					)
2712			else:
2713				still_pending.append(pending)
2714
2715		self._pending_hotkey_actions = still_pending
2716
2717	@property
2718	def seed (self) -> typing.Optional[int]:
2719
2720		"""
2721		The composition's random seed, or None when unseeded.
2722
2723		When set, every random decision derives deterministically from this
2724		value through named streams (see ``seed_for()``), so the same script
2725		produces the same music on every run.  Assign to set it::
2726
2727			comp.seed = 42
2728
2729		(Formerly the method ``comp.seed(42)`` — the call form is a hard
2730		break per the pre-1.0 rename policy.)
2731		"""
2732
2733		return self._seed
2734
2735	@seed.setter
2736	def seed (self, value: typing.Optional[int]) -> None:
2737
2738		"""Set the composition seed (``comp.seed = 42``)."""
2739
2740		self._seed = value
2741
2742	def _stream_seed (self, name: str) -> typing.Optional[int]:
2743
2744		"""
2745		Derive the effective integer seed for a named random stream.
2746
2747		The derivation is ``zlib.crc32(f"{seed}:{name}")`` — crc32 rather
2748		than ``hash()`` because it is stable across processes — plus the
2749		per-name nonce when ``reroll()`` has been called.  Returns None when
2750		the composition is unseeded.
2751		"""
2752
2753		if self._seed is None:
2754			return None
2755
2756		nonce = self._reroll_nonces.get(name, 0)
2757		key = f"{self._seed}:{name}" if nonce == 0 else f"{self._seed}:{name}:{nonce}"
2758		return zlib.crc32(key.encode())
2759
2760	def _stream (self, name: str) -> typing.Optional[random.Random]:
2761
2762		"""A fresh ``random.Random`` for a named stream, or None when unseeded."""
2763
2764		stream_seed = self._stream_seed(name)
2765		return None if stream_seed is None else random.Random(stream_seed)
2766
2767	def seed_for (self, name: str) -> typing.Optional[int]:
2768
2769		"""
2770		Surface the effective derived seed for a named stream.
2771
2772		Works for pattern names and equally for any name you invent for a
2773		standalone value generator (``seed=composition.seed_for("hook")``),
2774		so its randomness keys off the composition seed without sharing any
2775		other consumer's stream.  Reflects ``reroll()`` nonces.  Returns None
2776		when the composition is unseeded.
2777
2778		Example:
2779			```python
2780			hook_seed = composition.seed_for("hook")
2781			```
2782		"""
2783
2784		return self._stream_seed(name)
2785
2786	def reroll (self, name: str) -> None:
2787
2788		"""
2789		Deal a named stream a fresh deterministic seed — try a new variation.
2790
2791		Bumps the per-name nonce and prints the new effective seed.  The
2792		nonce lives only in this process, so the printed seed is what lets a
2793		variation you like survive a restart: note it down, or ``lock()`` the
2794		name to pin it for the session.  Refuses on locked names.
2795
2796		Parameters:
2797			name: The stream name — usually a pattern name.
2798
2799		Example:
2800			```python
2801			comp.reroll("lead")    # prints: reroll('lead') -> effective seed ...
2802			```
2803		"""
2804
2805		if name in self._locked_names:
2806			print(f"reroll('{name}') refused: '{name}' is locked - call unlock('{name}') first")
2807			return
2808
2809		self._reroll_nonces[name] = self._reroll_nonces.get(name, 0) + 1
2810		effective = self._stream_seed(name)
2811
2812		if effective is None:
2813			print(f"reroll('{name}'): composition has no seed - randomness is unseeded")
2814			return
2815
2816		running = self._running_patterns.get(name)
2817
2818		if running is not None and hasattr(running, "_rng"):
2819			running._rng = random.Random(effective)
2820
2821		print(f"reroll('{name}') -> effective seed {effective} (nonce {self._reroll_nonces[name]})")
2822
2823	def lock (self, name: str) -> None:
2824
2825		"""
2826		Pin a named stream: keep its current effective seed and realization.
2827
2828		Engine-side state, so it survives live reload (it is never a builder
2829		swap): a locked pattern re-deals its stream from the same effective
2830		seed on every rebuild, so every cycle realizes identically, and
2831		``reroll()`` refuses with a message until ``unlock()``.
2832
2833		Parameters:
2834			name: The stream name — usually a pattern name.
2835		"""
2836
2837		self._locked_names.add(name)
2838
2839	def unlock (self, name: str) -> None:
2840
2841		"""Release a ``lock()``: the stream runs free and ``reroll()`` works again."""
2842
2843		self._locked_names.discard(name)
2844
2845	def tuning (
2846		self,
2847		source: typing.Optional[typing.Union[str, "os.PathLike"]] = None,
2848		*,
2849		cents: typing.Optional[typing.List[float]] = None,
2850		ratios: typing.Optional[typing.List[float]] = None,
2851		equal: typing.Optional[int] = None,
2852		bend_range: float = 2.0,
2853		channels: typing.Optional[typing.List[int]] = None,
2854		reference_note: int = 60,
2855		exclude_drums: bool = True,
2856	) -> None:
2857
2858		"""Set a global microtonal tuning for the composition.
2859
2860		The tuning is applied automatically after each pattern rebuild (before
2861		the pattern is scheduled).  Drum patterns (those registered with a
2862		``drum_note_map``) are excluded by default.
2863
2864		Supply exactly one of the source parameters:
2865
2866		- ``source``: path to a Scala ``.scl`` file.
2867		- ``cents``: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).
2868		- ``ratios``: list of frequency ratios (e.g., ``[9/8, 5/4, 4/3, 3/2, 2]``).
2869		- ``equal``: integer for N-tone equal temperament (e.g., ``equal=19``).
2870
2871		For polyphonic parts, supply a ``channels`` pool.  Notes are spread
2872		across those MIDI channels so each can carry an independent pitch bend.
2873		The synth must be configured to match ``bend_range`` (its pitch-bend range
2874		setting in semitones).
2875
2876		Parameters:
2877			source: Path to a ``.scl`` file.
2878			cents: Cent offsets for scale degrees 1..N.
2879			ratios: Frequency ratios for scale degrees 1..N.
2880			equal: Number of equal divisions of the period.
2881			bend_range: Synth pitch-bend range in semitones (default ±2).
2882			channels: Channel pool for polyphonic rotation.
2883			reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).
2884			exclude_drums: When True (default), skip patterns that have a
2885			    ``drum_note_map`` (they use fixed GM pitches, not tuned ones).
2886
2887		Example:
2888			```python
2889			# Quarter-comma meantone from a Scala file
2890			comp.tuning("meanquar.scl")
2891
2892			# Just intonation from ratios
2893			comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2])
2894
2895			# 19-TET, monophonic
2896			comp.tuning(equal=19, bend_range=2.0)
2897
2898			# 31-TET with channel rotation for polyphony (channels 1-6)
2899			comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])
2900			```
2901		"""
2902		import subsequence.tuning as _tuning_mod
2903
2904		given = sum(x is not None for x in [source, cents, ratios, equal])
2905		if given == 0:
2906			raise ValueError("composition.tuning() requires one of: source, cents, ratios, or equal")
2907		if given > 1:
2908			raise ValueError("composition.tuning() accepts only one source parameter")
2909
2910		if source is not None:
2911			t = _tuning_mod.Tuning.from_scl(source)
2912		elif cents is not None:
2913			t = _tuning_mod.Tuning.from_cents(cents)
2914		elif ratios is not None:
2915			t = _tuning_mod.Tuning.from_ratios(ratios)
2916		else:
2917			t = _tuning_mod.Tuning.equal(equal)  # type: ignore[arg-type]
2918
2919		self._tuning = t
2920		self._tuning_bend_range = bend_range
2921		self._tuning_channels = channels
2922		self._tuning_reference_note = reference_note
2923		self._tuning_exclude_drums = exclude_drums
2924
2925	def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
2926
2927		"""
2928		Enable or disable the live terminal dashboard.
2929
2930		When enabled, Subsequence uses a safe logging handler that allows a
2931		persistent status line (BPM, Key, Bar, Section, Chord) to stay at
2932		the bottom of the terminal while logs scroll above it.
2933
2934		Parameters:
2935			enabled: Whether to show the display (default True).
2936			grid: When True, render an ASCII grid visualisation of all
2937				running patterns above the status line. The grid updates
2938				once per bar, showing which steps have notes and at what
2939				velocity.
2940			grid_scale: Horizontal zoom factor for the grid (default
2941				``1.0``).  Higher values add visual columns between
2942				grid steps, revealing micro-timing from swing and groove.
2943				Snapped to the nearest integer internally for uniform
2944				marker spacing.
2945		"""
2946
2947		if enabled:
2948			self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale)
2949		else:
2950			self._display = None
2951
2952	def web_ui (self, http_host: str = "127.0.0.1", ws_host: str = "127.0.0.1") -> None:
2953
2954		"""
2955		Enable the realtime Web UI Dashboard.
2956
2957		When enabled, Subsequence instantiates a WebSocket server that broadcasts
2958		the current state, signals, and active patterns (with high-res timing and
2959		note data) to any connected browser clients.
2960
2961		Both servers bind to localhost by default.  Pass ``http_host`` / ``ws_host``
2962		(e.g. "0.0.0.0") to opt into LAN exposure — the dashboard is read-only but
2963		broadcasts full composition state, so only do so on a trusted network.
2964		"""
2965
2966		self._web_ui_enabled = True
2967		self._web_ui_http_host = http_host
2968		self._web_ui_ws_host = ws_host
2969
2970	def midi_input (self, device: str, clock_follow: bool = False, name: typing.Optional[str] = None) -> None:
2971
2972		"""
2973		Configure a MIDI input device for external sync and MIDI messages.
2974
2975		May be called multiple times to register additional input devices.
2976		The first call sets the primary input (device 0).  Subsequent calls
2977		add additional input devices (device 1, 2, …).  Only one device may
2978		have ``clock_follow=True``.
2979
2980		Parameters:
2981			device: Which MIDI input port to use, matched against
2982				``mido.get_input_names()``.  Treated as a pattern — ``*``
2983				and ``?`` are wildcards, matching is case-insensitive, and a
2984				name without wildcards is a substring.  See
2985				``Composition.__init__`` for why a pattern like
2986				``"*Launchpad *:0"`` survives an ALSA client id changing
2987				between runs.  Because a wrong input would desynchronise or
2988				mis-record a performance, a pattern matching nothing raises,
2989				and one matching several asks which you meant rather than
2990				guessing.
2991			clock_follow: If True, Subsequence will slave its clock to incoming
2992				MIDI Ticks. It will also follow MIDI Start/Stop/Continue
2993				commands. Only one device can have this enabled at a time.
2994			name: Optional alias for use with ``cc_map(input_device=…)`` and
2995				``cc_forward(input_device=…)``.  When omitted, the raw device
2996				name is used.
2997
2998		Example:
2999			```python
3000			# Single controller (unchanged usage)
3001			comp.midi_input("Scarlett 2i4", clock_follow=True)
3002
3003			# Multiple controllers
3004			comp.midi_input("Arturia KeyStep", name="keys")
3005			comp.midi_input("Faderfox EC4", name="faders")
3006			```
3007		"""
3008
3009		if clock_follow:
3010			if self.is_clock_following:
3011				raise ValueError("Only one input device can be configured to follow external clock (clock_follow=True)")
3012
3013		if self._input_device is None:
3014			# First call: set primary input device (device 0)
3015			self._input_device = device
3016			self._input_device_alias = name
3017			self._clock_follow = clock_follow
3018		else:
3019			# Subsequent calls: register additional input devices
3020			self._additional_inputs.append((device, name, clock_follow))
3021
3022	def midi_output (self, device: str, name: typing.Optional[str] = None, latency_ms: float = 0.0) -> int:
3023
3024		"""
3025		Register an additional MIDI output device.
3026
3027		The first output device is always the one passed to
3028		``Composition(output_device=…)`` — that is device 0.
3029		Each call to ``midi_output()`` adds the next device (1, 2, …).
3030
3031		Parameters:
3032			device: Which MIDI output port to add, matched against
3033				``mido.get_output_names()``.  Treated as a pattern —
3034				``*`` and ``?`` are wildcards, matching is
3035				case-insensitive, and a name without wildcards is a
3036				substring.  See ``Composition.__init__`` for the lookup
3037				snippet and why a pattern like ``"*U6MIDI Pro *:0"``
3038				survives an ALSA client id changing between runs.
3039			name: Optional alias for use with ``pattern(device=…)``,
3040				``cc_forward(output_device=…)``, etc.  When omitted, the raw
3041				device name is used.
3042			latency_ms: Physical output latency of this device in
3043				milliseconds, for delay compensation (default 0.0, must be
3044				non-negative). Set this when the device sounds late (e.g. a
3045				software sampler) so Subsequence delays faster devices to
3046				line everything up.
3047
3048		Returns:
3049			The integer device index assigned (1, 2, 3, …).
3050
3051		Example:
3052			```python
3053			comp = subsequence.Composition(bpm=120, output_device="MOTU Express")
3054
3055			# Returns 1 — use as device=1 or device="integra"
3056			comp.midi_output("Roland Integra", name="integra")
3057
3058			# A software sampler that sounds 20ms late
3059			comp.midi_output("Subsample", name="sampler", latency_ms=20)
3060
3061			@comp.pattern(channel=1, beats=4, device="integra")
3062			def strings (p):
3063				p.note(60, beat=0)
3064			```
3065		"""
3066
3067		if latency_ms < 0:
3068			raise ValueError(f"latency_ms must be non-negative — got {latency_ms}")
3069
3070		idx = 1 + len(self._additional_outputs)  # device 0 is always the primary
3071		self._additional_outputs.append(_AdditionalOutput(device=device, alias=name, latency_ms=latency_ms))
3072		return idx
3073
3074	def _warn_if_high_latency (self) -> None:
3075
3076		"""Warn if delay compensation adds a large whole-rig latency.
3077
3078		The slowest device defines the alignment point — every faster device is
3079		delayed up to that amount — so a large maximum means the whole rig
3080		responds late to live input.  Emitted once at startup.
3081		"""
3082
3083		candidates: typing.List[typing.Tuple[str, float]] = [("primary output", self._output_latency_ms)]
3084		candidates += [(out.alias or out.device, out.latency_ms) for out in self._additional_outputs]
3085
3086		slow_name, max_ms = max(candidates, key=lambda c: c[1])
3087
3088		if max_ms > _LATENCY_WARN_THRESHOLD_MS:
3089			logger.warning(
3090				"Device latency compensation: '%s' is the slowest at %.0fms, so faster "
3091				"devices are delayed up to %.0fms to stay aligned — live-input feel may suffer.",
3092				slow_name, max_ms, max_ms,
3093			)
3094
3095	def clock_output (self, enabled: bool = True) -> None:
3096
3097		"""
3098		Send MIDI timing clock to connected hardware.
3099
3100		When enabled, Subsequence acts as a MIDI clock master and sends
3101		standard clock messages on the output port: a Start message (0xFA)
3102		when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN),
3103		and a Stop message (0xFC) when playback ends.
3104
3105		This allows hardware synthesizers, drum machines, and effect units to
3106		slave their tempo to Subsequence automatically.
3107
3108		**Note:** Clock output is automatically disabled when ``midi_input()``
3109		is called with ``clock_follow=True``, to prevent a clock feedback loop.
3110
3111		Parameters:
3112			enabled: Whether to send MIDI clock (default True).
3113
3114		Example:
3115			```python
3116			comp = subsequence.Composition(bpm=120, output_device="...")
3117			comp.clock_output()   # hardware will follow Subsequence tempo
3118			```
3119		"""
3120
3121		self._clock_output = enabled
3122
3123
3124	def link (self, quantum: float = 4.0) -> "Composition":
3125
3126		"""
3127		Enable Ableton Link tempo and phase synchronisation.
3128
3129		When enabled, Subsequence joins the local Link session and slaves its
3130		clock to the shared network tempo and beat phase.  All other Link-enabled
3131		apps on the same LAN — Ableton Live, iOS synths, other Subsequence
3132		instances — will automatically stay in time.
3133
3134		Playback starts on the next bar boundary aligned to the Link quantum,
3135		so downbeats stay in sync across all participants.
3136
3137		Requires the ``link`` optional extra::
3138
3139		    pip install subsequence[link]
3140
3141		Parameters:
3142			quantum: Beat cycle length.  ``4.0`` (default) = one bar in 4/4 time.
3143			         Change this if your composition uses a different meter.
3144
3145		Example::
3146
3147		    comp = subsequence.Composition(bpm=120, key="C")
3148		    comp.link()          # join the Link session
3149		    comp.play()
3150
3151		    # On another machine / instance:
3152		    comp2 = subsequence.Composition(bpm=120)
3153		    comp2.link()         # tempo and phase will lock to comp
3154		    comp2.play()
3155
3156		Note:
3157		    ``set_bpm()`` proposes the new tempo to the Link network when Link
3158		    is active.  The network-authoritative tempo is applied on the next
3159		    pulse, so there may be a brief lag before the change is visible.
3160		"""
3161
3162		# Eagerly check that aalink is installed — fail early with a clear message.
3163		subsequence.link_clock._require_aalink()
3164
3165		self._link_quantum = quantum
3166		return self
3167
3168
3169	def cc_map (
3170		self,
3171		cc: int,
3172		data_key: str,
3173		channel: typing.Optional[int] = None,
3174		min_val: float = 0.0,
3175		max_val: float = 1.0,
3176		input_device: subsequence.midi_utils.DeviceId = None,
3177	) -> None:
3178
3179		"""
3180		Map an incoming MIDI CC to a ``composition.data`` key.
3181
3182		When the composition receives a CC message on the configured MIDI
3183		input port, the value is scaled from the CC range (0–127) to
3184		*[min_val, max_val]* and stored in ``composition.data[data_key]``.
3185
3186		This lets hardware knobs, faders, and expression pedals control live
3187		parameters without writing any callback code.
3188
3189		**Requires** ``midi_input()`` to be called first to open an input port.
3190
3191		Parameters:
3192			cc: MIDI Control Change number (0–127).
3193			data_key: The ``composition.data`` key to write.
3194			channel: If given, only respond to CC messages on this channel.
3195				Uses the same numbering convention as ``pattern()`` (1-16
3196				by default, or 0-15 with ``zero_indexed_channels=True``).
3197				``None`` matches any channel (default).
3198			min_val: Scaled minimum — written when CC value is 0 (default 0.0).
3199			max_val: Scaled maximum — written when CC value is 127 (default 1.0).
3200			input_device: Only respond to CC messages from this input device
3201				(index or name).  ``None`` responds to any input device (default).
3202
3203		Example:
3204			```python
3205			comp.midi_input("Arturia KeyStep")
3206			comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
3207			comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader
3208
3209			# Multi-device: only listen to CC 74 from the "faders" controller
3210			comp.cc_map(74, "filter", input_device="faders")
3211			```
3212		"""
3213
3214		resolved_channel = self._resolve_channel(channel) if channel is not None else None
3215
3216		self._cc_mappings.append({
3217			'cc': cc,
3218			'data_key': data_key,
3219			'channel': resolved_channel,
3220			'min_val': min_val,
3221			'max_val': max_val,
3222			'input_device': input_device,  # resolved to int index in _run()
3223		})
3224
3225
3226	def note_input (
3227		self,
3228		channel: typing.Optional[int] = None,
3229		release_ms: float = 30.0,
3230		latch: bool = False,
3231		input_device: subsequence.midi_utils.DeviceId = None,
3232	) -> None:
3233
3234		"""Track notes held on a MIDI keyboard for live arpeggiation.
3235
3236		Incoming note-on/note-off messages build a live "currently held" set
3237		that any pattern reads via ``p.held_notes()`` — typically fed straight
3238		to ``p.arpeggio()``.  The composition still authors the rhythm and
3239		motion; the player's hands supply the pitch set.  This is a live
3240		*performance* layer over the deterministic, seeded composition: when
3241		rendering headlessly there is no input, so ``p.held_notes()`` is empty
3242		and seeded output is unchanged.
3243
3244		**Requires** ``midi_input()`` to be called first to open an input port.
3245
3246		Parameters:
3247			channel: If given, only track notes on this channel.  Uses the same
3248				numbering convention as ``pattern()`` (1-16 by default, or 0-15
3249				with ``zero_indexed_channels=True``).  ``None`` tracks any
3250				channel (default).
3251			release_ms: How long (milliseconds) a released note keeps counting
3252				as held.  This smooths the momentary all-keys-up gap during a
3253				hand-position change so the arp does not drop to silence.
3254				Default 30.0; set 0.0 to release instantly.  Ignored when
3255				``latch`` is True.
3256			latch: When True, the held set persists after you lift your hands
3257				until you play a new chord (the first key after every key is up
3258				replaces it) — like a hardware arp's latch.
3259			input_device: Only track notes from this input device (index or
3260				name).  ``None`` tracks any input device (default).
3261
3262		Example:
3263			```python
3264			comp.midi_input("Arturia KeyStep")
3265			comp.note_input(channel=1, release_ms=30)
3266
3267			@comp.pattern(channel=6, beats=4)
3268			def arp (p):
3269			    p.arpeggio(p.held_notes(), direction="up")  # rests when silent
3270			```
3271		"""
3272
3273		if self._note_input is not None:
3274			raise RuntimeError("only one note_input source is supported — named multi-source is not yet available")
3275
3276		resolved_channel = self._resolve_channel(channel) if channel is not None else None
3277
3278		self._note_input = {
3279			'channel': resolved_channel,
3280			'release_ms': release_ms,
3281			'latch': latch,
3282			'input_device': input_device,  # resolved to int index in _run()
3283		}
3284
3285
3286	@staticmethod
3287	def _make_cc_forward_transform (
3288		output: typing.Union[str, typing.Callable],
3289		cc: int,
3290		output_channel: typing.Optional[int],
3291	) -> typing.Callable:
3292
3293		"""Build a transform callable from a preset string or user-supplied callable.
3294
3295		The returned callable has signature ``(value: int, channel: int) -> Optional[mido.Message]``
3296		where ``channel`` is the 0-indexed incoming channel.
3297		"""
3298
3299		import mido as _mido
3300
3301		def _out_ch (incoming: int) -> int:
3302			return output_channel if output_channel is not None else incoming
3303
3304		if callable(output):
3305			if output_channel is None:
3306				return output
3307			def _wrapped (value: int, channel: int) -> typing.Optional[typing.Any]:
3308				msg = output(value, channel)
3309
3310				if msg is None:
3311					return None
3312
3313				# copy() re-channels without rebuilding: reconstructing from
3314				# __dict__ passed 'type' twice and raised TypeError on every
3315				# message, so callable+output_channel never forwarded anything.
3316				return msg.copy(channel=output_channel)
3317			return _wrapped
3318
3319		if output == 'cc':
3320			def _cc_identity (value: int, channel: int) -> typing.Any:
3321				return _mido.Message('control_change', channel=_out_ch(channel), control=cc, value=value)
3322			return _cc_identity
3323
3324		if output.startswith('cc:'):
3325			try:
3326				target_cc = int(output[3:])
3327			except ValueError:
3328				raise ValueError(f"cc_forward(): invalid preset '{output}' — expected 'cc:N' where N is 0–127")
3329			if not 0 <= target_cc <= 127:
3330				raise ValueError(f"cc_forward(): CC number {target_cc} out of range 0–127")
3331			def _cc_remap (value: int, channel: int) -> typing.Any:
3332				return _mido.Message('control_change', channel=_out_ch(channel), control=target_cc, value=value)
3333			return _cc_remap
3334
3335		if output == 'pitchwheel':
3336			def _pitchwheel (value: int, channel: int) -> typing.Any:
3337				pitch = int(value / 127 * 16383) - 8192
3338				return _mido.Message('pitchwheel', channel=_out_ch(channel), pitch=pitch)
3339			return _pitchwheel
3340
3341		raise ValueError(
3342			f"cc_forward(): unknown preset '{output}'. "
3343			"Use 'cc', 'cc:N' (e.g. 'cc:74'), 'pitchwheel', or a callable."
3344		)
3345
3346
3347	def cc_forward (
3348		self,
3349		cc: int,
3350		output: typing.Union[str, typing.Callable],
3351		*,
3352		channel: typing.Optional[int] = None,
3353		output_channel: typing.Optional[int] = None,
3354		mode: str = "instant",
3355		input_device: subsequence.midi_utils.DeviceId = None,
3356		output_device: subsequence.midi_utils.DeviceId = None,
3357	) -> None:
3358
3359		"""
3360		Forward an incoming MIDI CC to the MIDI output in real-time.
3361
3362		Unlike ``cc_map()`` which writes incoming CC values to ``composition.data``
3363		for use at pattern rebuild time, ``cc_forward()`` routes the signal
3364		directly to the MIDI output — bypassing the pattern cycle entirely.
3365
3366		Both ``cc_map()`` and ``cc_forward()`` may be registered for the same CC
3367		number; they operate independently.
3368
3369		Parameters:
3370			cc: Incoming CC number to listen for (0–127).
3371			output: What to send. Either a **preset string**:
3372
3373				- ``"cc"`` — identity forward, same CC number and value.
3374				- ``"cc:N"`` — forward as CC number N (e.g. ``"cc:74"``).
3375				- ``"pitchwheel"`` — scale 0–127 to -8192..8191 and send as pitch bend.
3376
3377				Or a **callable** with signature
3378				``(value: int, channel: int) -> Optional[mido.Message]``.
3379				Return a fully formed ``mido.Message`` to send, or ``None`` to suppress.
3380				``channel`` is 0-indexed (the incoming channel).
3381			channel: If given, only respond to CC messages on this channel.
3382				Uses the same numbering convention as ``cc_map()``.
3383				``None`` matches any channel (default).
3384			output_channel: Override the output channel. ``None`` uses the
3385				incoming channel. Uses the same numbering convention as ``pattern()``.
3386			input_device: Only respond to CC from this input device — an index,
3387				a registered name, or ``None`` for any input (default), the
3388				same convention as ``cc_map()``.
3389			output_device: Send to this output device — an index, a registered
3390				name, or ``None`` for the primary output (default).
3391			mode: Dispatch mode:
3392
3393				- ``"instant"`` *(default)* — send immediately on the MIDI input
3394				  callback thread. Lowest latency (~1–5 ms). Instant forwards are
3395				  **not** recorded when recording is enabled.
3396				- ``"queued"`` — inject into the sequencer event queue and send at
3397				  the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards
3398				  **are** recorded when recording is enabled.
3399
3400		Example:
3401			```python
3402			comp.midi_input("Arturia KeyStep")
3403
3404			# CC 1 → CC 1 (identity, instant)
3405			comp.cc_forward(1, "cc")
3406
3407			# CC 1 → pitch bend on channel 1, queued (recordable)
3408			comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued")
3409
3410			# CC 1 → CC 74, custom channel
3411			comp.cc_forward(1, "cc:74", output_channel=2)
3412
3413			# Custom transform — remap CC range 0–127 to CC 74 range 40–100
3414			import subsequence.midi as midi
3415			comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch))
3416
3417			# Forward AND map to data simultaneously — both active on the same CC
3418			comp.cc_map(1, "mod_wheel")
3419			comp.cc_forward(1, "cc:74")
3420			```
3421		"""
3422
3423		if not 0 <= cc <= 127:
3424			raise ValueError(f"cc_forward(): cc {cc} out of range 0–127")
3425
3426		if mode not in ('instant', 'queued'):
3427			raise ValueError(f"cc_forward(): mode must be 'instant' or 'queued', got '{mode}'")
3428
3429		resolved_in_channel = self._resolve_channel(channel) if channel is not None else None
3430		resolved_out_channel = self._resolve_channel(output_channel) if output_channel is not None else None
3431
3432		transform = self._make_cc_forward_transform(output, cc, resolved_out_channel)
3433
3434		self._cc_forwards.append({
3435			'cc': cc,
3436			'channel': resolved_in_channel,
3437			'output_channel': resolved_out_channel,
3438			'mode': mode,
3439			'transform': transform,
3440			'input_device': input_device,   # resolved to int index in _run()
3441			'output_device': output_device, # resolved to int index in _run()
3442		})
3443
3444
3445	def live (self, port: int = 5555) -> None:
3446
3447		"""
3448		Enable the live coding eval server.
3449
3450		This allows you to connect to a running composition using the
3451		``subsequence.live_client`` REPL and hot-swap pattern code or
3452		modify variables in real-time.
3453
3454		Security:
3455			The server executes arbitrary Python in this process — it is **not** a
3456			sandbox.  It binds to localhost only and is opt-in, but any process on
3457			the same machine that can reach the port gains full code execution here.
3458			Do not enable it on shared or multi-user hosts, and never expose the
3459			port to a network.
3460
3461		Parameters:
3462			port: The TCP port to listen on (default 5555).
3463		"""
3464
3465		self._live_server = subsequence.live_server.LiveServer(self, port=port)
3466		self._is_live = True
3467
3468	def watch (self, path: typing.Union[str, pathlib.Path], poll_interval: float = 0.25) -> None:
3469
3470		"""Watch a Python file and reload it into the composition on every save.
3471
3472		The watched file is exec'd into a namespace with ``composition`` and
3473		``subsequence`` available.  ``@composition.pattern`` decorators inside
3474		the file hot-swap their corresponding running patterns in place;
3475		patterns whose function bodies have been deleted from the file are
3476		unregistered automatically on the next reload (notes stopped,
3477		removed from the running-pattern set).
3478
3479		An **initial synchronous load** happens here — if the file has a
3480		``SyntaxError`` or doesn't exist at this moment, the exception
3481		propagates so the user knows immediately.  Subsequent reloads
3482		happen on the composition's event loop and tolerate transient
3483		errors (logged, skipped).
3484
3485		Call BEFORE ``composition.play()``.  Reloads happen on the
3486		composition's event loop, so all mutations are thread-safe.
3487
3488		See the "Live coding via file watching" section of the README for
3489		the recommended wrapper-script + live-file split.
3490
3491		Parameters:
3492			path: Path to the Python file to watch.
3493			poll_interval: Seconds between ``mtime`` polls (default 0.25 s).
3494
3495		Example::
3496
3497			# live_init.py — runs once
3498			composition = subsequence.Composition(bpm=120, key="E")
3499			composition.harmony(style="aeolian_minor")
3500			composition.watch("live_patterns.py")
3501			composition.play()
3502		"""
3503
3504		# Required for the decorator hot-swap path to fire on re-decoration.
3505		self._is_live = True
3506
3507		# Detect the single-file workflow: if watch() is called from inside
3508		# the very file being watched, the outer Python script execution will
3509		# already register the patterns (the decorators sit at module level
3510		# below ``watch(__file__)``).  In that case, _load_initial's re-exec
3511		# would double-register every pattern, so skip it.  For the two-file
3512		# workflow (path != caller's __file__) the initial exec is essential
3513		# — it's the only way the watched file's patterns ever reach the
3514		# composition.
3515		caller_file = self._caller_module_file()
3516		self_watch = False
3517		if caller_file is not None:
3518			try:
3519				self_watch = pathlib.Path(caller_file).resolve() == pathlib.Path(path).resolve()
3520			except OSError:
3521				self_watch = False
3522
3523		self._live_reloader = subsequence.live_reloader.LiveReloader(
3524			composition = self,
3525			path = path,
3526			poll_interval = poll_interval,
3527			skip_initial_exec = self_watch,
3528		)
3529		self._live_reloader.start()
3530
3531	@staticmethod
3532	def _caller_module_file () -> typing.Optional[str]:
3533
3534		"""Return ``__file__`` of the module that invoked the caller, if available.
3535
3536		Walks one frame up the call stack — the immediate caller is
3537		``watch()``, so ``f_back`` is the user's code.  Returns the
3538		module-level ``__file__`` of that frame's globals; ``None`` when
3539		the caller has no ``__file__`` (REPL, exec'd context, etc.).
3540		"""
3541
3542		frame = inspect.currentframe()
3543		if frame is None or frame.f_back is None or frame.f_back.f_back is None:
3544			return None
3545		# f_back = watch(); f_back.f_back = user code calling watch().
3546		return frame.f_back.f_back.f_globals.get("__file__")
3547
3548	def load_patterns (
3549		self,
3550		source:       str,
3551		source_label: str = "<string>",
3552	) -> None:
3553
3554		"""Compile and apply a pattern-source string to the composition.
3555
3556		Equivalent to one ``watch()`` reload triggered by save, but with the
3557		source presented in-memory rather than on disk.  Useful for web /
3558		REST handlers that accept pattern uploads from a trusted contributor,
3559		or for one-shot session loads with no file backing.
3560
3561		Behaviour mirrors ``watch()``:
3562
3563		* The source is exec'd into a fresh namespace with ``composition``
3564		  and ``subsequence`` in scope.
3565		* ``@composition.pattern`` decorators in the source hot-swap their
3566		  corresponding running patterns in place.
3567		* Patterns currently running but **not** declared in the source are
3568		  unregistered — the source is treated as the full new truth.
3569		* If the composition is already playing, the swap happens on the
3570		  event loop thread; the call blocks until it completes.
3571		* If the composition has not yet called ``play()``, the source runs
3572		  on the caller's thread; decorators populate ``_pending_patterns``
3573		  and ``play()`` picks them up in the usual way.
3574
3575		Errors are raised so the caller can act on them:
3576
3577		* ``SyntaxError`` if ``source`` fails to compile.
3578		* The exception raised inside ``exec()`` for any runtime error.
3579		* ``RuntimeError`` if called from inside the composition's own
3580		  event loop thread (would deadlock — see Threading below).
3581
3582		In either failure case, existing composition state is preserved —
3583		the diff-and-unregister phase is skipped if exec raised, so a
3584		half-broken upload cannot tear down working patterns.
3585
3586		Threading:
3587			Designed to be called from a thread DIFFERENT from the
3588			composition's event loop — typically a web-handler worker.
3589			Cannot be called from inside the loop itself (a pattern
3590			callback, an asyncio task spawned by the composition).  From
3591			there, ``await composition._apply_source_async(...)`` directly.
3592
3593		SECURITY WARNING: ``exec()`` is not sandboxed.  The source has full
3594		Python access in this process.  Only pass source from trusted
3595		senders.  The built-in blocklist (``help``, ``input``, ``breakpoint``,
3596		``exit``, ``quit``) prevents calls that would stall the event loop;
3597		it is not a security boundary.
3598
3599		Parameters:
3600			source:       Python source declaring ``@composition.pattern``
3601				functions.
3602			source_label: Identifier used in compile errors and tracebacks
3603				(appears as the filename in ``SyntaxError`` and ``__file__``-
3604				style traceback lines).  Default ``"<string>"``.
3605		"""
3606
3607		# Required for the decorator hot-swap path to fire on re-decoration.
3608		self._is_live = True
3609
3610		# Compile on the caller's thread so SyntaxError comes back fast,
3611		# before any cross-thread scheduling.
3612		compiled = compile(source, source_label, "exec")
3613		namespace = self._build_live_namespace(source_label = source_label)
3614
3615		loop = self._sequencer._event_loop
3616
3617		if loop is not None and loop.is_running():
3618
3619			# Refuse to deadlock: calling load_patterns() from inside the
3620			# composition's own event loop (e.g. from a pattern callback or
3621			# an asyncio task spawned by the composition) would have us
3622			# block waiting for a coroutine that can only run when this
3623			# thread yields.  Tell the caller exactly what to do instead.
3624			try:
3625				current_loop: typing.Optional[asyncio.AbstractEventLoop] = asyncio.get_running_loop()
3626			except RuntimeError:
3627				current_loop = None
3628
3629			if current_loop is loop:
3630				raise RuntimeError(
3631					"load_patterns() cannot be called from inside the composition's "
3632					"event loop thread — it would deadlock waiting for the "
3633					"scheduled coroutine to run on the very thread that's blocked. "
3634					"From a worker thread, call it normally.  From an async "
3635					"coroutine already on the loop, "
3636					"`await composition._apply_source_async(compile(source, label, 'exec'), "
3637					"composition._build_live_namespace())` instead."
3638				)
3639
3640			# Composition is playing — mutation must happen on the loop thread.
3641			# future.result() blocks the caller until the coroutine finishes
3642			# and re-raises any exception it threw.
3643			future = asyncio.run_coroutine_threadsafe(
3644				self._apply_source_async(compiled, namespace, source_key = source_label),
3645				loop = loop,
3646			)
3647			future.result()
3648
3649		else:
3650			# Pre-play: no event loop yet.  Decorators populate
3651			# _pending_patterns; play() graduates them in the usual way.
3652			# Diff-and-unregister is unnecessary here — nothing is running,
3653			# but RECORD what this source declares so a later post-play
3654			# reload under the same label can tear down its deletions.
3655			self._declared_names = set()
3656			exec(compiled, namespace)
3657			self._source_declared[source_label] = set(self._declared_names)
3658
3659	async def _apply_source_async (
3660		self,
3661		compiled:  types.CodeType,
3662		namespace: typing.Dict[str, typing.Any],
3663		source_key: str = "<live>",
3664	) -> None:
3665
3666		"""Execute pre-compiled live source against the running composition.
3667
3668		Runs on the event loop thread.  Performs ``exec()``, graduates any
3669		newly-decorated patterns into ``_running_patterns``, then unregisters
3670		any patterns that *this source* declared on its previous exec but no
3671		longer declares (keyed by ``source_key`` — a watched file's path or a
3672		``load_patterns`` label).  Patterns registered by the wrapper script
3673		or by another source are never this source's to tear down.
3674
3675		Raises whatever ``exec()`` raises.  When that happens, the diff-and-
3676		unregister phase is skipped — the namespace is incomplete, so any
3677		patterns the source failed to reach would be misinterpreted as
3678		deletions and torn down.
3679
3680		Called from two places:
3681
3682		* ``Composition.load_patterns()`` via ``run_coroutine_threadsafe``.
3683		* ``LiveReloader._reload_async`` directly (already on the loop).
3684		"""
3685
3686		# Track which patterns the source declares this exec.  pattern() and
3687		# layer() add their (resolved) names to _declared_names as they run, so
3688		# this covers decorated patterns AND layer()/merged patterns — the latter
3689		# have no module-level callable to match against by name, which is why the
3690		# old namespace-based diff tore layers down on every reload.
3691		self._declared_names = set()
3692
3693		# Bail before any state mutation if exec raises — propagates to
3694		# the caller (load_patterns re-raises; LiveReloader catches + logs).
3695		exec(compiled, namespace)
3696
3697		# Graduate newly-decorated patterns from _pending_patterns into
3698		# _running_patterns so they start firing on the next reschedule.
3699		# Patterns that hot-swapped via the decorator/layer path don't appear
3700		# in _pending_patterns and don't need this step.
3701		await self._activate_new_pending_patterns()
3702
3703		# Detect deletions: a name THIS source declared last time but not this
3704		# time has been removed by the user and should be torn down.  (An
3705		# unknown source — first exec — tears down nothing: patterns running
3706		# from the wrapper script or another source are not ours to remove.)
3707		# Decorators/layer() do NOT remove from _running_patterns when a
3708		# definition disappears from the source.
3709		previous = self._source_declared.get(source_key)
3710
3711		if previous is not None:
3712			for name in previous - self._declared_names:
3713				if name in self._running_patterns:
3714					self.unregister(name)
3715
3716		self._source_declared[source_key] = set(self._declared_names)
3717
3718	def _build_live_namespace (self, source_label: str = "<live>") -> typing.Dict[str, typing.Any]:
3719
3720		"""Build a fresh namespace dict for exec'ing live source.
3721
3722		Provides ``composition`` (this Composition), ``subsequence`` (the
3723		package), and a safe builtins set with ``help``, ``input``,
3724		``breakpoint``, ``exit``, ``quit`` blocked.
3725
3726		Also injects two dunder globals that make the single-file live-coding
3727		workflow ergonomic:
3728
3729		* ``__name__ = "__live_reload__"`` — so ``if __name__ == "__main__":``
3730		  blocks in the watched file are *skipped* during live reload.  The
3731		  same file run directly with ``python my_session.py`` sees
3732		  ``__name__ == "__main__"`` and runs setup; saves trigger reload
3733		  with ``__name__ == "__live_reload__"``, skipping setup and only
3734		  re-running pattern definitions.
3735		* ``__file__ = source_label`` — so ``composition.watch(__file__)``
3736		  and any user code referencing ``__file__`` works inside the live
3737		  namespace.  Set to the file path for ``LiveReloader``, the
3738		  user-supplied ``source_label`` for ``Composition.load_patterns``,
3739		  and ``"<live>"`` for ``LiveServer``.
3740
3741		Single source of truth: ``live_reloader`` (file watching),
3742		``live_server`` (TCP REPL), and ``load_patterns`` (string source)
3743		all call this so live source sees the same environment from any
3744		entry point.
3745
3746		The blocklist prevents calls that would stall the async event loop
3747		running the sequencer.  It is **not** a security sandbox — exec'd
3748		code can still do anything Python allows.
3749
3750		Parameters:
3751			source_label: Value to bind to ``__file__`` in the namespace.
3752				Defaults to ``"<live>"``.
3753		"""
3754
3755		import subsequence  # local import: this module is imported during subsequence init
3756
3757		safe_builtins = {name: getattr(builtins, name) for name in dir(builtins)}
3758
3759		blocked = {"help", "input", "breakpoint", "exit", "quit"}
3760
3761		for name in blocked:
3762			safe_builtins[name] = _live_blocked(name)
3763
3764		return {
3765			"__builtins__": safe_builtins,
3766			"__name__":     "__live_reload__",
3767			"__file__":     source_label,
3768			"composition":  self,
3769			"subsequence":  subsequence,
3770		}
3771
3772	def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1", receive_host: str = "0.0.0.0") -> None:
3773
3774		"""
3775		Enable bi-directional Open Sound Control (OSC).
3776
3777		Subsequence will listen for commands (like ``/bpm`` or ``/mute``) and
3778		broadcast its internal state (like ``/chord`` or ``/bar``) over UDP.
3779
3780		Parameters:
3781			receive_port: Port to listen for incoming OSC messages (default 9000).
3782			send_port: Port to send state updates to (default 9001).
3783			send_host: The IP address to send updates to (default "127.0.0.1").
3784			receive_host: Interface to listen on (default "0.0.0.0" — all
3785				interfaces, so external OSC controllers on the LAN can reach it).
3786				The listener can change tempo, mute patterns, and write data, so on
3787				an untrusted network restrict it with ``receive_host="127.0.0.1"``.
3788		"""
3789
3790		self._osc_server = subsequence.osc.OscServer(
3791			self,
3792			receive_port = receive_port,
3793			send_port = send_port,
3794			send_host = send_host,
3795			receive_host = receive_host
3796		)
3797
3798	def osc_map (self, address: str, handler: typing.Callable) -> None:
3799
3800		"""
3801		Register a custom OSC handler.
3802
3803		Must be called after :meth:`osc` has been configured.
3804
3805		Parameters:
3806			address: OSC address pattern to match (e.g. ``"/my/param"``).
3807			handler: Callable invoked with ``(address, *args)`` when a
3808				matching message arrives.
3809
3810		Example::
3811
3812			composition.osc()
3813
3814			def on_intensity (address, value):
3815				composition.data["intensity"] = float(value)
3816
3817			composition.osc_map("/intensity", on_intensity)
3818		"""
3819
3820		if self._osc_server is None:
3821			raise RuntimeError("Call composition.osc() before composition.osc_map()")
3822
3823		self._osc_server.map(address, handler)
3824
3825	def set_bpm (self, bpm: float) -> None:
3826
3827		"""
3828		Instantly change the tempo.
3829
3830		Parameters:
3831			bpm: The new tempo in beats per minute.
3832
3833		When Ableton Link is active, this proposes the new tempo to the Link
3834		network instead of applying it locally.  The network-authoritative tempo
3835		is picked up on the next pulse.
3836		"""
3837
3838		self._sequencer.set_bpm(bpm)
3839
3840		if not self.is_clock_following and self._link_quantum is None:
3841			self.bpm = bpm
3842
3843	def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None:
3844
3845		"""
3846		Smoothly ramp the tempo to a target value over a number of bars.
3847
3848		Parameters:
3849			bpm: Target tempo in beats per minute.
3850			bars: Duration of the transition in bars.
3851			shape: Easing curve name.  Defaults to ``"linear"``.
3852			       ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural-
3853			       sounding tempo changes.  See :mod:`subsequence.easing` for all
3854			       available shapes.
3855
3856		Example:
3857			```python
3858			# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
3859			comp.target_bpm(140, bars=8, shape="ease_in_out")
3860			```
3861
3862		Note:
3863			Ignored while Ableton Link is active — the shared session tempo is
3864			authoritative.  Use ``set_bpm()`` to propose a tempo to the Link network.
3865		"""
3866
3867		self._sequencer.set_target_bpm(bpm, bars, shape)
3868
3869	def live_info (self) -> typing.Dict[str, typing.Any]:
3870
3871		"""
3872		Return a dictionary containing the current state of the composition.
3873		
3874		Includes BPM, key, current bar, active section, current chord, 
3875		running patterns, and custom data.
3876		"""
3877
3878		section_info = None
3879		if self._form_state is not None:
3880			section = self._form_state.get_section_info()
3881			if section is not None:
3882				section_info = {
3883					"name": section.name,
3884					"bar": section.bar,
3885					"bars": section.bars,
3886					"progress": section.progress
3887				}
3888
3889		chord_name = None
3890		sounding_chord = self.current_chord()
3891		if sounding_chord is not None:
3892			chord_name = sounding_chord.name()
3893
3894		pattern_list = []
3895		channel_offset = 0 if self._zero_indexed_channels else 1
3896		for name, pat in self._running_patterns.items():
3897			pattern_list.append({
3898				"name": name,
3899				"channel": pat.channel + channel_offset,
3900				"length": pat.length,
3901				"cycle": pat._cycle_count,
3902				"muted": pat._muted,
3903				"tweaks": dict(pat._tweaks)
3904			})
3905
3906		return {
3907			"bpm": self._sequencer.current_bpm,
3908			"key": self.key,
3909			"bar": self._builder_bar,
3910			"section": section_info,
3911			"chord": chord_name,
3912			"patterns": pattern_list,
3913			"input_device": self._input_device,
3914			"clock_follow": self.is_clock_following,
3915			"data": self.data
3916		}
3917
3918	def mute (self, name: str) -> None:
3919
3920		"""
3921		Mute a running pattern by name.
3922		
3923		The pattern continues to 'run' and increment its cycle count in 
3924		the background, but it will not produce any MIDI notes until unmuted.
3925
3926		Parameters:
3927			name: The function name of the pattern to mute.
3928		"""
3929
3930		if name not in self._running_patterns:
3931			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3932
3933		# The performer takes ownership: if a transition's approach window had
3934		# muted this pattern, drop it from that set so the section boundary
3935		# does not silently unmute it ("performer mutes win").
3936		self._transition_muted.discard(name)
3937
3938		self._running_patterns[name]._muted = True
3939		logger.info(f"Muted pattern: {name}")
3940
3941	def unmute (self, name: str) -> None:
3942
3943		"""
3944		Unmute a previously muted pattern.
3945		"""
3946
3947		if name not in self._running_patterns:
3948			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3949
3950		# Symmetric ownership claim: an explicit unmute means the transition
3951		# machinery should no longer manage this pattern at the boundary.
3952		self._transition_muted.discard(name)
3953
3954		self._running_patterns[name]._muted = False
3955		logger.info(f"Unmuted pattern: {name}")
3956
3957	def unregister (self, name: str) -> None:
3958
3959		"""Fully remove a running pattern from rotation.
3960
3961		Unlike ``mute()`` (which keeps the pattern alive but silent),
3962		``unregister()`` tears the pattern down entirely.  It sets
3963		``pattern._removed = True`` so the sequencer's reschedule loop
3964		skips re-adding it on the next pulse; sends ``note_off`` for any
3965		of the pattern's currently-sounding notes on the primary
3966		destination AND on every mirror destination (so drones and
3967		sustaining notes stop immediately); and removes the entry from
3968		``_running_patterns`` so it no longer appears in ``live_info()``,
3969		the terminal grid, or any other consumer that enumerates running
3970		patterns.
3971
3972		Already-queued events in the sequencer's event queue play out —
3973		note_offs are paired with their note_ons at queue time, so notes
3974		end at their natural duration; only drones rely on the targeted
3975		``_stop_pattern_notes`` pass.
3976
3977		Idempotent: silently logs a ``debug`` and returns if the pattern
3978		is already absent.  Useful from both the live REPL
3979		(``composition.live()``) and the file watcher
3980		(``composition.watch()``), which calls this for any pattern
3981		removed from the watched file between reloads.
3982
3983		Parameters:
3984			name: Function name of the pattern to remove.
3985		"""
3986
3987		if name not in self._running_patterns:
3988			logger.debug(f"unregister() no-op: pattern '{name}' not running")
3989			return
3990
3991		pattern = self._running_patterns[name]
3992
3993		# Mark for removal first so the reschedule loop sees the flag even if
3994		# it fires concurrently with the note-off pass below.
3995		pattern._removed = True
3996
3997		# Stop sustaining notes (including drones) on every destination this
3998		# pattern outputs to.  Fire-and-forget across threads via the event
3999		# loop; ``_stop_pattern_notes`` acquires the queue lock internally.
4000		if self._sequencer._event_loop is not None:
4001			asyncio.run_coroutine_threadsafe(
4002				self._sequencer._stop_pattern_notes(pattern),
4003				loop = self._sequencer._event_loop,
4004			)
4005
4006		def _finalise_removal () -> None:
4007			self._running_patterns.pop(name, None)
4008
4009			# Forget any pending (not-yet-graduated) declaration too, so a
4010			# later live reload cannot resurrect the pattern.
4011			self._pending_patterns = [
4012				pending for pending in self._pending_patterns
4013				if pending.builder_fn.__name__ != name
4014			]
4015
4016			logger.info(f"Unregistered pattern: {name}")
4017
4018		# The running-patterns dict is iterated by the display, web UI, and
4019		# reschedule loop on the event loop thread — mutate it there when this
4020		# call arrives from another thread (e.g. the live TCP server).
4021		loop = self._sequencer._event_loop
4022
4023		try:
4024			on_loop = loop is not None and asyncio.get_running_loop() is loop
4025		except RuntimeError:
4026			on_loop = False
4027
4028		if loop is not None and loop.is_running() and not on_loop:
4029			loop.call_soon_threadsafe(_finalise_removal)
4030		else:
4031			_finalise_removal()
4032
4033	def mirror (self, name: str, device: int, channel: int, drum_note_map: typing.Optional[typing.Dict[str, int]] = None) -> None:
4034
4035		"""
4036		Add a mirror destination to a running pattern.
4037
4038		Every note, CC, pitch bend, NRPN/RPN, program change, SysEx, and drone
4039		event the pattern emits will also be sent to ``(device, channel)``,
4040		starting from the next cycle rebuild.  Idempotent on ``(device, channel)``
4041		— calling with the same destination twice does not double-fan; calling
4042		again with a different ``drum_note_map`` re-points it in place.
4043
4044		Parameters:
4045			name: Function name of the pattern to mirror.
4046			device: Output device index (the integer returned from
4047				``midi_output()``; 0 = primary device).
4048			channel: MIDI channel using this composition's numbering convention
4049				(1-16 by default; 0-15 if ``zero_indexed_channels=True``).
4050			drum_note_map: Optional per-destination drum map.  When set, mirrored
4051				drum hits are re-resolved by name through it, so a named voice
4052				lands on this device's own note number — see the README
4053				"MIDI mirroring" section.
4054
4055		Bandwidth: each mirror adds another full copy of the pattern's events.
4056		See the README "MIDI mirroring" section for the tradeoffs.
4057		"""
4058
4059		if name not in self._running_patterns:
4060			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4061
4062		resolved_channel = self._resolve_channel(channel)
4063		prefix = (device, resolved_channel)
4064		entry: subsequence.pattern.MirrorSpec = prefix if drum_note_map is None else (device, resolved_channel, drum_note_map)
4065
4066		pattern = self._running_patterns[name]
4067
4068		# Mirror-to-self check: comparing the (device, channel) prefix against the
4069		# live pattern's resolved destination.  Unlike the decorator path this is
4070		# always concrete.
4071		if prefix == (pattern.device, pattern.channel):
4072			logger.warning(
4073				f"Mirror destination {prefix} matches '{name}'s primary destination "
4074				f"— every event will double-fire on this (device, channel).  This is almost "
4075				f"certainly unintended."
4076			)
4077
4078		# Idempotent on (device, channel): replace any existing entry for the same
4079		# destination (so its map can be re-pointed), else append.
4080		existing_index = next((idx for idx, e in enumerate(pattern.mirrors) if (e[0], e[1]) == prefix), None)
4081		if existing_index is None:
4082			pattern.mirrors.append(entry)
4083			logger.info(f"Mirror added: {name} -> device={device}, channel={resolved_channel}")
4084		elif pattern.mirrors[existing_index] != entry:
4085			pattern.mirrors[existing_index] = entry
4086			logger.info(f"Mirror updated: {name} -> device={device}, channel={resolved_channel}")
4087		else:
4088			logger.debug(f"Mirror already present on {name}: device={device}, channel={resolved_channel}")
4089
4090	def unmirror (self, name: str, device: int, channel: int) -> None:
4091
4092		"""
4093		Remove a single mirror destination from a running pattern.
4094
4095		Matches on ``(device, channel)`` only — any attached ``drum_note_map`` is
4096		ignored.  Idempotent: silently does nothing if the destination is not
4097		currently mirrored.  The change applies on the next cycle rebuild.
4098		"""
4099
4100		if name not in self._running_patterns:
4101			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4102
4103		resolved_channel = self._resolve_channel(channel)
4104		prefix = (device, resolved_channel)
4105
4106		pattern = self._running_patterns[name]
4107
4108		filtered = [e for e in pattern.mirrors if (e[0], e[1]) != prefix]
4109		if len(filtered) != len(pattern.mirrors):
4110			pattern.mirrors[:] = filtered
4111			logger.info(f"Mirror removed: {name} -> device={device}, channel={resolved_channel}")
4112		else:
4113			logger.debug(f"unmirror() no-op on {name}: device={device}, channel={resolved_channel} not in mirrors")
4114
4115	def unmirror_all (self, name: str) -> None:
4116
4117		"""
4118		Remove every mirror destination from a running pattern.
4119		"""
4120
4121		if name not in self._running_patterns:
4122			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4123
4124		pattern = self._running_patterns[name]
4125
4126		if pattern.mirrors:
4127			pattern.mirrors.clear()
4128			logger.info(f"All mirrors cleared on pattern: {name}")
4129
4130	def tweak (self, name: str, **kwargs: typing.Any) -> None:
4131
4132		"""Override parameters for a running pattern.
4133
4134		Values set here are available inside the pattern's builder
4135		function via ``p.param()``.  They persist across rebuilds
4136		until explicitly changed or cleared.  Changes take effect
4137		on the next rebuild cycle.
4138
4139		Parameters:
4140			name: The function name of the pattern.
4141			``**kwargs``: Parameter names and their new values.
4142
4143		Example (from the live REPL)::
4144
4145			composition.tweak("bass", pitches=[48, 52, 55, 60])
4146		"""
4147
4148		if name not in self._running_patterns:
4149			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4150
4151		self._running_patterns[name]._tweaks.update(kwargs)
4152		logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}")
4153
4154	def clear_tweak (self, name: str, *param_names: str) -> None:
4155
4156		"""Remove tweaked parameters from a running pattern.
4157
4158		If no parameter names are given, all tweaks for the pattern
4159		are cleared and every ``p.param()`` call reverts to its
4160		default.
4161
4162		Parameters:
4163			name: The function name of the pattern.
4164			*param_names: Specific parameter names to clear.  If
4165				omitted, all tweaks are removed.
4166		"""
4167
4168		if name not in self._running_patterns:
4169			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4170
4171		if not param_names:
4172			self._running_patterns[name]._tweaks.clear()
4173			logger.info(f"Cleared all tweaks for pattern '{name}'")
4174		else:
4175			for param_name in param_names:
4176				self._running_patterns[name]._tweaks.pop(param_name, None)
4177			logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}")
4178
4179	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
4180
4181		"""Return a copy of the current tweaks for a running pattern.
4182
4183		Parameters:
4184			name: The function name of the pattern.
4185		"""
4186
4187		if name not in self._running_patterns:
4188			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4189
4190		return dict(self._running_patterns[name]._tweaks)
4191
4192	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
4193
4194		"""
4195		Register a custom function to run on a repeating beat-based cycle.
4196
4197		Subsequence automatically runs synchronous functions in a thread pool
4198		so they don't block the timing-critical MIDI clock. Async functions
4199		are run directly on the event loop.
4200
4201		Parameters:
4202			fn: The function to call.
4203			cycle_beats: How often to call it (e.g., 4 = every bar).
4204			reschedule_lookahead: How far in advance to schedule the next call.
4205			wait_for_initial: If True, run the function once during startup
4206				and wait for it to complete before playback begins. This
4207				ensures ``composition.data`` is populated before patterns
4208				first build. Implies ``defer=True`` for the repeating
4209				schedule.
4210			defer: If True, skip the pulse-0 fire and defer the first
4211				repeating call to just before the second cycle boundary.
4212
4213		Raises:
4214			RuntimeError: If called after ``play()`` has started — scheduled
4215				tasks register at startup, so a late registration would be
4216				silently ignored otherwise.
4217		"""
4218
4219		if self._sequencer.running:
4220			raise RuntimeError("schedule() must be called before play() - scheduled tasks register at startup")
4221
4222		self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer))
4223
4224	def form (
4225		self,
4226		sections: typing.Union[
4227			"subsequence.forms.Form",
4228			typing.List[typing.Any],
4229			typing.Iterator[typing.Tuple[str, int]],
4230			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
4231		],
4232		loop: bool = False,
4233		start: typing.Optional[str] = None,
4234		at_end: str = "stop",
4235		key: typing.Optional[str] = None,
4236		scale: typing.Optional[str] = None,
4237	) -> None:
4238
4239		"""
4240		Define the structure (sections) of the composition.
4241
4242		You can define form in four ways:
4243
4244		1. **Form value**: a frozen :class:`~subsequence.forms.Form` of
4245		   :class:`~subsequence.forms.Section` values — the payload home
4246		   (energy, key per section); editable, navigable.
4247		2. **Sequence (List)**: a fixed order of ``(name, bars)`` tuples
4248		   or Sections (lists coerce — they are the same form).
4249		3. **Graph (Dict)**: dynamic transitions based on weights.
4250		4. **Generator**: a Python generator that yields ``(name, bars)`` pairs.
4251
4252		Form-value and list forms are **navigable**: ``form_jump()`` and
4253		``form_next()`` work on them (the jump lands on the next occurrence
4254		of the name, wrapping).
4255
4256		Re-binding ``form()`` during playback takes effect at the next bar —
4257		the clock reads the current form state on every bar, so the new form
4258		advances from there (its first section plays from its first bar).
4259
4260		Parameters:
4261			sections: The form definition (Form, List, Dict, or Generator).
4262			loop: Sugar for ``at_end="loop"``.
4263			start: The section to start with (Graph mode only).
4264			at_end: What happens when a sequence form runs out —
4265				``"stop"`` (the form finishes and patterns see no section;
4266				default), ``"hold"`` (the final section repeats until
4267				navigated away from), or ``"loop"`` (start over).  Graphs
4268				end via their terminal sections instead.
4269			key: A form-level key — the **form tier** of the key-source
4270				chain (``Section.key`` overrides it; it overrides the
4271				composition key).  Re-anchors key-relative content for the
4272				whole form.  When *sections* is a ``Form`` value carrying its
4273				own ``key``, that value is used unless this argument overrides.
4274			scale: A form-level scale/mode, paired with ``key``.
4275
4276		Example:
4277			```python
4278			# A simple pop structure
4279			comp.form([
4280				("verse", 8),
4281				("chorus", 8),
4282				("verse", 8),
4283				("chorus", 16)
4284			])
4285
4286			# The same structure with payloads, held open at the end
4287			S = subsequence.Section
4288			comp.form(subsequence.Form([
4289				S("verse", 8, energy=0.5), S("chorus", 8, energy=0.9),
4290			]), at_end="hold")
4291			```
4292		"""
4293
4294		# Seed FormState at form() time (per-call salt) so build-time walks —
4295		# the frozen clones form_freeze will take — are deterministic without
4296		# play(); the play-time stream is re-dealt name-keyed in _run().
4297		self._form_count += 1
4298
4299		self._form_state = subsequence.form_state.FormState(
4300			sections,
4301			loop = loop,
4302			start = start,
4303			rng = self._stream(f"form:{self._form_count}"),
4304			at_end = at_end,
4305		)
4306
4307		# A Form value carries energy payloads — that counts as an energy
4308		# source for the min_energy registration check in _run().
4309		self._form_has_payload = isinstance(sections, subsequence.forms.Form) or (
4310			isinstance(sections, list) and any(isinstance(element, subsequence.forms.Section) for element in sections)
4311		)
4312
4313		# Form-tier key/scale: an explicit argument wins; otherwise a Form
4314		# value's own key/scale seeds the tier.  Re-binding the form drops any
4315		# stale per-section resolution cache.
4316		if isinstance(sections, subsequence.forms.Form):
4317			self._form_key = key if key is not None else sections.key
4318			self._form_scale = scale if scale is not None else sections.scale
4319		else:
4320			self._form_key = key
4321			self._form_scale = scale
4322
4323		self._resolved_section_cache = {}
4324
4325	def form_freeze (self, sections: typing.Optional[int] = None) -> "subsequence.forms.Form":
4326
4327		"""Freeze the graph form's walk into an editable :class:`~subsequence.forms.Form`.
4328
4329		Walks a **clone** of the live form state — the same RNG state, so the
4330		frozen path is exactly the path the live graph would have played —
4331		and returns it as a Form value: inspect it, edit it
4332		(``path.replace(3, bars=16)``), and rebind it with
4333		``composition.form(path, at_end=...)``.  The live form state is
4334		untouched (rebinding replaces it).
4335
4336		Parameters:
4337			sections: Number of sections to freeze.  Without it, the walk
4338				runs until a terminal section; a graph with no terminal
4339				sections requires ``sections=`` explicitly.
4340
4341		Raises:
4342			ValueError: If no graph form is bound (a list form is already a
4343				frozen sequence), the form has already finished, or the walk
4344				cannot terminate.
4345
4346		Example::
4347
4348			composition.form({...}, start="intro")
4349			path = composition.form_freeze()          # the walk, frozen
4350			composition.form(path, at_end="stop")     # rebind the editable value
4351		"""
4352
4353		fs = self._form_state
4354
4355		if fs is None or fs._graph is None or fs._section_bars is None:
4356			raise ValueError(
4357				"form_freeze() freezes a graph form's walk — call form() with a dict first "
4358				"(a list form is already a frozen sequence)"
4359			)
4360
4361		if fs._current is None:
4362			raise ValueError("the form has already finished — nothing left to freeze")
4363
4364		if sections is not None and sections < 1:
4365			raise ValueError("sections must be at least 1")
4366
4367		if sections is None and not fs._terminal_sections:
4368			raise ValueError(
4369				"this graph has no terminal section, so the walk would never end — "
4370				"pass sections=n to bound it"
4371			)
4372
4373		# Clone the RNG state: the frozen walk reproduces the live form's
4374		# future draws without consuming them.
4375		rng = random.Random()
4376		rng.setstate(fs._rng.getstate())
4377
4378		walked = [fs._current]
4379		next_name = fs._next_section_name		# already decided by the live state
4380
4381		while next_name is not None:
4382			if sections is not None and len(walked) >= sections:
4383				break
4384			if sections is None and len(walked) >= 10000:
4385				raise ValueError(
4386					"form_freeze() walked 10000 sections without reaching a terminal — "
4387					"the terminals look unreachable; pass sections=n to bound the walk"
4388				)
4389
4390			walked.append(subsequence.forms.Section(name = next_name, bars = fs._section_bars[next_name]))
4391			next_name = None if next_name in fs._terminal_sections else fs._graph.choose_next(next_name, rng)
4392
4393		# Carry the form-tier key/scale onto the frozen value so a freeze →
4394		# rebind round-trip is lossless (an explicit form(key=) on rebind
4395		# still overrides).
4396		return subsequence.forms.Form(walked, key = self._form_key, scale = self._form_scale)
4397
4398	def energy (self, energies: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]]) -> None:
4399
4400		"""Set per-section energy — the arranging dial, as one plain dict.
4401
4402		``{"verse": 0.5, "chorus": 0.9, "build": (0.3, 1.0)}`` — a float is
4403		the section's level; a ``(start, end)`` tuple interpolates across the
4404		section (a build).  Patterns read ``p.energy`` (0.5 when nothing is
4405		configured) and gate themselves, or declare ``min_energy=`` on
4406		``pattern()`` for automatic muting.
4407
4408		The dict **overrides** any energy payload carried by bound
4409		:class:`~subsequence.forms.Section` values — it is the later,
4410		performance-level dial.  Re-calling replaces the whole mapping
4411		(idempotent, live-reload friendly).
4412
4413		Example::
4414
4415			composition.energy({"intro": 0.2, "verse": 0.55, "drop": 0.95})
4416		"""
4417
4418		validated: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]] = {}
4419
4420		for name, value in energies.items():
4421			if isinstance(value, tuple):
4422				if len(value) != 2:
4423					raise ValueError(f"energy ramp for {name!r} must be (start, end), got {value!r}")
4424				start_level, end_level = float(value[0]), float(value[1])
4425				for level in (start_level, end_level):
4426					if not 0.0 <= level <= 1.0:
4427						raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4428				validated[name] = (start_level, end_level)
4429			else:
4430				level = float(value)
4431				if not 0.0 <= level <= 1.0:
4432					raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4433				validated[name] = level
4434
4435		self._energy_map = validated
4436
4437	def _current_energy (self, info: typing.Optional[subsequence.form_state.SectionInfo]) -> float:
4438
4439		"""Resolve the energy for a section snapshot.
4440
4441		Priority: the ``energy()`` dict (ramps interpolate by section
4442		progress) > the bound Section payload > 0.5.
4443		"""
4444
4445		if info is None:
4446			return 0.5
4447
4448		spec = self._energy_map.get(info.name)
4449
4450		if spec is None:
4451			return info.energy
4452
4453		if isinstance(spec, tuple):
4454			start_level, end_level = spec
4455
4456			# A build reaches its declared end ON the final bar, so the ramp spans
4457			# bar 0 → bar (bars-1).  (info.progress is bar/bars, which would top
4458			# out one bar short and never deliver end.)  A one-bar section sits at
4459			# the destination level.
4460			span = info.bars - 1
4461			fraction = info.bar / span if span > 0 else 1.0
4462
4463			return start_level + (end_level - start_level) * fraction
4464
4465		return spec
4466
4467	def on_section (self, callback: typing.Callable[..., typing.Any]) -> None:
4468
4469		"""Register a callback fired on every section change.
4470
4471		The callback receives the new :class:`~subsequence.form_state.SectionInfo`
4472		(or ``None`` when the form finishes).  It fires from the form clock,
4473		one lookahead-beat **early** — in time to affect the new section's
4474		first patterns — and once at play start for the opening section.
4475
4476		Example::
4477
4478			composition.on_section(lambda info: print(f"now: {info.name if info else 'end'}"))
4479		"""
4480
4481		self.on_event("section", callback)
4482
4483	def transition (
4484		self,
4485		before: str,
4486		fill: typing.Optional[typing.Any] = None,
4487		channel: typing.Optional[int] = None,
4488		beat: float = 0.0,
4489		mute: typing.Optional[typing.List[str]] = None,
4490		beats: typing.Optional[float] = None,
4491		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4492		device: subsequence.midi_utils.DeviceId = None,
4493	) -> None:
4494
4495		"""Declare boundary material — the automatic fill or mute, one line.
4496
4497		``before`` names the incoming section (``"chorus"``), or ``"*"`` for
4498		any *different* section (repeats don't fire it).  Two actions,
4499		combinable:
4500
4501		- ``fill=`` (+ ``channel=``, ``beat=``): a Motif played in the last
4502		  bar before the boundary, starting at ``beat`` of that bar.  Drum
4503		  names resolve through ``drum_note_map=`` if given, otherwise the
4504		  map is borrowed from a registered pattern on the same channel.
4505		- ``mute=`` (+ ``beats=``): pattern names muted over the approach
4506		  and unmuted at the boundary.  Muting is **bar-granular** (the
4507		  existing rule), so ``beats`` rounds up to whole bars.  Performer
4508		  mutes win: a pattern you muted yourself stays muted.
4509
4510		Transitions stack — call once per rule.  Registration is additive
4511		and idempotent per identical rule.
4512
4513		Example::
4514
4515			composition.transition(before="*", fill=FILL, channel=10, beat=2.0)
4516			composition.transition(before="drop", mute=["pads"], beats=4)
4517		"""
4518
4519		if fill is None and mute is None:
4520			raise ValueError("transition() needs fill= and/or mute= — it declares what happens at the boundary")
4521
4522		if fill is not None:
4523			if channel is None:
4524				raise ValueError("transition(fill=) needs channel= — the fill must land somewhere")
4525			if not hasattr(fill, "events") or not hasattr(fill, "length"):
4526				raise TypeError(f"fill must be a Motif-like value with .events/.length, got {type(fill).__name__}")
4527
4528		if mute is not None and beats is None:
4529			beats = float(self.time_signature[0])		# one bar by default
4530
4531		rule = _Transition(
4532			before = before,
4533			fill = fill,
4534			channel = self._resolve_channel(channel) if channel is not None else None,
4535			beat = float(beat),
4536			mute = list(mute) if mute is not None else None,
4537			beats = beats,
4538			drum_note_map = drum_note_map,
4539			device = device,			# resolved at fire time — names aren't known until play()
4540		)
4541
4542		if rule not in self._transitions:
4543			self._transitions.append(rule)
4544
4545	def _transition_drum_map (self, channel: typing.Optional[int]) -> typing.Optional[typing.Dict[str, int]]:
4546
4547		"""Borrow a drum map from a registered pattern on the same channel."""
4548
4549		if channel is None:
4550			return None
4551
4552		for pending in self._pending_patterns:
4553			if pending.channel == channel and pending.drum_note_map:
4554				return pending.drum_note_map
4555
4556		for running in self._running_patterns.values():
4557			candidate = getattr(running, "_drum_note_map", None)
4558			if running.channel == channel and candidate:
4559				return typing.cast(typing.Dict[str, int], candidate)
4560
4561		return None
4562
4563	def _fire_fill (self, rule: _Transition, start_pulse: int) -> None:
4564
4565		"""Build a transition fill as a one-shot pattern and schedule it."""
4566
4567		assert rule.fill is not None and rule.channel is not None
4568
4569		drum_map = rule.drum_note_map if rule.drum_note_map is not None else self._transition_drum_map(rule.channel)
4570
4571		pattern = subsequence.pattern.Pattern(
4572			channel = rule.channel,
4573			length = float(rule.fill.length),
4574			device = self._resolve_device_id(rule.device),
4575		)
4576
4577		harmony_view: typing.Optional[HarmonyView] = None
4578		if not self._harmony_horizon.is_empty:
4579			harmony_view = HarmonyView(self._harmony_horizon, start_pulse / self._sequencer.pulses_per_beat)
4580
4581		# The fill sounds in the outgoing section's final bar, so a degree-
4582		# bearing fill resolves against THAT section's effective key/scale —
4583		# previously it took the composition key, ignoring the section.
4584		fill_section = self._form_state.get_section_info() if self._form_state else None
4585		fill_key, fill_scale = self._effective_key_scale(fill_section)
4586
4587		builder = subsequence.pattern_builder.PatternBuilder(
4588			pattern = pattern,
4589			cycle = 0,
4590			drum_note_map = drum_map,
4591			section = fill_section,
4592			bar = self._builder_bar,
4593			conductor = self.conductor,
4594			rng = self._stream(f"transition:{rule.before}:{start_pulse}") or random.Random(),
4595			tweaks = {},
4596			default_grid = 16,
4597			data = self.data,
4598			key = fill_key,
4599			scale = fill_scale,
4600			time_signature = self.time_signature,
4601			harmony = harmony_view,
4602		)
4603
4604		try:
4605			builder.motif(rule.fill)
4606		except Exception:
4607			logger.exception("transition fill failed to build — the boundary plays without it")
4608			return
4609
4610		self._schedule_one_shot(pattern, start_pulse)
4611
4612	def _check_transitions (self, boundary_pulse: int, section_changed: bool) -> None:
4613
4614		"""The form clock's boundary hook: fire fills, manage approach mutes.
4615
4616		Called once per bar (lookahead-early, with the bar-line pulse).
4617		Fill rules fire when the current bar is the section's last before a
4618		matching boundary; mute rules close over the approach window
4619		(rounded up to whole bars — muting is bar-granular) and reopen at
4620		the boundary.  Performer mutes are never touched.
4621		"""
4622
4623		if section_changed and self._transition_muted:
4624			# The boundary arrived — restore only what we muted ourselves.
4625			for name in self._transition_muted:
4626				running = self._running_patterns.get(name)
4627				if running is not None:
4628					running._muted = False
4629			self._transition_muted.clear()
4630
4631		if not self._transitions or self._form_state is None:
4632			return
4633
4634		info = self._form_state.get_section_info()
4635
4636		if info is None or info.next_section is None:
4637			return
4638
4639		bar_beats = float(self.time_signature[0])
4640		bars_remaining = info.bars - info.bar
4641
4642		for rule in self._transitions:
4643
4644			if rule.before == "*":
4645				if info.next_section == info.name:
4646					continue		# a repeat is not a boundary
4647			elif info.next_section != rule.before:
4648				continue
4649
4650			if rule.fill is not None and bars_remaining == 1:
4651				self._fire_fill(rule, boundary_pulse + int(round(rule.beat * self._sequencer.pulses_per_beat)))
4652
4653			if rule.mute:
4654				window_beats = rule.beats if rule.beats is not None else bar_beats
4655				window_bars = max(1, int((window_beats + bar_beats - 1e-9) // bar_beats))
4656
4657				if bars_remaining <= window_bars:
4658					for name in rule.mute:
4659						running = self._running_patterns.get(name)
4660						if running is None or name in self._transition_muted:
4661							continue
4662						if running._muted:
4663							continue		# the performer's mute — not ours to manage
4664						running._muted = True
4665						self._transition_muted.add(name)
4666
4667	@staticmethod
4668	def _resolve_length (
4669		beats: typing.Optional[float],
4670		bars: typing.Optional[float],
4671		steps: typing.Optional[float],
4672		step_duration: typing.Optional[float],
4673		default: float = 4.0,
4674		beats_per_bar: int = 4,
4675	) -> typing.Tuple[float, int]:
4676
4677		"""
4678		Resolve the beat_length and default_grid from the duration parameters.
4679
4680		Two modes:
4681
4682		- **Duration mode** (no ``step_duration``): specify ``beats=`` or ``bars=``.
4683		  ``beats=4`` = 4 quarter notes; ``bars=2`` = 8 beats.
4684		- **Step mode** (with ``step_duration``): specify ``steps=`` and ``step_duration=``.
4685		  ``steps=6, step_duration=dur.SIXTEENTH`` = 6 sixteenth notes = 1.5 beats.
4686
4687		Constraints:
4688
4689		- ``beats`` and ``bars`` are mutually exclusive.
4690		- ``steps`` requires ``step_duration``; ``step_duration`` requires ``steps``.
4691		- ``steps`` cannot be combined with ``beats`` or ``bars``.
4692
4693		Returns:
4694			(beat_length, default_grid) — beat_length in beats (quarter notes);
4695			default_grid the number of grid steps (16th-notes in beat mode, or the
4696			explicit ``steps`` value directly in step mode).
4697		"""
4698
4699		if beats is not None and bars is not None:
4700			raise ValueError("Specify only one of beats= or bars=")
4701
4702		if steps is not None and (beats is not None or bars is not None):
4703			raise ValueError("steps= cannot be combined with beats= or bars=")
4704
4705		if step_duration is not None and steps is None:
4706			raise ValueError("step_duration= requires steps= (e.g. steps=6, step_duration=dur.SIXTEENTH)")
4707
4708		if steps is not None:
4709			if step_duration is None:
4710				raise ValueError("steps= requires step_duration= (e.g. step_duration=dur.SIXTEENTH)")
4711			return steps * step_duration, int(steps)
4712
4713		if bars is not None:
4714			raw = bars * beats_per_bar
4715		elif beats is not None:
4716			raw = beats
4717		else:
4718			raw = default
4719
4720		return raw, round(raw / subsequence.constants.durations.SIXTEENTH)
4721
4722	def pattern (
4723		self,
4724		channel: int,
4725		beats: typing.Optional[float] = None,
4726		bars: typing.Optional[float] = None,
4727		steps: typing.Optional[float] = None,
4728		step_duration: typing.Optional[float] = None,
4729		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4730		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4731		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4732		reschedule_lookahead: float = 1,
4733		voice_leading: bool = False,
4734		device: subsequence.midi_utils.DeviceId = None,
4735		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4736		min_energy: typing.Optional[float] = None,
4737	) -> typing.Callable:
4738
4739		"""
4740		Register a function as a repeating MIDI pattern.
4741
4742		The decorated function will be called once per cycle to 'rebuild' its
4743		content. This allows for generative logic that evolves over time.
4744
4745		Two ways to specify pattern length:
4746
4747		- **Duration mode** (default): use ``beats=`` or ``bars=``.
4748		  The grid defaults to sixteenth-note resolution.
4749		- **Step mode**: use ``steps=`` paired with ``step_duration=``.
4750		  The grid equals the step count, so ``p.hit_steps()`` indices map
4751		  directly to steps.
4752
4753		Parameters:
4754			channel: MIDI channel. By default uses 1-based numbering (1-16).
4755				Set ``zero_indexed_channels=True`` on the ``Composition`` to use
4756				0-based numbering (0-15), matching the raw MIDI protocol, instead.
4757			beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar.
4758			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4). ``bars=2`` = 8 beats.
4759			steps: Step count for step mode. Requires ``step_duration=``.
4760			step_duration: Duration of one step in beats (e.g. ``dur.SIXTEENTH``).
4761				Requires ``steps=``.
4762			drum_note_map: Optional mapping for drum instruments.
4763			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4764				Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``.
4765			nrpn_name_map: Optional mapping of NRPN parameter names (strings) to
4766				14-bit parameter numbers (0–16383).  Enables string-based names
4767				in ``p.nrpn()`` and ``p.nrpn_ramp()`` — typically a
4768				device-specific dictionary (e.g. Sequential Take 5's
4769				``Osc1FreqFine`` → 9).
4770			reschedule_lookahead: Beats in advance to compute the next cycle.
4771			voice_leading: If True, chords in this pattern will automatically
4772				use inversions that minimize voice movement.
4773			mirrors: Optional list of additional ``(device, channel)`` destinations
4774				to duplicate every event from this pattern onto.  Notes, CCs, pitch
4775				bend, NRPN/RPN bursts, program changes, SysEx, and drone events are
4776				all mirrored; OSC events are not (OSC is not bound to a MIDI port).
4777				``device`` is the integer index returned by ``midi_output()`` (0 =
4778				primary).  ``channel`` follows this composition's channel-numbering
4779				convention.  See also ``mirror()`` / ``unmirror()`` for live toggling.
4780			min_energy: Automatic energy gating — the pattern is silent while
4781				the current section's energy (``composition.energy()`` dict,
4782				or the bound Section payload) is below this threshold.
4783				Composes with ``mute()``: a performer mute always wins.
4784
4785		Example:
4786			```python
4787			@comp.pattern(channel=1, beats=4)
4788			def chords (p):
4789				p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)
4790
4791			@comp.pattern(channel=1, bars=2)
4792			def long_phrase (p):
4793				...
4794
4795			@comp.pattern(channel=1, steps=6, step_duration=dur.SIXTEENTH)
4796			def riff (p):
4797				p.sequence(steps=[0, 1, 3, 5], pitches=60)
4798			```
4799		"""
4800
4801		channel = self._resolve_channel(channel)
4802
4803		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4804
4805		# Resolve device string name to index if possible now; otherwise store
4806		# the raw DeviceId and resolve it in _run() once all devices are open.
4807		resolved_device: subsequence.midi_utils.DeviceId = device
4808
4809		# Mirror-to-self check is only reliable when the primary device is a
4810		# concrete integer at decoration time.  ``None`` resolves to device 0
4811		# downstream, so we treat it as 0 here too.  Strings are deferred to
4812		# ``_run()`` and we skip the check for them.
4813		primary: typing.Optional[typing.Tuple[int, int]]
4814		if isinstance(resolved_device, str):
4815			primary = None
4816		else:
4817			primary = (resolved_device if resolved_device is not None else 0, channel)
4818		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4819
4820		def decorator (fn: typing.Callable) -> typing.Callable:
4821
4822			"""
4823			Wrap the builder function and register it as a pending pattern.
4824			During live sessions, hot-swap an existing pattern's builder instead.
4825			"""
4826
4827			# Record this declaration so the live-reload deletion diff knows the
4828			# pattern is still present in the source (see _apply_source_async).
4829			self._declared_names.add(fn.__name__)
4830
4831			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
4832			if self._is_live and fn.__name__ in self._running_patterns:
4833				running = self._running_patterns[fn.__name__]
4834				running._builder_fn = fn
4835				running._wants_chord = _fn_has_parameter(fn, "chord")
4836				logger.info(f"Hot-swapped pattern: {fn.__name__}")
4837				return fn
4838
4839			# Names key the seeded stream, mutes, tweaks, and reroll/lock — a
4840			# duplicate means two scheduled copies sharing one stream with
4841			# only one reachable by name.  Warn loudly at registration.
4842			if any(existing.builder_fn.__name__ == fn.__name__ for existing in self._pending_patterns):
4843				logger.warning(
4844					f"Duplicate pattern name '{fn.__name__}': both copies will be "
4845					f"scheduled, they share one seeded stream, and only one is "
4846					f"reachable by name — rename one of them."
4847				)
4848
4849			pending = _PendingPattern(
4850				builder_fn = fn,
4851				channel = channel,  # already resolved to 0-indexed
4852				length = beat_length,
4853				default_grid = default_grid,
4854				drum_note_map = drum_note_map,
4855				cc_name_map = cc_name_map,
4856				nrpn_name_map = nrpn_name_map,
4857				reschedule_lookahead = reschedule_lookahead,
4858				voice_leading = voice_leading,
4859				# For int/None: resolve immediately.  For str: store 0 as
4860				# placeholder; _resolve_pending_devices() fixes it in _run().
4861				device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device,
4862				raw_device = resolved_device,
4863				mirrors = resolved_mirrors,
4864				min_energy = min_energy,
4865			)
4866
4867			self._pending_patterns.append(pending)
4868
4869			return fn
4870
4871		return decorator
4872
4873	def layer (
4874		self,
4875		*builder_fns: typing.Callable,
4876		channel: int,
4877		beats: typing.Optional[float] = None,
4878		bars: typing.Optional[float] = None,
4879		steps: typing.Optional[float] = None,
4880		step_duration: typing.Optional[float] = None,
4881		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4882		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4883		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4884		reschedule_lookahead: float = 1,
4885		voice_leading: bool = False,
4886		device: subsequence.midi_utils.DeviceId = None,
4887		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4888	) -> None:
4889
4890		"""
4891		Combine multiple functions into a single MIDI pattern.
4892
4893		This is useful for composing complex patterns out of reusable
4894		building blocks (e.g., a 'kick' function and a 'snare' function).
4895
4896		See ``pattern()`` for the full description of ``beats``, ``bars``,
4897		``steps``, and ``step_duration``.
4898
4899		Parameters:
4900			builder_fns: One or more pattern builder functions.
4901			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
4902			beats: Duration in beats (quarter notes).
4903			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
4904			steps: Step count for step mode. Requires ``step_duration=``.
4905			step_duration: Duration of one step in beats. Requires ``steps=``.
4906			drum_note_map: Optional mapping for drum instruments.
4907			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4908			nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit
4909				parameter numbers.
4910			reschedule_lookahead: Beats in advance to compute the next cycle.
4911			voice_leading: If True, chords use smooth voice leading.
4912			mirrors: Optional list of additional ``(device, channel)`` destinations
4913				to duplicate every event onto.  See ``pattern()`` for details.
4914		"""
4915
4916		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4917
4918		# Resolve channel up-front so the mirror-to-self check has the canonical
4919		# primary form to compare against.
4920		resolved_channel = self._resolve_channel(channel)
4921
4922		# See pattern() for the same comment about None / str handling.
4923		primary: typing.Optional[typing.Tuple[int, int]]
4924		if isinstance(device, str):
4925			primary = None
4926		else:
4927			primary = (device if device is not None else 0, resolved_channel)
4928		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4929
4930		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
4931
4932		if wants_chord:
4933
4934			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
4935
4936				for fn in builder_fns:
4937					if _fn_has_parameter(fn, "chord"):
4938						fn(p, chord)
4939					else:
4940						fn(p)
4941
4942		else:
4943
4944			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
4945
4946				for fn in builder_fns:
4947					fn(p)
4948
4949		# Give the merged builder a stable, unique name derived from its
4950		# components so multiple layer() calls don't all register under
4951		# "merged_builder" and collide in _running_patterns (which made
4952		# mute/tweak/unregister/live_info reach only the LAST layer).  "+" can't
4953		# appear in a Python identifier, so this never clashes with a real
4954		# pattern function's name.
4955		base_name = ("+".join(fn.__name__ for fn in builder_fns) or "layer") + f"@ch{resolved_channel}"
4956		merged_name = base_name
4957		suffix = 2
4958
4959		# Two layers with the same components (e.g. on different saves of a
4960		# live file) must map to the same names pass-over-pass, while two
4961		# DIFFERENT layers sharing components in one pass must not collide.
4962		while merged_name in self._declared_names:
4963			merged_name = f"{base_name}#{suffix}"
4964			suffix += 1
4965
4966		merged_builder.__name__ = merged_name
4967
4968		# Record the declaration for the live-reload deletion diff, and hot-swap
4969		# in place when this layer is already running so a reload picks up edits
4970		# to the component functions without losing the pattern's cycle count,
4971		# tweaks, or mirrors (mirrors the pattern() decorator's hot-swap).
4972		self._declared_names.add(merged_builder.__name__)
4973
4974		if self._is_live and merged_builder.__name__ in self._running_patterns:
4975			running = self._running_patterns[merged_builder.__name__]
4976			running._builder_fn = merged_builder
4977			running._wants_chord = wants_chord
4978			logger.info(f"Hot-swapped layer: {merged_builder.__name__}")
4979			return
4980
4981		pending = _PendingPattern(
4982			builder_fn = merged_builder,
4983			channel = resolved_channel,  # already resolved to 0-indexed above
4984			length = beat_length,
4985			default_grid = default_grid,
4986			drum_note_map = drum_note_map,
4987			cc_name_map = cc_name_map,
4988			nrpn_name_map = nrpn_name_map,
4989			reschedule_lookahead = reschedule_lookahead,
4990			voice_leading = voice_leading,
4991			mirrors = resolved_mirrors,
4992			device = 0 if (device is None or isinstance(device, str)) else device,
4993			raw_device = device,
4994		)
4995
4996		self._pending_patterns.append(pending)
4997
4998	def chords (
4999		self,
5000		*,
5001		channel: int,
5002		progression: subsequence.progressions.ProgressionSource,
5003		harmonic_rhythm: subsequence.progressions.HarmonicRhythmSpec,
5004		bars: typing.Optional[float] = None,
5005		beats: typing.Optional[float] = None,
5006		voicing: subsequence.progressions.VoicingSpec = (3, 4),
5007		velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY,
5008		detached: typing.Optional[float] = None,
5009		root: int = 60,
5010		key: typing.Optional[str] = None,
5011		seed: typing.Optional[int] = None,
5012		device: subsequence.midi_utils.DeviceId = None,
5013		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5014	) -> subsequence.progressions.Progression:
5015
5016		"""Declare a self-contained chord part: a progression at a chosen harmonic rhythm.
5017
5018		The one-call form of ``p.progression()`` — it registers a pattern on
5019		*channel* that plays *progression* across *bars* (or *beats*), each chord
5020		lasting a length drawn from *harmonic_rhythm* (the musical term for how often
5021		the chords change).  It needs no ``composition.harmony()`` call and, with an
5022		explicit chord list or a ``key=``, no composition key either — so a
5023		drums-plus-one-chord-part sketch stays simple.
5024
5025		The progression is realised once, up front, and the same timeline plays every
5026		cycle (a stable phrase).  That timeline is returned so you can see exactly what
5027		was chosen — ``print(comp.chords(...))``.
5028
5029		Parameters:
5030			channel: MIDI channel for the chord part.
5031			progression: A chord-graph style name to generate from, or an explicit list
5032				of chords (``Chord`` objects or names like ``["Cm7", "Dbmaj7"]``).
5033			harmonic_rhythm: How long each chord lasts — a number, a list of lengths,
5034				or ``between(low, high, step=...)``.  See ``p.progression()``.
5035			bars / beats: Length of the part (defaults to 4 beats if neither is given).  ``bars`` uses the
5036				composition's time signature.
5037			voicing: Notes per chord — an int, or a ``(low, high)`` range (e.g. ``(3, 4)``).
5038			velocity: MIDI velocity, or a ``(low, high)`` tuple for per-voice humanisation.
5039			detached: Beats of silence before each next chord (``duration = length - detached``).
5040			root: MIDI root the voicings are centred on (e.g. 48 = C3).
5041			key: Key for a generated progression; defaults to the composition key.
5042			seed: Seed for the (otherwise fixed) realisation; defaults to the
5043				composition seed, so the part is reproducible.
5044			device: Optional output-device override.
5045			mirrors: Optional additional ``(device, channel)`` destinations.
5046
5047		Returns:
5048			The realised :class:`~subsequence.progressions.Progression`.
5049		"""
5050
5051		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5052		resolved_channel = self._resolve_channel(channel)
5053		resolved_key = key if key is not None else self.key
5054
5055		rng = random.Random(seed if seed is not None else self._seed)
5056		timeline = subsequence.progressions.realize(
5057			source = progression,
5058			harmonic_rhythm = harmonic_rhythm,
5059			key = resolved_key,
5060			length = beat_length,
5061			rng = rng,
5062			scale = self.scale or "ionian",
5063		)
5064
5065		captured_root = root
5066		captured_velocity = velocity
5067		captured_detached = detached
5068		captured_voicing = voicing
5069
5070		def chords_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5071
5072			"""Replay the realised timeline as block chords each cycle (voicing per chord)."""
5073
5074			for chord, start, length in timeline:
5075				ring = length - captured_detached if (captured_detached and captured_detached < length) else length
5076				voices = subsequence.progressions.resolve_voices(captured_voicing, p.rng)
5077				p.chord(chord, root=captured_root, beat=start, duration=ring, count=voices, velocity=captured_velocity)
5078
5079		# Unique, stable name so multiple chord parts don't collide in
5080		# _running_patterns — including two parts on the SAME channel, which
5081		# get a deterministic #2/#3 suffix in declaration order.
5082		base_name = f"chords@ch{resolved_channel}"
5083		chords_name = base_name
5084		suffix = 2
5085
5086		while chords_name in self._declared_names:
5087			chords_name = f"{base_name}#{suffix}"
5088			suffix += 1
5089
5090		chords_builder.__name__ = chords_name
5091		self._declared_names.add(chords_name)
5092
5093		primary: typing.Optional[typing.Tuple[int, int]]
5094		if isinstance(device, str):
5095			primary = None
5096		else:
5097			primary = (device if device is not None else 0, resolved_channel)
5098		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5099
5100		if self._is_live and chords_builder.__name__ in self._running_patterns:
5101			running = self._running_patterns[chords_builder.__name__]
5102			running._builder_fn = chords_builder
5103			running._wants_chord = False
5104			logger.info(f"Hot-swapped chords: {chords_builder.__name__}")
5105			return timeline
5106
5107		pending = _PendingPattern(
5108			builder_fn = chords_builder,
5109			channel = resolved_channel,
5110			length = beat_length,
5111			default_grid = default_grid,
5112			drum_note_map = None,
5113			reschedule_lookahead = 1,
5114			voice_leading = False,
5115			mirrors = resolved_mirrors,
5116			device = 0 if (device is None or isinstance(device, str)) else device,
5117			raw_device = device,
5118		)
5119		self._pending_patterns.append(pending)
5120		return timeline
5121
5122	def phrase_part (
5123		self,
5124		*,
5125		channel: int,
5126		part: typing.Optional[str] = None,
5127		root: int = 60,
5128		bars: typing.Optional[float] = None,
5129		beats: typing.Optional[float] = None,
5130		velocity: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
5131		fit: typing.Optional[float] = None,
5132		device: subsequence.midi_utils.DeviceId = None,
5133		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5134	) -> None:
5135
5136		"""Declare a part that plays each section's bound Motif/Phrase.
5137
5138		The one-call consumer for :meth:`section_motifs` — it registers a
5139		pattern on *channel* that walks whatever value is bound to the
5140		current section for *part* (stateless position from the cycle
5141		counter, via ``p.phrase()``).  A section with no binding for the
5142		part is **silent** for that part — bind material or don't; no
5143		fallback guessing.
5144
5145		Parameters:
5146			channel: MIDI channel for the part.
5147			part: The part label to read from the registry (``None`` = the
5148				unlabelled binding).
5149			root: Register anchor for degree resolution.
5150			bars / beats: Cycle length of the part (defaults to 4 beats);
5151				the phrase is sliced one cycle window at a time.
5152			velocity: Optional override applied to every note.
5153			fit: Passed through (active with the melody engine stage).
5154			device: Optional output-device override.
5155			mirrors: Optional additional ``(device, channel)`` destinations.
5156
5157		Example::
5158
5159			composition.section_motifs("verse",  verse_line,  part="lead")
5160			composition.section_motifs("chorus", chorus_line, part="lead")
5161			composition.phrase_part(channel=4, part="lead", root=72, bars=2)
5162		"""
5163
5164		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5165		resolved_channel = self._resolve_channel(channel)
5166
5167		captured_part = part
5168		captured_root = root
5169		captured_velocity = velocity
5170		captured_fit = fit
5171
5172		def phrase_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5173
5174			"""Walk the current section's bound value (silent when unbound)."""
5175
5176			value = p.section_motif(captured_part)
5177
5178			if value is None:
5179				return	# unbound section: silence for this part, by design
5180
5181			p.phrase(value, root=captured_root, velocity=captured_velocity, fit=captured_fit)
5182
5183		# Unique, stable name so multiple phrase parts don't collide —
5184		# including two parts on the SAME channel (deterministic #2/#3
5185		# suffixes in declaration order, the chords() convention).
5186		base_name = f"phrase@{captured_part}@ch{resolved_channel}" if captured_part else f"phrase@ch{resolved_channel}"
5187		phrase_name = base_name
5188		suffix = 2
5189
5190		while phrase_name in self._declared_names:
5191			phrase_name = f"{base_name}#{suffix}"
5192			suffix += 1
5193
5194		phrase_builder.__name__ = phrase_name
5195		self._declared_names.add(phrase_name)
5196
5197		primary: typing.Optional[typing.Tuple[int, int]]
5198		if isinstance(device, str):
5199			primary = None
5200		else:
5201			primary = (device if device is not None else 0, resolved_channel)
5202		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5203
5204		if self._is_live and phrase_builder.__name__ in self._running_patterns:
5205			running = self._running_patterns[phrase_builder.__name__]
5206			running._builder_fn = phrase_builder
5207			running._wants_chord = False
5208			logger.info(f"Hot-swapped phrase part: {phrase_builder.__name__}")
5209			return
5210
5211		pending = _PendingPattern(
5212			builder_fn = phrase_builder,
5213			channel = resolved_channel,
5214			length = beat_length,
5215			default_grid = default_grid,
5216			drum_note_map = None,
5217			reschedule_lookahead = 1,
5218			voice_leading = False,
5219			mirrors = resolved_mirrors,
5220			device = 0 if (device is None or isinstance(device, str)) else device,
5221			raw_device = device,
5222		)
5223		self._pending_patterns.append(pending)
5224
5225	def trigger (
5226		self,
5227		fn: typing.Callable,
5228		channel: int,
5229		beats: typing.Optional[float] = None,
5230		bars: typing.Optional[float] = None,
5231		steps: typing.Optional[float] = None,
5232		step_duration: typing.Optional[float] = None,
5233		quantize: float = 0,
5234		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
5235		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
5236		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
5237		chord: bool = False,
5238		device: subsequence.midi_utils.DeviceId = None,
5239		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5240	) -> None:
5241
5242		"""
5243		Trigger a one-shot pattern immediately or on a quantized boundary.
5244
5245		This is useful for real-time response to sensors, OSC messages, or other
5246		external events. The builder function is called immediately with a fresh
5247		PatternBuilder, and the generated events are injected into the queue at
5248		the specified quantize boundary.
5249
5250		The builder function has the same API as a ``@composition.pattern``
5251		decorated function and can use all PatternBuilder methods: ``p.note()``,
5252		``p.euclidean()``, ``p.arpeggio()``, and so on.
5253
5254		See ``pattern()`` for the full description of ``beats``, ``bars``,
5255		``steps``, and ``step_duration``. Default is 1 beat.
5256
5257		Parameters:
5258			fn: The pattern builder function (same signature as ``@comp.pattern``).
5259			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
5260			beats: Duration in beats (quarter notes, default 1).
5261			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
5262			steps: Step count for step mode. Requires ``step_duration=``.
5263			step_duration: Duration of one step in beats. Requires ``steps=``.
5264			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
5265				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
5266				constants from ``subsequence.constants.durations``.
5267			drum_note_map: Optional drum name mapping for this pattern.
5268			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
5269			nrpn_name_map: Optional mapping of NRPN parameter names to
5270				14-bit parameter numbers.
5271			chord: If ``True``, the builder function receives the current chord as
5272				a second parameter (same as ``@composition.pattern``).
5273			mirrors: Optional list of additional ``(device, channel)`` destinations
5274				to fire this one-shot onto in parallel with the primary destination.
5275
5276		Example:
5277			```python
5278			# Immediate single note (channels are 1-16 by default)
5279			composition.trigger(
5280				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
5281				channel=1
5282			)
5283
5284			# Quantized fill (next bar) — channel 10 is the GM drum channel
5285			import subsequence.constants.durations as dur
5286			composition.trigger(
5287				lambda p: p.euclidean("snare", pulses=7, velocity=90),
5288				channel=10,
5289				drum_note_map=gm_drums.GM_DRUM_MAP,
5290				quantize=dur.WHOLE
5291			)
5292
5293			# With chord context — the builder receives the chord as a second
5294			# argument when chord=True.
5295			composition.trigger(
5296				lambda p, chord: p.arpeggio(chord.tones(root=60), spacing=dur.SIXTEENTH),
5297				channel=1,
5298				quantize=dur.QUARTER,
5299				chord=True
5300			)
5301			```
5302		"""
5303
5304		# Resolve channel numbering
5305		resolved_channel = self._resolve_channel(channel)
5306
5307		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, default=1.0, beats_per_bar=self.time_signature[0])
5308
5309		# Resolve device index — for trigger() this is always concrete by call time,
5310		# so the mirror-to-self check has the full primary tuple available.
5311		resolved_device_idx = self._resolve_device_id(device)
5312		resolved_mirrors = self._resolve_mirrors(mirrors, primary=(resolved_device_idx, resolved_channel))
5313
5314		# Create a temporary Pattern
5315		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx, mirrors=resolved_mirrors)
5316
5317		# Resolve the section context once: the one-shot inherits the section's
5318		# effective key/scale (so a triggered degree resolves like everywhere
5319		# else) and a harmony view at the current playhead (so ChordTone /
5320		# Approach resolve too).
5321		trigger_section = self._form_state.get_section_info() if self._form_state else None
5322		trigger_key, trigger_scale = self._effective_key_scale(trigger_section)
5323
5324		trigger_harmony: typing.Optional[HarmonyView] = None
5325		if not self._harmony_horizon.is_empty:
5326			trigger_harmony = HarmonyView(self._harmony_horizon, self._sequencer.pulse_count / self._sequencer.pulses_per_beat)
5327
5328		# Create a PatternBuilder
5329		builder = subsequence.pattern_builder.PatternBuilder(
5330			pattern=pattern,
5331			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
5332			drum_note_map=drum_note_map,
5333			cc_name_map=cc_name_map,
5334			nrpn_name_map=nrpn_name_map,
5335			section=trigger_section,
5336			bar=self._builder_bar,
5337			conductor=self.conductor,
5338			rng=random.Random(),  # Fresh random state for each trigger
5339			tweaks={},
5340			default_grid=default_grid,
5341			data=self.data,
5342			# A one-shot resolves key-relative content against the same
5343			# effective key/scale as the section it fires into (previously
5344			# omitted entirely — degrees raised even in a keyed composition).
5345			key=trigger_key,
5346			scale=trigger_scale,
5347			time_signature=self.time_signature,
5348			held_notes=self._sequencer._held_notes,
5349			harmony=trigger_harmony,
5350			energy=self._current_energy(trigger_section)
5351		)
5352
5353		# Call the builder function
5354		try:
5355
5356			current_chord = self.current_chord() if chord else None
5357
5358			if current_chord is not None:
5359				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
5360				fn(builder, injected)
5361
5362			else:
5363				fn(builder)
5364
5365		except Exception:
5366			logger.exception("Error in trigger builder — pattern will be silent")
5367			return
5368
5369		# Calculate the start pulse based on quantize
5370		current_pulse = self._sequencer.pulse_count
5371		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
5372
5373		if quantize == 0:
5374			# Immediate: use current pulse
5375			start_pulse = current_pulse
5376
5377		else:
5378			# Quantize to the next multiple of (quantize * pulses_per_beat)
5379			quantize_pulses = int(quantize * pulses_per_beat)
5380			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
5381
5382		self._schedule_one_shot(pattern, start_pulse)
5383
5384	def _schedule_one_shot (self, pattern: subsequence.pattern.Pattern, start_pulse: int) -> None:
5385
5386		"""Schedule a one-shot pattern at an absolute pulse, thread-safely."""
5387
5388		try:
5389			# Probe only: raises RuntimeError when not on the event loop.
5390			asyncio.get_running_loop()
5391			asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse))
5392
5393		except RuntimeError:
5394			# Not on the event loop — hand the coroutine to the loop thread.
5395			if self._sequencer._event_loop is not None:
5396				asyncio.run_coroutine_threadsafe(
5397					self._sequencer.schedule_pattern(pattern, start_pulse),
5398					loop=self._sequencer._event_loop
5399				)
5400			else:
5401				logger.warning("trigger() called before playback started; pattern ignored")
5402
5403	@property
5404	def is_clock_following (self) -> bool:
5405
5406		"""True if either the primary or any additional device is following external clock."""
5407
5408		return self._clock_follow or any(cf for _, _, cf in self._additional_inputs)
5409
5410
5411	def play (self) -> None:
5412
5413		"""
5414		Start the composition.
5415
5416		This call blocks until the program is interrupted (e.g., via Ctrl+C).
5417		It initializes the MIDI hardware, launches the background sequencer,
5418		and begins playback.
5419		"""
5420
5421		try:
5422			asyncio.run(self._run())
5423
5424		except KeyboardInterrupt:
5425			pass
5426
5427
5428	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
5429
5430		"""Render the composition to a MIDI file without real-time playback.
5431
5432		Runs the sequencer as fast as possible (no timing delays) and stops
5433		when the first active limit is reached.  The result is saved as a
5434		standard MIDI file that can be imported into any DAW.
5435
5436		All patterns, scheduled callbacks, and harmony logic run exactly as
5437		they would during live playback — BPM transitions, generative fills,
5438		and probabilistic gates all work in render mode.  The only difference
5439		is that time is simulated rather than wall-clock driven.
5440
5441		Parameters:
5442			bars: Number of bars to render, or ``None`` for no bar limit
5443			      (default ``None``).  When both *bars* and *max_minutes* are
5444			      active, playback stops at whichever limit is reached first.
5445			filename: Output MIDI filename (default ``"render.mid"``).
5446			max_minutes: Safety cap on the length of rendered MIDI in minutes
5447			             (default ``60.0``).  Pass ``None`` to disable the time
5448			             cap — you must then provide an explicit *bars* value.
5449
5450		Raises:
5451			ValueError: If both *bars* and *max_minutes* are ``None``, which
5452			            would produce an infinite render.
5453
5454		Examples:
5455			```python
5456			# Default: renders up to 60 minutes of MIDI content.
5457			composition.render()
5458
5459			# Render exactly 64 bars (time cap still active as backstop).
5460			composition.render(bars=64, filename="demo.mid")
5461
5462			# Render up to 5 minutes of an infinite generative composition.
5463			composition.render(max_minutes=5, filename="five_min.mid")
5464
5465			# Remove the time cap — must supply bars instead.
5466			composition.render(bars=128, max_minutes=None, filename="long.mid")
5467			```
5468		"""
5469
5470		if bars is None and max_minutes is None:
5471			raise ValueError(
5472				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
5473				"Passing both as None would produce an infinite render."
5474			)
5475
5476		self._sequencer.recording = True
5477		self._sequencer.record_filename = filename
5478		self._sequencer.render_mode = True
5479		self._sequencer.render_bars = bars if bars is not None else 0
5480		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
5481		asyncio.run(self._run())
5482
5483	def _broadcast_osc_status (self, bar: int) -> None:
5484
5485		"""
5486		Send the per-bar OSC status snapshot: bar number, current tempo,
5487		and (when active) the current chord name and form section.
5488		"""
5489
5490		if self._osc_server:
5491			self._osc_server.send("/bar", bar)
5492			self._osc_server.send("/bpm", self._sequencer.current_bpm)
5493
5494			sounding = self.current_chord()
5495			if sounding is not None:
5496				self._osc_server.send("/chord", sounding.name())
5497
5498			if self._form_state:
5499				info = self._form_state.get_section_info()
5500				if info:
5501					self._osc_server.send("/section", info.name)
5502
5503	async def _run (self) -> None:
5504
5505		"""
5506		Async entry point that schedules all patterns and runs the sequencer.
5507		"""
5508
5509		# 1. Pre-calculate MIDI input indices and configure sequencer clock follow.
5510		if self._input_device is not None:
5511			self._sequencer.input_device_name = self._input_device
5512			self._sequencer.clock_follow = self._clock_follow
5513			self._sequencer.clock_device_idx = 0
5514
5515			if not self._clock_follow:
5516				# Find first additional input that wants to be the clock master.
5517				for idx, (_, _, cf) in enumerate(self._additional_inputs, start=1):
5518					if cf:
5519						self._sequencer.clock_follow = True
5520						self._sequencer.clock_device_idx = idx
5521						break
5522
5523		# Populate input device name mapping early (before opening ports) so we can
5524		# resolve CC mappings to integer device indices immediately.
5525		if self._sequencer.input_device_name:
5526			self._input_device_names[self._sequencer.input_device_name] = 0
5527			if self._input_device_alias is not None:
5528				self._input_device_names[self._input_device_alias] = 0
5529
5530		for idx, (dev_name, alias, _) in enumerate(self._additional_inputs, start=1):
5531			self._input_device_names[dev_name] = idx
5532			if alias:
5533				self._input_device_names[alias] = idx
5534
5535		# 2. Pre-calculate output device names.
5536		if self._sequencer.output_device_name:
5537			self._output_device_names[self._sequencer.output_device_name] = 0
5538			# Primary device (index 0) is open by now (_init_midi_output ran in
5539			# the Sequencer constructor), so its latency can be set safely here.
5540			if self._output_latency_ms:
5541				self._sequencer.set_device_latency(0, self._output_latency_ms)
5542
5543		# 3. Resolve name-based INPUT device ids in cc_map/cc_forward early — the
5544		# input-names map is fully populated above, and the callback thread needs
5545		# integer indices as soon as ports open.  OUTPUT names (cc_forward
5546		# output_device=, pattern device=) resolve after the additional outputs
5547		# are opened below; resolving them here matched against a map containing
5548		# only the primary and silently routed everything to device 0.
5549		for mapping in self._cc_mappings:
5550			raw = mapping.get('input_device')
5551			if isinstance(raw, str):
5552				mapping['input_device'] = self._resolve_input_device_id(raw)
5553		for fwd in self._cc_forwards:
5554			raw_in = fwd.get('input_device')
5555			if isinstance(raw_in, str):
5556				fwd['input_device'] = self._resolve_input_device_id(raw_in)
5557
5558		# 4. Share CC input mappings, forwards, and a reference to composition.data
5559		# with the sequencer BEFORE opening the ports. This ensures that any initial
5560		# messages in the OS buffer are correctly mapped as soon as the port opens.
5561		self._sequencer.cc_mappings = self._cc_mappings
5562		self._sequencer.cc_forwards = self._cc_forwards
5563		self._sequencer._composition_data = self.data
5564
5565		# Held-note input: create the tracker and resolve its channel/device
5566		# filter so the callback thread can buffer matching note events.
5567		if self._note_input is not None:
5568			if self._input_device is None and not self._additional_inputs:
5569				raise RuntimeError("note_input() requires a MIDI input — call composition.midi_input(device) first")
5570			raw_dev = self._note_input.get('input_device')
5571			if isinstance(raw_dev, str):
5572				raw_dev = self._resolve_input_device_id(raw_dev)
5573			self._sequencer._note_input_channel = self._note_input['channel']
5574			self._sequencer._note_input_device = raw_dev
5575			self._sequencer._held_notes = subsequence.held_notes.HeldNotes(
5576				release_ms = self._note_input['release_ms'],
5577				latch = self._note_input['latch'],
5578			)
5579
5580		# 5. Open MIDI input ports early. Even without a deliberate sleep, opening
5581		# them before pattern building minimizes the window for missed messages.
5582		# Primary input
5583		self._sequencer._open_midi_inputs()
5584
5585		# Additional inputs
5586		for idx, (dev_name, alias, cf) in enumerate(self._additional_inputs, start=1):
5587			# Use the pre-calculated index
5588			callback = self._sequencer._make_input_callback(idx)
5589			open_name, port = subsequence.midi_utils.select_input_device(dev_name, callback)
5590			if open_name and port is not None:
5591				self._sequencer.add_input_device(open_name, port)
5592			else:
5593				logger.warning(f"Could not open additional input device '{dev_name}'")
5594
5595		# 6. Open additional MIDI output devices.
5596		for out in self._additional_outputs:
5597			open_name, port = subsequence.midi_utils.select_output_device(out.device)
5598			if open_name and port is not None:
5599				idx = self._sequencer.add_output_device(open_name, port, out.latency_ms)
5600				self._output_device_names[open_name] = idx
5601				if out.alias is not None:
5602					self._output_device_names[out.alias] = idx
5603			else:
5604				logger.warning(f"Could not open additional output device '{out.device}'")
5605
5606		# Warn if latency compensation adds noticeable whole-rig delay: the
5607		# slowest device defines the alignment point, so every faster device is
5608		# delayed up to that amount and live-input feel suffers.
5609		self._warn_if_high_latency()
5610
5611		# Resolve any name-based output device IDs on patterns that may have been added
5612		# for additional output devices.
5613		self._resolve_pending_devices()
5614
5615		# Resolve cc_forward output-device names now that every output port and
5616		# alias is registered (resolving earlier silently routed to device 0).
5617		for fwd in self._cc_forwards:
5618			raw_out = fwd.get('output_device')
5619			if isinstance(raw_out, str):
5620				fwd['output_device'] = self._resolve_device_id(raw_out)
5621
5622		# Pass clock output flag (suppressed automatically when clock_follow=True).
5623		self._sequencer.clock_output = self._clock_output and not self.is_clock_following
5624
5625		# Create Ableton Link clock if comp.link() was called.
5626		if self._link_quantum is not None:
5627			self._sequencer._link_clock = subsequence.link_clock.LinkClock(
5628				bpm = self.bpm,
5629				quantum = self._link_quantum,
5630				loop = asyncio.get_running_loop(),
5631			)
5632
5633		# Deal play-time streams.  Every stream is NAME-keyed (crc32 of
5634		# "seed:name", see _stream_seed) rather than dealt from one master in
5635		# registration order: adding or removing one consumer can never shift
5636		# another's stream, and patterns added live derive identically in
5637		# _build_pattern_from_pending.  When no seed is set, components keep
5638		# their own unseeded RNGs (existing behaviour).
5639		if self._seed is not None:
5640
5641			harmony_stream = self._stream("play:harmony")
5642			if self._harmonic_state is not None and harmony_stream is not None:
5643				self._harmonic_state.rng = harmony_stream
5644
5645			form_stream = self._stream("play:form")
5646			if self._form_state is not None and form_stream is not None:
5647				self._form_state._rng = form_stream
5648
5649		# The clocks fire BEFORE pattern rebuilds at the same pulse, and their
5650		# lookahead is RAISED to the maximum pattern lookahead (never patterns
5651		# clamped down): when a pattern rebuilds for its next cycle, the form
5652		# state and the harmony window already describe that cycle.
5653		bar_beats = float(self.time_signature[0])
5654
5655		pattern_lookaheads = [pending.reschedule_lookahead for pending in self._pending_patterns]
5656		pattern_lookaheads += [pattern.reschedule_lookahead for pattern in self._running_patterns.values()]
5657		max_pattern_lookahead = max(pattern_lookaheads, default = 1)
5658
5659		clock_lookahead = max(1.0, float(self._harmony_reschedule_lookahead), float(max_pattern_lookahead))
5660
5661		if clock_lookahead > bar_beats:
5662			logger.warning(
5663				"A pattern's reschedule_lookahead (%.2g beats) exceeds the bar length (%.2g) — "
5664				"the harmony/form clocks fire at most one bar ahead, so that pattern may "
5665				"rebuild before the window covers its cycle start.",
5666				clock_lookahead, bar_beats,
5667			)
5668			clock_lookahead = bar_beats
5669
5670		# Minimum span >= maximum lookahead: the clock cannot prepare a chord
5671		# boundary that arrives sooner than it fires.  Harmonic motion faster
5672		# than this floor stays available at the part level (p.progression),
5673		# where placement is not clock-bound.
5674		def _check_span_floor (progression: typing.Optional[Progression], label: str) -> None:
5675			if progression is None:
5676				return
5677			shortest = min(span.beats for span in progression.spans)
5678			if shortest < clock_lookahead - 1e-9:
5679				raise ValueError(
5680					f"{label}: shortest chord span ({shortest:g} beats) is below the clock "
5681					f"lookahead ({clock_lookahead:g} beats — the largest pattern lookahead). "
5682					"Lengthen the span, lower the pattern lookaheads, or place fast harmony "
5683					"at the part level with p.progression()."
5684				)
5685
5686		_check_span_floor(self._bound_progression, "harmony(progression=)")
5687		for section_name, section_progression in self._section_progressions.items():
5688			_check_span_floor(section_progression, f"section_chords({section_name!r})")
5689
5690		# Key-relative section progressions resolve late, per occurrence — so
5691		# verify they WILL resolve now, before playback, rather than surfacing
5692		# a silent skip (or a dead clock) mid-render.  For each occurrence's
5693		# effective key+scale: a missing key, or a degree/scale that does not
5694		# resolve, is raised here with an actionable message.
5695		fs = self._form_state
5696
5697		for section_name, section_progression in self._section_progressions.items():
5698			if section_progression.is_concrete:
5699				continue
5700
5701			# The (key, scale) contexts this section may be resolved against.
5702			contexts: typing.List[typing.Tuple[typing.Optional[str], typing.Optional[str]]] = []
5703			if fs is not None and fs._sequence is not None and any(s.name == section_name for s in fs._sequence):
5704				for section in fs._sequence:
5705					if section.name != section_name:
5706						continue
5707					ctx = (section.key or self._form_key or self.key, section.scale or self._form_scale or self.scale)
5708					if ctx not in contexts:
5709						contexts.append(ctx)
5710			else:
5711				contexts.append((self._form_key or self.key, self._form_scale or self.scale))
5712
5713			for ctx_key, ctx_scale in contexts:
5714				if ctx_key is None:
5715					raise ValueError(
5716						f"section_chords({section_name!r}) is key-relative (degrees/romans) but no key "
5717						"resolves for it — set key= on the Composition, a form key (form(key=...)), or "
5718						f"a Section.key on every {section_name!r} section."
5719					)
5720				try:
5721					section_progression.resolve(ctx_key, ctx_scale or "ionian")
5722				except ValueError as error:
5723					raise ValueError(
5724						f"section_chords({section_name!r}) does not resolve against its effective key "
5725						f"{ctx_key} {ctx_scale or 'ionian'}: {error}"
5726					)
5727
5728		# min_energy with nothing feeding p.energy is a silent no-op — warn loudly.
5729		energy_gated = [p.builder_fn.__name__ for p in self._pending_patterns if p.min_energy is not None]
5730
5731		if energy_gated and not self._energy_map and not self._form_has_payload:
5732			logger.warning(
5733				f"min_energy is set on {', '.join(energy_gated)} but no energy source is "
5734				"configured — p.energy is always 0.5 (call composition.energy() or bind a "
5735				"Form whose Sections carry energy)"
5736			)
5737
5738		# The form clock MUST be registered before the harmonic clock: same-pulse
5739		# fixed callbacks fire in registration order (and all fixed callbacks fire
5740		# before callback sequences), and on a section-boundary bar the harmonic
5741		# clock reads the current section (via _get_section_progression) to decide
5742		# whether to walk that section's chords.  Registering harmony first would
5743		# make it read the OLD section on every boundary, shifting section_chords()
5744		# replays by one bar and bleeding them across sections.
5745		if self._form_state is not None:
5746
5747			await schedule_form(
5748				sequencer = self._sequencer,
5749				form_state = self._form_state,
5750				reschedule_lookahead = clock_lookahead,
5751				on_bar = self._check_transitions,
5752				# Re-read every bar so a mid-playback form() re-bind advances
5753				# the NEW state instead of the abandoned object.
5754				get_form_state = lambda: self._form_state,
5755			)
5756
5757		self._harmony_horizon.reset()
5758		self._harmonic_clock_started = False
5759
5760		if self._harmonic_state is not None or self._bound_progression is not None or self._section_progressions:
5761			await self._start_harmonic_clock(bar_beats, clock_lookahead)
5762
5763		# Bar counter - always active so p.bar is available to all builders.
5764		def _advance_builder_bar (pulse: int) -> None:
5765			self._builder_bar += 1
5766
5767		first_bar_pulse = int(self.time_signature[0] * self._sequencer.pulses_per_beat)
5768
5769		await self._sequencer.schedule_callback_repeating(
5770			callback = _advance_builder_bar,
5771			interval_beats = self.time_signature[0],
5772			start_pulse = first_bar_pulse,
5773			# Same raised lookahead as the form/harmony clocks: a pattern
5774			# rebuilding lookahead-early for its next cycle must read the bar
5775			# that cycle starts in, not the previous one.
5776			reschedule_lookahead = clock_lookahead
5777		)
5778
5779		# Run wait_for_initial=True scheduled functions and block until all complete.
5780		# This ensures composition.data is populated before patterns build.
5781		initial_tasks = [t for t in self._pending_scheduled if t.wait_for_initial]
5782
5783		if initial_tasks:
5784
5785			names = ", ".join(getattr(t.fn, '__name__', repr(t.fn)) for t in initial_tasks)
5786			logger.info(f"Waiting for initial scheduled {'function' if len(initial_tasks) == 1 else 'functions'} before start: {names}")
5787
5788			async def _run_initial (fn: typing.Callable) -> None:
5789
5790				accepts_ctx = _fn_has_parameter(fn, "p")
5791				ctx = ScheduleContext(cycle=0)
5792
5793				try:
5794					if inspect.iscoroutinefunction(fn):
5795						await (fn(ctx) if accepts_ctx else fn())
5796					else:
5797						loop = asyncio.get_running_loop()
5798						call = (lambda: fn(ctx)) if accepts_ctx else fn
5799						await loop.run_in_executor(None, call)
5800				except Exception as exc:
5801					logger.warning(f"Initial run of {getattr(fn, '__name__', repr(fn))!r} failed: {exc}")
5802
5803			await asyncio.gather(*[_run_initial(t.fn) for t in initial_tasks])
5804
5805		for pending_task in self._pending_scheduled:
5806
5807			accepts_ctx = _fn_has_parameter(pending_task.fn, "p")
5808
5809			# A wait_for_initial task already ran once as cycle 0 (the blocking
5810			# pre-roll above), so its repeating wrapper starts at cycle 1 — keeping
5811			# ScheduleContext.cycle monotonic across the initial and repeating runs.
5812			wrapped = _make_safe_callback(
5813				pending_task.fn,
5814				accepts_context = accepts_ctx,
5815				start_cycle = 1 if pending_task.wait_for_initial else 0,
5816			)
5817
5818			# wait_for_initial=True implies defer — no point firing at pulse 0
5819			# after the blocking run just completed.  defer=True skips the
5820			# backshift fire so the first repeating call happens one full cycle
5821			# later.
5822			if pending_task.wait_for_initial or pending_task.defer:
5823				start_pulse = int(pending_task.cycle_beats * self._sequencer.pulses_per_beat)
5824			else:
5825				start_pulse = 0
5826
5827			await self._sequencer.schedule_callback_repeating(
5828				callback = wrapped,
5829				interval_beats = pending_task.cycle_beats,
5830				start_pulse = start_pulse,
5831				reschedule_lookahead = pending_task.reschedule_lookahead
5832			)
5833
5834		# Build Pattern objects from pending registrations.
5835		patterns: typing.List[subsequence.pattern.Pattern] = []
5836
5837		for i, pending in enumerate(self._pending_patterns):
5838
5839			pattern = self._build_pattern_from_pending(pending)
5840			patterns.append(pattern)
5841
5842		await schedule_patterns(
5843			sequencer = self._sequencer,
5844			patterns = patterns,
5845			start_pulse = 0
5846		)
5847
5848		# Populate the running patterns dict for live hot-swap and mute/unmute.
5849		for i, pending in enumerate(self._pending_patterns):
5850			name = pending.builder_fn.__name__
5851			self._running_patterns[name] = patterns[i]
5852
5853		# Everything pending is running now; drop the declarations so a later
5854		# live reload cannot graduate stale copies.
5855		self._pending_patterns = []
5856
5857		if self._display is not None and not self._sequencer.render_mode:
5858			self._display.start()
5859			self._sequencer.on_event("bar",  self._display.update)
5860			self._sequencer.on_event("beat", self._display.update)
5861
5862		if self._live_server is not None:
5863			await self._live_server.start()
5864
5865		if self._osc_server is not None:
5866			await self._osc_server.start()
5867			self._sequencer.osc_server = self._osc_server
5868			self._sequencer.on_event("bar", self._broadcast_osc_status)
5869
5870		# Start keystroke listener if hotkeys are enabled and not in render mode.
5871		if self._hotkeys_enabled and not self._sequencer.render_mode:
5872			self._keystroke_listener = subsequence.keystroke.KeystrokeListener()
5873			self._keystroke_listener.start()
5874
5875			if self._keystroke_listener.active:
5876				# Listener started successfully — register the bar handler
5877				# and show all bindings so the user knows what's available.
5878				self._sequencer.on_event("bar", self._process_hotkeys)
5879				self._list_hotkeys()
5880			# If not active, KeystrokeListener.start() already logged a warning.
5881
5882		if self._web_ui_enabled and not self._sequencer.render_mode:
5883			self._web_ui_server = subsequence.web_ui.WebUI(self, http_host=self._web_ui_http_host, ws_host=self._web_ui_ws_host)
5884			self._web_ui_server.start()
5885
5886		try:
5887			await run_until_stopped(self._sequencer)
5888		finally:
5889			# Tear down every service even if run_until_stopped (or an earlier
5890			# stop) raised, and guard each individually, so one failure can't
5891			# strand the rest — most importantly the keystroke listener's
5892			# terminal restore.
5893			if self._web_ui_server is not None:
5894				try:
5895					self._web_ui_server.stop()
5896				except Exception:
5897					logger.exception("Error stopping web UI")
5898
5899			if self._live_server is not None:
5900				try:
5901					await self._live_server.stop()
5902				except Exception:
5903					logger.exception("Error stopping live server")
5904
5905			if self._live_reloader is not None:
5906				try:
5907					self._live_reloader.stop()
5908				except Exception:
5909					logger.exception("Error stopping live reloader")
5910
5911			if self._osc_server is not None:
5912				try:
5913					await self._osc_server.stop()
5914				except Exception:
5915					logger.exception("Error stopping OSC server")
5916				self._sequencer.osc_server = None
5917
5918			if self._display is not None:
5919				try:
5920					self._display.stop()
5921				except Exception:
5922					logger.exception("Error stopping display")
5923
5924			if self._keystroke_listener is not None:
5925				try:
5926					self._keystroke_listener.stop()
5927				except Exception:
5928					logger.exception("Error stopping keystroke listener")
5929				self._keystroke_listener = None
5930
5931	def _build_pattern_from_pending (self, pending: _PendingPattern, start_pulse: int = 0) -> subsequence.pattern.Pattern:
5932
5933		"""
5934		Create a Pattern from a pending registration using a temporary subclass.
5935
5936		The pattern's play stream is dealt here, keyed by NAME (crc32 of
5937		"seed:name" plus any reroll nonce), so registration order is
5938		irrelevant and a pattern added live gets exactly the stream it would
5939		have had at startup.  ``start_pulse`` anchors the first cycle on the
5940		beat axis so the initial build reads the harmony window at the right
5941		place (the sequencer keeps the anchor current on every reschedule).
5942		"""
5943
5944		composition_ref = self
5945		rng = self._stream(pending.builder_fn.__name__)
5946
5947		class _DecoratorPattern (subsequence.pattern.Pattern):
5948
5949			"""
5950			Pattern subclass that delegates to a builder function on each reschedule.
5951			"""
5952
5953			def __init__ (self, pending: _PendingPattern, pattern_rng: typing.Optional[random.Random] = None) -> None:
5954
5955				"""
5956				Initialize the decorator pattern from pending registration details.
5957				"""
5958
5959				super().__init__(
5960					channel = pending.channel,
5961					length = pending.length,
5962					reschedule_lookahead = pending.reschedule_lookahead,
5963					device = pending.device,
5964					mirrors = pending.mirrors,
5965				)
5966
5967				self._builder_fn = pending.builder_fn
5968				self._drum_note_map = pending.drum_note_map
5969				self._cc_name_map = pending.cc_name_map
5970				self._nrpn_name_map = pending.nrpn_name_map
5971				self._default_grid: int = pending.default_grid
5972				self._wants_chord = _fn_has_parameter(pending.builder_fn, "chord")
5973				self._cycle_count = 0
5974				self._rng = pattern_rng
5975				self._muted = False
5976				self._min_energy = pending.min_energy
5977				self._energy_gated = False
5978				self._voice_leading_state: typing.Optional[subsequence.voicings.VoiceLeadingState] = (
5979					subsequence.voicings.VoiceLeadingState() if pending.voice_leading else None
5980				)
5981				self._tweaks: typing.Dict[str, typing.Any] = {}
5982
5983				# Anchor of the cycle being built, on the absolute pulse axis.
5984				# The sequencer updates this on every reschedule; the initial
5985				# value is the pattern's first scheduled start.
5986				self._cycle_start_pulse = start_pulse
5987
5988				self._rebuild()
5989
5990			def _rebuild (self) -> None:
5991
5992				"""
5993				Clear steps and call the builder function to repopulate.
5994				"""
5995
5996				self.steps = {}
5997				self.cc_events = []
5998				self.osc_events = []
5999				self.raw_note_events = []
6000				current_cycle = self._cycle_count
6001				self._cycle_count += 1
6002
6003				# lock(): re-deal the stream from its effective seed every
6004				# rebuild so a locked pattern realizes identically each cycle.
6005				# Checked here (engine-side) so it survives live reload.
6006				if self._builder_fn.__name__ in composition_ref._locked_names:
6007					locked_seed = composition_ref._stream_seed(self._builder_fn.__name__)
6008					if locked_seed is not None:
6009						self._rng = random.Random(locked_seed)
6010
6011				if self._muted:
6012					return
6013
6014				section_info = composition_ref._form_state.get_section_info() if composition_ref._form_state else None
6015				energy = composition_ref._current_energy(section_info)
6016				effective_key, effective_scale = composition_ref._effective_key_scale(section_info)
6017
6018				# Automatic energy gating: below the threshold the pattern is
6019				# silent this cycle (composing with _muted — a performer mute
6020				# always wins).  Gate flips log once.
6021				if self._min_energy is not None:
6022					gated = energy < self._min_energy
6023
6024					if gated != self._energy_gated:
6025						state_word = "closed" if gated else "open"
6026						logger.info(
6027							f"Pattern '{self._builder_fn.__name__}': energy gate {state_word} "
6028							f"(energy {energy:.2f}, min_energy {self._min_energy:g})"
6029						)
6030						self._energy_gated = gated
6031
6032					if gated:
6033						return
6034
6035				# The harmony view for this cycle, anchored at its start beat —
6036				# under variable harmonic rhythm the window, not the engine's
6037				# mutating singleton, is the source of truth.
6038				harmony_view: typing.Optional[HarmonyView] = None
6039
6040				if not composition_ref._harmony_horizon.is_empty:
6041					origin_beat = self._cycle_start_pulse / composition_ref._sequencer.pulses_per_beat
6042					harmony_view = HarmonyView(composition_ref._harmony_horizon, origin_beat)
6043
6044				builder = subsequence.pattern_builder.PatternBuilder(
6045					pattern = self,
6046					cycle = current_cycle,
6047					drum_note_map = self._drum_note_map,
6048					cc_name_map = self._cc_name_map,
6049					nrpn_name_map = self._nrpn_name_map,
6050					section = section_info,
6051					bar = composition_ref._builder_bar,
6052					conductor = composition_ref.conductor,
6053					rng = self._rng,
6054					tweaks = self._tweaks,
6055					default_grid = self._default_grid,
6056					data = composition_ref.data,
6057					# The effective key/scale re-anchors key-relative content
6058					# (degrees, romans, generated material) to the section /
6059					# form / composition tier in force — mode travels too.
6060					key = effective_key,
6061					scale = effective_scale,
6062					time_signature = composition_ref.time_signature,
6063					held_notes = composition_ref._sequencer._held_notes,
6064					harmony = harmony_view,
6065					section_motifs = composition_ref._section_motifs,
6066					energy = energy,
6067				)
6068
6069				try:
6070
6071					if self._wants_chord:
6072
6073						# The two-parameter convention: the injected chord is
6074						# the cycle-start snapshot from the window (falling
6075						# back to the engine before the clock has run).
6076						chord = harmony_view.chord if harmony_view is not None else (
6077							composition_ref._harmonic_state.get_current_chord()
6078							if composition_ref._harmonic_state is not None else None
6079						)
6080
6081						if chord is not None:
6082							injected = _InjectedChord(
6083								chord,
6084								self._voice_leading_state,
6085								next_chord = harmony_view.next_chord if harmony_view is not None else None,
6086								beats_remaining = harmony_view.until_change if harmony_view is not None else None,
6087							)
6088							self._builder_fn(builder, injected)
6089						else:
6090							self._builder_fn(builder)
6091
6092					else:
6093						self._builder_fn(builder)
6094
6095				except Exception:
6096					# Discard whatever the builder placed before it raised —
6097					# otherwise a half-built pattern plays and the log lies.
6098					self.steps = {}
6099					self.cc_events = []
6100					self.osc_events = []
6101					self.raw_note_events = []
6102					logger.exception("Error in pattern builder '%s' (cycle %d) - pattern will be silent this cycle", self._builder_fn.__name__, current_cycle)
6103
6104				# Auto-apply global tuning if set and not already applied by the builder.
6105				if (
6106					composition_ref._tuning is not None
6107					and not builder._tuning_applied
6108					and not (composition_ref._tuning_exclude_drums and self._drum_note_map)
6109				):
6110					import subsequence.tuning as _tuning_mod
6111					_tuning_mod.apply_tuning_to_pattern(
6112						self,
6113						composition_ref._tuning,
6114						bend_range=composition_ref._tuning_bend_range,
6115						channels=composition_ref._tuning_channels,
6116						reference_note=composition_ref._tuning_reference_note,
6117					)
6118
6119			def on_reschedule (self) -> None:
6120
6121				"""
6122				Rebuild the pattern from the builder function before the next cycle.
6123				"""
6124
6125				self._rebuild()
6126
6127		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, scale: Optional[str] = None, seed: Optional[int] = None, record: bool = False, record_filename: Optional[str] = None, zero_indexed_channels: bool = False, latency_ms: float = 0.0)
1281	def __init__ (
1282		self,
1283		output_device: typing.Optional[str] = None,
1284		bpm: float = 120,
1285		time_signature: typing.Tuple[int, int] = (4, 4),
1286		key: typing.Optional[str] = None,
1287		scale: typing.Optional[str] = None,
1288		seed: typing.Optional[int] = None,
1289		record: bool = False,
1290		record_filename: typing.Optional[str] = None,
1291		zero_indexed_channels: bool = False,
1292		latency_ms: float = 0.0
1293	) -> None:
1294
1295		"""
1296		Initialize a new composition.
1297
1298		Parameters:
1299			output_device: Which MIDI output port to use, matched against
1300				``mido.get_output_names()``.  The name is treated as a
1301				pattern: ``*`` stands for any run of characters and ``?``
1302				for exactly one, matching is case-insensitive, and a name
1303				with no wildcards is simply a substring — so a plain
1304				``"Scarlett"`` finds the port without typing the rest.
1305				An exact name always wins outright.
1306
1307				Wildcards matter on Linux/ALSA, where names carry the
1308				client and port ids (e.g.
1309				``"Scarlett 2i4 USB:Scarlett 2i4 USB MIDI 1 16:0"``).  The
1310				client id — ``16`` here — is handed out in connection order
1311				and moves between reboots or when a virtual port is
1312				recreated, while the port index after it (``0``) stays put.
1313				Wildcard the one that moves and keep the one that does not::
1314
1315				    "*Scarlett 2i4 USB *:0"
1316
1317				Keep that trailing port index.  A multi-port interface
1318				reports one name per port, so ``"*U6MIDI Pro*"`` matches
1319				all three ports of a 3-port unit and asks which you meant
1320				at every launch, while ``"*U6MIDI Pro *:0"`` names one for
1321				good.  Prefer ``*`` to ``?`` — ``?`` matches a single
1322				character, so a pattern written for ``16:0`` quietly stops
1323				matching once ids reach three digits.  To look up the
1324				current names::
1325
1326				    import mido
1327				    for n in mido.get_output_names(): print(n)
1328
1329				If ``None``, Subsequence auto-discovers — uses the only
1330				available device, or prompts to choose if several exist.
1331			bpm: Initial tempo in beats per minute (default 120).
1332			time_signature: The metre as ``(beats, unit)``, default ``(4, 4)``.
1333				Sets the bar length everywhere bars matter: ``bars=`` pattern
1334				lengths, ``p.bar``/``p.signal()``, form advancement and
1335				transitions, and pinned-chord bar numbers.
1336			key: The root key of the piece (e.g., "C", "F#", "Bb").
1337				Required if you plan to use ``harmony()``.
1338			scale: The scale/mode of the piece (e.g. "minor", "dorian",
1339				or any registered scale name).  Used to resolve scale
1340				degrees in motifs; defaults to major (ionian) when unset.
1341			seed: An optional integer for deterministic randomness. When set,
1342				every random decision (chord choices, drum probability, etc.)
1343				will be identical on every run.
1344			record: When True, record all MIDI events to a file.
1345			record_filename: Optional filename for the recording (defaults to timestamp).
1346			zero_indexed_channels: When False (default), MIDI channels use
1347				1-based numbering (1-16) matching instrument labelling.
1348				Channel 10 is drums, the way musicians and hardware panels
1349				show it. When True, channels use 0-based numbering (0-15)
1350				matching the raw MIDI protocol.
1351			latency_ms: Physical output latency of the primary device in
1352				milliseconds, for delay compensation (default 0.0, must be
1353				non-negative). Set this when the primary output sounds late
1354				(e.g. a software sampler) so Subsequence delays faster
1355				devices to line everything up. See ``midi_output()`` for
1356				additional devices.
1357
1358		Example:
1359			```python
1360			comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
1361			```
1362		"""
1363
1364		if latency_ms < 0:
1365			raise ValueError(f"latency_ms must be non-negative — got {latency_ms}")
1366
1367		self.output_device = output_device
1368		self.bpm = bpm
1369		self.time_signature = time_signature
1370		self.key = key
1371		self.scale = scale
1372		self._seed: typing.Optional[int] = seed
1373		self._zero_indexed_channels: bool = zero_indexed_channels
1374		self._output_latency_ms: float = latency_ms
1375
1376		# Determinism plumbing: named-stream derivation state.  Build-time
1377		# consumers draw per-call-salted streams (freeze:1, harmony:2, ...) so
1378		# adding one call never shifts another's stream; play-time pattern
1379		# streams are name-keyed in _build_pattern_from_pending.
1380		self._freeze_count: int = 0
1381		self._harmony_count: int = 0
1382		self._form_count: int = 0
1383		self._reroll_nonces: typing.Dict[str, int] = {}
1384		self._locked_names: typing.Set[str] = set()
1385
1386		self._sequencer = subsequence.sequencer.Sequencer(
1387			output_device_name = output_device,
1388			initial_bpm = bpm,
1389			time_signature = time_signature,
1390			record = record,
1391			record_filename = record_filename
1392		)
1393
1394		self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None
1395		self._harmony_cycle_beats: typing.Optional[int] = None
1396		self._harmony_style: typing.Optional[str] = None
1397		# The style (name or ChordGraph) from the most recent style-configuring
1398		# harmony() call — reused by parameter-only re-calls.
1399		self._last_harmony_style: typing.Optional[typing.Union[str, subsequence.chord_graphs.ChordGraph]] = None
1400		self._harmony_reschedule_lookahead: float = 1
1401		self._section_progressions: typing.Dict[str, Progression] = {}
1402		self._bound_progression: typing.Optional[Progression] = None
1403		self._pinned_chords: typing.Dict[int, typing.Any] = {}
1404		self._cadence_requests: typing.Dict[int, str] = {}
1405		self._section_cadences: typing.Dict[str, str] = {}
1406		self._harmony_horizon = _HarmonyHorizon()
1407		# True once the span-walking clock is registered for this playback —
1408		# lets a first mid-playback harmony() call start it exactly once.
1409		self._harmonic_clock_started: bool = False
1410		self._section_motifs: typing.Dict[typing.Tuple[str, typing.Optional[str]], typing.Any] = {}
1411		self._energy_map: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]] = {}
1412		self._form_has_payload: bool = False
1413		self._form_key: typing.Optional[str] = None
1414		self._form_scale: typing.Optional[str] = None
1415		# Cache of section progressions resolved against an effective key/scale
1416		# (key-relative section harmony re-keys per occurrence; resolution is a
1417		# pure function of (content, key, scale), so this is just memoisation).
1418		self._resolved_section_cache: typing.Dict[typing.Tuple[str, typing.Optional[str], typing.Optional[str]], Progression] = {}
1419		self._transitions: typing.List[_Transition] = []
1420		self._transition_muted: typing.Set[str] = set()
1421		self._pending_patterns: typing.List[_PendingPattern] = []
1422		# Names of patterns declared by the most recent live-reload exec (added by
1423		# pattern()/layer() as they run); the deletion diff in _apply_source_async
1424		# compares this against the same source's PREVIOUS exec.
1425		self._declared_names: typing.Set[str] = set()
1426		# Per-source declaration history: source label/path → the names it
1427		# declared last time it was exec'd.  The deletion diff unregisters only
1428		# names a source used to declare and no longer does — never patterns
1429		# registered by the wrapper script or by another watched source.
1430		self._source_declared: typing.Dict[str, typing.Set[str]] = {}
1431		self._pending_scheduled: typing.List[_PendingScheduled] = []
1432		self._form_state: typing.Optional[subsequence.form_state.FormState] = None
1433		self._builder_bar: int = 0
1434		self._display: typing.Optional[subsequence.display.Display] = None
1435		self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None
1436		self._live_reloader: typing.Optional[subsequence.live_reloader.LiveReloader] = None
1437		self._is_live: bool = False
1438		self._running_patterns: typing.Dict[str, typing.Any] = {}
1439		self._input_device: typing.Optional[str] = None
1440		self._input_device_alias: typing.Optional[str] = None
1441		self._clock_follow: bool = False
1442		self._clock_output: bool = False
1443		self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = []
1444		self._cc_forwards: typing.List[typing.Dict[str, typing.Any]] = []
1445		# Held-note input config from note_input() (None = not declared).
1446		self._note_input: typing.Optional[typing.Dict[str, typing.Any]] = None
1447		# Additional output devices registered with midi_output() after construction.
1448		self._additional_outputs: typing.List[_AdditionalOutput] = []
1449		# Additional input devices: (device_name: str, alias: Optional[str], clock_follow: bool)
1450		self._additional_inputs: typing.List[typing.Tuple[str, typing.Optional[str], bool]] = []
1451		# Maps alias/name → output device index (populated in _run after all devices are opened).
1452		self._output_device_names: typing.Dict[str, int] = {}
1453		# Maps alias/name → input device index (populated in _run after all input devices are opened).
1454		self._input_device_names: typing.Dict[str, int] = {}
1455		self.data: typing.Dict[str, typing.Any] = {}
1456		self._osc_server: typing.Optional[subsequence.osc.OscServer] = None
1457		self.conductor = subsequence.conductor.Conductor()
1458		self._web_ui_enabled: bool = False
1459		self._web_ui_http_host: str = "127.0.0.1"
1460		self._web_ui_ws_host: str = "127.0.0.1"
1461		self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None
1462		self._link_quantum: typing.Optional[float] = None
1463
1464		# Hotkey state — populated by hotkeys() and hotkey().
1465		self._hotkeys_enabled: bool = False
1466		self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {}
1467		self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = []
1468		self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None
1469
1470		# Tuning state — populated by tuning().
1471		self._tuning: typing.Optional[typing.Any] = None       # subsequence.tuning.Tuning
1472		self._tuning_bend_range: float = 2.0
1473		self._tuning_channels: typing.Optional[typing.List[int]] = None
1474		self._tuning_reference_note: int = 60
1475		self._tuning_exclude_drums: bool = True

Initialize a new composition.

Arguments:
  • output_device: Which MIDI output port to use, matched against mido.get_output_names(). The name is treated as a pattern: * stands for any run of characters and ? for exactly one, matching is case-insensitive, and a name with no wildcards is simply a substring — so a plain "Scarlett" finds the port without typing the rest. An exact name always wins outright.

    Wildcards matter on Linux/ALSA, where names carry the client and port ids (e.g. "Scarlett 2i4 USB:Scarlett 2i4 USB MIDI 1 16:0"). The client id — 16 here — is handed out in connection order and moves between reboots or when a virtual port is recreated, while the port index after it (0) stays put. Wildcard the one that moves and keep the one that does not::

    "*Scarlett 2i4 USB *:0"
    

    Keep that trailing port index. A multi-port interface reports one name per port, so "*U6MIDI Pro*" matches all three ports of a 3-port unit and asks which you meant at every launch, while "*U6MIDI Pro *:0" names one for good. Prefer * to ?? matches a single character, so a pattern written for 16:0 quietly stops matching once ids reach three digits. To look up the current names::

    import mido
    for n in mido.get_output_names(): print(n)
    

    If None, Subsequence auto-discovers — uses the only available device, or prompts to choose if several exist.

  • bpm: Initial tempo in beats per minute (default 120).
  • time_signature: The metre as (beats, unit), default (4, 4). Sets the bar length everywhere bars matter: bars= pattern lengths, p.bar/p.signal(), form advancement and transitions, and pinned-chord bar numbers.
  • key: The root key of the piece (e.g., "C", "F#", "Bb"). Required if you plan to use harmony().
  • scale: The scale/mode of the piece (e.g. "minor", "dorian", or any registered scale name). Used to resolve scale degrees in motifs; defaults to major (ionian) when unset.
  • seed: An optional integer for deterministic randomness. When set, every random decision (chord choices, drum probability, etc.) will be identical on every run.
  • record: When True, record all MIDI events to a file.
  • record_filename: Optional filename for the recording (defaults to timestamp).
  • zero_indexed_channels: When False (default), MIDI channels use 1-based numbering (1-16) matching instrument labelling. Channel 10 is drums, the way musicians and hardware panels show it. When True, channels use 0-based numbering (0-15) matching the raw MIDI protocol.
  • latency_ms: Physical output latency of the primary device in milliseconds, for delay compensation (default 0.0, must be non-negative). Set this when the primary output sounds late (e.g. a software sampler) so Subsequence delays faster devices to line everything up. See midi_output() for additional devices.
Example:
comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
output_device
bpm
time_signature
key
scale
data: Dict[str, Any]
conductor
harmonic_state: Optional[subsequence.harmonic_state.HarmonicState]
1673	@property
1674	def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]:
1675		"""The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called."""
1676		return self._harmonic_state

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

def current_chord(self) -> Optional[Any]:
1678	def current_chord (self) -> typing.Optional[typing.Any]:
1679
1680		"""The chord sounding at the playhead, or ``None`` without harmony.
1681
1682		Reads the harmony window at the current pulse, so it stays accurate
1683		under variable harmonic rhythm and clock lookahead (the engine's
1684		``current_chord`` flips *lookahead* beats early — this does not).
1685		Falls back to the engine's chord before playback starts.  The chord
1686		may be a decorated wrapper (``Am9``, ``C/G``) when the sounding span
1687		is spiced; it duck-types the ``Chord`` voicing protocol either way.
1688		"""
1689
1690		if not self._harmony_horizon.is_empty:
1691			beat = self._sequencer.pulse_count / self._sequencer.pulses_per_beat
1692			chord = self._harmony_horizon.chord_at(beat)
1693			if chord is not None:
1694				return chord
1695
1696		if self._harmonic_state is not None:
1697			return self._harmonic_state.get_current_chord()
1698
1699		return None

The chord sounding at the playhead, or None without harmony.

Reads the harmony window at the current pulse, so it stays accurate under variable harmonic rhythm and clock lookahead (the engine's current_chord flips lookahead beats early — this does not). Falls back to the engine's chord before playback starts. The chord may be a decorated wrapper (Am9, C/G) when the sounding span is spiced; it duck-types the Chord voicing protocol either way.

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

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

sequencer: subsequence.sequencer.Sequencer
1796	@property
1797	def sequencer (self) -> subsequence.sequencer.Sequencer:
1798		"""The underlying ``Sequencer`` instance."""
1799		return self._sequencer

The underlying Sequencer instance.

running_patterns: Dict[str, Any]
1801	@property
1802	def running_patterns (self) -> typing.Dict[str, typing.Any]:
1803		"""The currently active patterns, keyed by name."""
1804		return self._running_patterns

The currently active patterns, keyed by name.

builder_bar: int
1806	@property
1807	def builder_bar (self) -> int:
1808		"""Current bar index used by pattern builders."""
1809		return self._builder_bar

Current bar index used by pattern builders.

def harmony( self, style: Union[str, subsequence.chord_graphs.ChordGraph, NoneType] = None, cycle_beats: int = 4, dominant_7th: bool = True, gravity: float = 1.0, nir_strength: float = 0.5, minor_turnaround_weight: float = 0.0, root_diversity: float = 0.4, reschedule_lookahead: float = 1, progression: Optional[Any] = None) -> None:
1857	def harmony (
1858		self,
1859		style: typing.Optional[typing.Union[str, subsequence.chord_graphs.ChordGraph]] = None,
1860		cycle_beats: int = 4,
1861		dominant_7th: bool = True,
1862		gravity: float = 1.0,
1863		nir_strength: float = 0.5,
1864		minor_turnaround_weight: float = 0.0,
1865		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1866		reschedule_lookahead: float = 1,
1867		progression: typing.Optional[typing.Any] = None,
1868	) -> None:
1869
1870		"""
1871		Configure the harmonic logic and chord change intervals.
1872
1873		Two sources, combinable: a **bound progression** (``progression=`` — a
1874		:class:`Progression` value, an element list like ``[1, 6, 3, "bVII7"]``,
1875		or chord names) walked span by span on the global clock; and/or a
1876		**graph style** stepping live chords.  With only a progression bound,
1877		it loops on exhaustion; with a style configured too, exhaustion falls
1878		through to live stepping (the frozen-replay bridge).  Calling with
1879		neither argument keeps today's default live engine
1880		(``style="functional_major"``).
1881
1882		Parameters:
1883			style: The harmonic style to use. Built-in: "functional_major"
1884				(alias "diatonic_major"), "hooktheory_major" (alias
1885				"pop_major"), "turnaround", "aeolian_minor",
1886				"phrygian_minor", "lydian_major", "dorian_minor",
1887				"chromatic_mediant", "suspended", "mixolydian", "whole_tone",
1888				"diminished". See README for full descriptions.
1889			cycle_beats: How many beats each live chord lasts (default 4).
1890				Bound progressions carry their own harmonic rhythm in their
1891				spans, so this applies to live stepping only.  A re-call
1892				during playback takes effect from the next chord boundary;
1893				a FIRST harmony() call mid-playback starts the clock itself.
1894			dominant_7th: Whether to include V7 chords (default True).
1895			gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
1896			nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement
1897				expectations.
1898			minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
1899			root_diversity: Root-repetition damping (0.0 to 1.0). Each recent
1900				chord sharing a candidate's root reduces the weight to 40% at
1901				the default (0.4). Set to 1.0 to disable.
1902			reschedule_lookahead: How many beats in advance to calculate the
1903				next chord.
1904			progression: A progression to bind to the global clock.  Key-
1905				relative content resolves now, against the composition key
1906				and scale (binding freezes one realisation).
1907
1908		Example:
1909			```python
1910			# A moody minor progression that changes every 8 beats
1911			comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
1912
1913			# Manual harmony driving everything — loops forever
1914			comp.harmony(progression=subsequence.progression([1, 6, 3, 7]))
1915			```
1916		"""
1917
1918		if style is None and progression is None:
1919			# A parameter-only re-call (gravity=, cycle_beats=, ...) keeps the
1920			# configured style — defaulting unconditionally here would silently
1921			# replace e.g. aeolian_minor with functional_major.
1922			style = self._last_harmony_style if self._last_harmony_style is not None else "functional_major"
1923
1924		if style is not None:
1925
1926			if self.key is None:
1927				raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor")
1928
1929			preserved_history: typing.List[subsequence.chords.Chord] = []
1930			preserved_current: typing.Optional[subsequence.chords.Chord] = None
1931
1932			if self._harmonic_state is not None:
1933				preserved_history = self._harmonic_state.history.copy()
1934				preserved_current = self._harmonic_state.current_chord
1935
1936			# Per-call salted build stream (harmony:1, harmony:2, ...): a re-call
1937			# gets its own deterministic stream while history and current chord
1938			# are preserved above, and adding a re-call never shifts any other
1939			# consumer's stream.
1940			self._harmony_count += 1
1941
1942			self._harmonic_state = subsequence.harmonic_state.HarmonicState(
1943				key_name = self.key,
1944				graph_style = style,
1945				include_dominant_7th = dominant_7th,
1946				key_gravity_blend = gravity,
1947				nir_strength = nir_strength,
1948				minor_turnaround_weight = minor_turnaround_weight,
1949				root_diversity = root_diversity,
1950				rng = self._stream(f"harmony:{self._harmony_count}")
1951			)
1952
1953			if preserved_history:
1954				self._harmonic_state.history = preserved_history
1955			if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current):
1956				self._harmonic_state.current_chord = preserved_current
1957
1958			self._harmony_style = style if isinstance(style, str) else None
1959			self._last_harmony_style = style
1960
1961		if progression is not None:
1962			self._bound_progression = self._coerce_progression(progression, "harmony(progression=)")
1963
1964		self._harmony_cycle_beats = cycle_beats
1965		self._harmony_reschedule_lookahead = reschedule_lookahead
1966
1967		# A re-call invalidates whatever the horizon had planned.
1968		self._harmony_horizon.invalidate_future()
1969
1970		# A FIRST harmony() call mid-playback must start the clock itself —
1971		# _run() only schedules clocks for sources it can see at play() time.
1972		# (Re-calls need nothing here: the clock reads its sources through
1973		# getters on every tick.)
1974		loop = self._sequencer._event_loop
1975
1976		if loop is not None and loop.is_running() and not self._harmonic_clock_started:
1977			try:
1978				on_loop = asyncio.get_running_loop() is loop
1979			except RuntimeError:
1980				on_loop = False
1981
1982			if on_loop:
1983				loop.create_task(self._start_harmonic_clock())
1984			else:
1985				asyncio.run_coroutine_threadsafe(self._start_harmonic_clock(), loop)

Configure the harmonic logic and chord change intervals.

Two sources, combinable: a bound progression (progression= — a Progression value, an element list like [1, 6, 3, "bVII7"], or chord names) walked span by span on the global clock; and/or a graph style stepping live chords. With only a progression bound, it loops on exhaustion; with a style configured too, exhaustion falls through to live stepping (the frozen-replay bridge). Calling with neither argument keeps today's default live engine (style="functional_major").

Arguments:
  • style: The harmonic style to use. Built-in: "functional_major" (alias "diatonic_major"), "hooktheory_major" (alias "pop_major"), "turnaround", "aeolian_minor", "phrygian_minor", "lydian_major", "dorian_minor", "chromatic_mediant", "suspended", "mixolydian", "whole_tone", "diminished". See README for full descriptions.
  • cycle_beats: How many beats each live chord lasts (default 4). Bound progressions carry their own harmonic rhythm in their spans, so this applies to live stepping only. A re-call during playback takes effect from the next chord boundary; a FIRST harmony() call mid-playback starts the clock itself.
  • dominant_7th: Whether to include V7 chords (default True).
  • gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
  • nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement expectations.
  • minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
  • root_diversity: Root-repetition damping (0.0 to 1.0). Each recent chord sharing a candidate's root reduces the weight to 40% at the default (0.4). Set to 1.0 to disable.
  • reschedule_lookahead: How many beats in advance to calculate the next chord.
  • progression: A progression to bind to the global clock. Key- relative content resolves now, against the composition key and scale (binding freezes one realisation).
Example:
# A moody minor progression that changes every 8 beats
comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)

# Manual harmony driving everything — loops forever
comp.harmony(progression=subsequence.progression([1, 6, 3, 7]))
def freeze( self, bars: int, end: Optional[Any] = None, pins: Optional[Dict[int, Any]] = None, avoid: Optional[Sequence[Any]] = None, cadence: Optional[str] = None) -> Progression:
2065	def freeze (
2066		self,
2067		bars: int,
2068		end: typing.Optional[typing.Any] = None,
2069		pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
2070		avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
2071		cadence: typing.Optional[str] = None,
2072	) -> "Progression":
2073
2074		"""Capture a chord progression from the live harmony engine.
2075
2076		Runs the harmony engine forward by *bars* chord changes, records each
2077		chord, and returns it as a :class:`Progression` that can be bound to a
2078		form section with :meth:`section_chords`.
2079
2080		The engine state **advances** — successive ``freeze()`` calls produce a
2081		continuing compositional journey so section progressions feel like parts
2082		of a whole rather than isolated islands.
2083
2084		The hybrid constraints compile into the walk: ``end=`` fixes the last
2085		bar ("end on V at bar 8"), ``pins=`` fix any 1-based bar, ``avoid=``
2086		excludes chords throughout.  Specs follow the progression-element
2087		grammar (ints where diatonic, roman/name strings where chromatic) and
2088		resolve against the composition key and scale.  A backward
2089		feasibility pass guarantees satisfiability before any chord is drawn;
2090		the forward walk keeps the engine's real history-dependent weighting.
2091		Bar 1 is always the engine's current chord — the journey continues —
2092		so ``pins={1: ...}`` may only name it redundantly.
2093
2094		Parameters:
2095			bars: Number of chords to capture (one per harmony cycle).
2096			end: The chord at the final bar — ``end="V"`` is the cadential
2097				major dominant in minor.
2098			pins: ``{bar: chord}`` — 1-based fiat positions.
2099			avoid: Chords excluded from the walk.
2100			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
2101				``"fakeout"``, theory aliases accepted) — its formula pins
2102				the final bars, so the walk approaches the close.
2103				Conflicts with ``end=`` or pins on those bars.
2104
2105		Returns:
2106			A :class:`Progression` with the captured chords and trailing
2107			history for NIR continuity.
2108
2109		Raises:
2110			ValueError: If :meth:`harmony` has not been called first, or the
2111				constraints are contradictory or unsatisfiable.
2112
2113		Example::
2114
2115			composition.harmony(style="functional_major", cycle_beats=4)
2116			verse  = composition.freeze(8, end="V")   # the verse sets up the chorus
2117			chorus = composition.freeze(4)            # next 4 chords, continuing on
2118			composition.section_chords("verse",  verse)
2119			composition.section_chords("chorus", chorus)
2120		"""
2121
2122		hs = self._require_harmonic_state()
2123
2124		if bars < 1:
2125			raise ValueError("bars must be at least 1")
2126
2127		if cadence is not None:
2128			pins = subsequence.progressions.cadence_pins(cadence, bars, pins, end)
2129			end = None
2130
2131		scale = self._constraint_scale()
2132		key_pc = subsequence.chords.key_name_to_pc(self.key) if self.key is not None else hs.key_root_pc
2133
2134		resolved_pins = {
2135			position: subsequence.progressions.resolve_constraint(spec, key_pc, scale, f"pins[{position}]")
2136			for position, spec in (pins or {}).items()
2137		}
2138		resolved_end = subsequence.progressions.resolve_constraint(end, key_pc, scale, "end") if end is not None else None
2139		resolved_avoid = [subsequence.progressions.resolve_constraint(spec, key_pc, scale, "avoid") for spec in (avoid or [])]
2140
2141		if 1 in resolved_pins and resolved_pins[1] != hs.current_chord:
2142			raise ValueError(
2143				f"pins[1]={resolved_pins[1].name()} conflicts with the engine's current chord "
2144				f"({hs.current_chord.name()}) — bar 1 of a freeze continues the journey; "
2145				"pin a later bar, or use pin_chord() for playback fiat"
2146			)
2147
2148		# Per-call salted stream (freeze:1, freeze:2, ...): each call's draws
2149		# are independent of every other consumer, so frozen progressions are
2150		# reproducible WITHOUT play() and adding a call cannot shift a
2151		# neighbour's output.  Engine state still advances normally — chord
2152		# continuity comes from current_chord/history, randomness from the
2153		# salted stream (swap-and-restore keeps hs.rng for play untouched).
2154		self._freeze_count += 1
2155		stream = self._stream(f"freeze:{self._freeze_count}")
2156		saved_rng = hs.rng
2157
2158		if stream is not None:
2159			hs.rng = stream
2160
2161		try:
2162			# The kernel with the engine's own hooks is draw-for-draw the old
2163			# step() loop when unconstrained — one walk path for both.
2164			def _commit (chosen: subsequence.chords.Chord) -> None:
2165				hs.current_chord = chosen
2166
2167			collected = subsequence.sequence_utils.constrained_walk(
2168				hs.graph,
2169				hs.current_chord,
2170				bars,
2171				rng = hs.rng,
2172				pins = resolved_pins,
2173				end = resolved_end,
2174				avoid = resolved_avoid,
2175				weight_modifier = hs._transition_weight,
2176				before_choice = hs._record_transition_source,
2177				after_choice = _commit,
2178			)
2179
2180			# Advance past the last captured chord so the next freeze() call or
2181			# live playback does not duplicate it.
2182			hs.step()
2183
2184		finally:
2185			hs.rng = saved_rng
2186
2187		span_beats = float(self._harmony_cycle_beats or 4)
2188
2189		return Progression(
2190			spans = tuple(
2191				subsequence.progressions.ChordSpan(chord = chord, beats = span_beats)
2192				for chord in collected
2193			),
2194			trailing_history = tuple(hs.history),
2195		)

Capture a chord progression from the live harmony engine.

Runs the harmony engine forward by bars chord changes, records each chord, and returns it as a Progression that can be bound to a form section with section_chords().

The engine state advances — successive freeze() calls produce a continuing compositional journey so section progressions feel like parts of a whole rather than isolated islands.

The hybrid constraints compile into the walk: end= fixes the last bar ("end on V at bar 8"), pins= fix any 1-based bar, avoid= excludes chords throughout. Specs follow the progression-element grammar (ints where diatonic, roman/name strings where chromatic) and resolve against the composition key and scale. A backward feasibility pass guarantees satisfiability before any chord is drawn; the forward walk keeps the engine's real history-dependent weighting. Bar 1 is always the engine's current chord — the journey continues — so pins={1: ...} may only name it redundantly.

Arguments:
  • bars: Number of chords to capture (one per harmony cycle).
  • end: The chord at the final bar — end="V" is the cadential major dominant in minor.
  • pins: {bar: chord} — 1-based fiat positions.
  • avoid: Chords excluded from the walk.
  • cadence: A cadence name ("strong"/"soft"/"open"/ "fakeout", theory aliases accepted) — its formula pins the final bars, so the walk approaches the close. Conflicts with end= or pins on those bars.
Returns:

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

Raises:
  • ValueError: If harmony() has not been called first, or the constraints are contradictory or unsatisfiable.

Example::

    composition.harmony(style="functional_major", cycle_beats=4)
    verse  = composition.freeze(8, end="V")   # the verse sets up the chorus
    chorus = composition.freeze(4)            # next 4 chords, continuing on
    composition.section_chords("verse",  verse)
    composition.section_chords("chorus", chorus)
def section_chords(self, section_name: str, progression: Any) -> None:
2197	def section_chords (self, section_name: str, progression: typing.Any) -> None:
2198
2199		"""Bind a :class:`Progression` to a named form section.
2200
2201		Every time *section_name* plays, the harmonic clock walks the
2202		progression's spans instead of calling the live engine.  Sections
2203		without a bound progression continue generating live chords.
2204
2205		Accepts a :class:`Progression` value (from :meth:`freeze`, the
2206		``progression()`` factory, or hand-built) or anything the factory
2207		accepts — an element list like ``[1, 6, 3, "bVII7"]`` or chord
2208		names.
2209
2210		**Key-relative content re-keys per occurrence.**  A progression
2211		written in degrees or romans is *key-relative* content: it resolves
2212		late, each time the section plays, against that section's effective
2213		key and scale (``Section.key`` > form key > composition key, with
2214		mode following the same chain).  So a ``Section(key="A")`` plays the
2215		same numbered progression a tone higher — its chords and its degrees
2216		share one tonic.  *Absolute* content — chord names (``"Am"``),
2217		:class:`~subsequence.progressions.PitchSet`, and frozen captures from
2218		:meth:`freeze` — names exact chords and is never transposed by a key.
2219
2220		On exhaustion mid-section the progression loops when no graph style
2221		is configured (and always when it contains a ``PitchSet``); with a
2222		live engine, exhaustion **falls through to live stepping in the
2223		COMPOSITION key** — the live graph engine does not transpose for a
2224		section (a stateful walk does not modulate mid-stream), so a
2225		re-keyed section that runs out of written chords hands off to
2226		composition-key harmony.  Bind a full-length progression (or set
2227		``at_end``/loop intent) if you need the whole section in its key.
2228
2229		Parameters:
2230			section_name: Name of the section as defined in :meth:`form`.
2231			progression: The progression to bind.
2232
2233		Raises:
2234			ValueError: If a graph-based form has been configured and
2235				*section_name* is not one of its sections.  List and generator
2236				forms yield names lazily, so they cannot be validated here.
2237				(A key-relative progression with no resolvable key for the
2238				section is caught at :meth:`play`/:meth:`render`, once the
2239				form's keys are known.)
2240
2241		Example::
2242
2243			composition.section_chords("verse",  verse_progression)
2244			composition.section_chords("chorus", [1, 6, 3, 7])
2245			# "bridge" is not bound — it generates live chords
2246		"""
2247
2248		if (
2249			self._form_state is not None
2250			and self._form_state._section_bars is not None
2251			and section_name not in self._form_state._section_bars
2252		):
2253			known = ", ".join(sorted(self._form_state._section_bars))
2254			raise ValueError(
2255				f"Section '{section_name}' not found in form. "
2256				f"Known sections: {known}"
2257			)
2258
2259		self._section_progressions[section_name] = self._coerce_section_progression(progression)
2260		self._resolved_section_cache = {}
2261		self._harmony_horizon.invalidate_future()

Bind a Progression to a named form section.

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

Accepts a Progression value (from freeze(), the progression() factory, or hand-built) or anything the factory accepts — an element list like [1, 6, 3, "bVII7"] or chord names.

Key-relative content re-keys per occurrence. A progression written in degrees or romans is key-relative content: it resolves late, each time the section plays, against that section's effective key and scale (Section.key > form key > composition key, with mode following the same chain). So a Section(key="A") plays the same numbered progression a tone higher — its chords and its degrees share one tonic. Absolute content — chord names ("Am"), ~subsequence.progressions.PitchSet, and frozen captures from freeze() — names exact chords and is never transposed by a key.

On exhaustion mid-section the progression loops when no graph style is configured (and always when it contains a PitchSet); with a live engine, exhaustion falls through to live stepping in the COMPOSITION key — the live graph engine does not transpose for a section (a stateful walk does not modulate mid-stream), so a re-keyed section that runs out of written chords hands off to composition-key harmony. Bind a full-length progression (or set at_end/loop intent) if you need the whole section in its key.

Arguments:
  • section_name: Name of the section as defined in form().
  • progression: The progression to bind.
Raises:
  • ValueError: If a graph-based form has been configured and section_name is not one of its sections. List and generator forms yield names lazily, so they cannot be validated here. (A key-relative progression with no resolvable key for the section is caught at play()/render(), once the form's keys are known.)

Example::

    composition.section_chords("verse",  verse_progression)
    composition.section_chords("chorus", [1, 6, 3, 7])
    # "bridge" is not bound — it generates live chords
def pin_chord(self, bar: int, chord: Optional[Any]) -> None:
2263	def pin_chord (self, bar: int, chord: typing.Optional[typing.Any]) -> None:
2264
2265		"""Force the chord sounding at a bar — fiat over live generation.
2266
2267		Whatever the harmonic source (live walk, bound progression, section
2268		progression) produces for *bar*, the pinned chord overrides it.
2269		Pass ``None`` to remove a pin.
2270
2271		Parameters:
2272			bar: 1-based bar number (the musician count).
2273			chord: A chord name, int degree, roman string, ``Chord``,
2274				``PitchSet``, or ``None`` to unpin.  A **key-relative** spec
2275				(int degree, roman) re-keys like section harmony: it resolves
2276				late, against the effective key of the section sounding at
2277				that bar (so ``pin_chord(8, "V")`` is the dominant of
2278				wherever bar 8 lands).  A **concrete** spec (name, ``Chord``,
2279				``PitchSet``) is absolute and never moves.
2280
2281		Example::
2282
2283			composition.pin_chord(8, "E7")    # the turnaround lands on E7
2284			composition.pin_chord(8, "V")     # the dominant of bar 8's section
2285			composition.pin_chord(8, None)    # let it walk again
2286		"""
2287
2288		if not isinstance(bar, int) or isinstance(bar, bool) or bar < 1:
2289			raise ValueError(f"bars are 1-based ints, got {bar!r}")
2290
2291		if chord is None:
2292			self._pinned_chords.pop(bar, None)
2293		else:
2294			# Store the parsed span — relative pins resolve late (per section)
2295			# at the clock; concrete pins are absolute.
2296			span = subsequence.progressions.parse_element(chord, beats = float(self.time_signature[0]))
2297
2298			if not span.is_concrete:
2299				# Raise early only when no key is resolvable for this bar — the
2300				# bar's own section (sequence forms) may supply one even with no
2301				# composition/form key.
2302				probe_info = self._form_state.section_info_at_bar(bar) if self._form_state is not None else None
2303				probe_key, _ = self._effective_key_scale(probe_info)
2304				if probe_key is None:
2305					raise ValueError(
2306						"pin_chord with a key-relative spec (degree/roman) needs a key — set key= on "
2307						"the Composition, a form key, or a Section.key for that bar (the pin re-keys "
2308						"to the section's effective key)"
2309					)
2310
2311			self._pinned_chords[bar] = span
2312
2313		self._harmony_horizon.invalidate_future()

Force the chord sounding at a bar — fiat over live generation.

Whatever the harmonic source (live walk, bound progression, section progression) produces for bar, the pinned chord overrides it. Pass None to remove a pin.

Arguments:
  • bar: 1-based bar number (the musician count).
  • chord: A chord name, int degree, roman string, Chord, PitchSet, or None to unpin. A key-relative spec (int degree, roman) re-keys like section harmony: it resolves late, against the effective key of the section sounding at that bar (so pin_chord(8, "V") is the dominant of wherever bar 8 lands). A concrete spec (name, Chord, PitchSet) is absolute and never moves.

Example::

    composition.pin_chord(8, "E7")    # the turnaround lands on E7
    composition.pin_chord(8, "V")     # the dominant of bar 8's section
    composition.pin_chord(8, None)    # let it walk again
def request_cadence(self, cadence: str = 'strong', bar: Optional[int] = None) -> None:
2355	def request_cadence (self, cadence: str = "strong", bar: typing.Optional[int] = None) -> None:
2356
2357		"""Ask the live engine to approach a cadence arriving at a bar.
2358
2359		The request hook: where :meth:`pin_chord` is fiat, this is a
2360		*steered approach* — at the next chord boundary the clock plans the
2361		remaining changes up to *bar* as a constrained walk through the
2362		engine's real weights, pinned to the cadence formula at the tail
2363		(``"strong"`` arrives V→I, ``"soft"`` IV→I, ``"open"`` IV→V,
2364		``"fakeout"`` V→vi; theory aliases accepted).  The chords still
2365		commit one boundary at a time, so the journey continues through the
2366		close.
2367
2368		One-shot: the request is consumed when planned.  Live harmony only —
2369		bound/section progressions are data and cannot be steered; a request
2370		whose bar passes unserved expires with a warning.  If the formula is
2371		not walkable from where the harmony stands, the arrival lands by
2372		fiat (loudly).  Ask at least a pattern-lookahead ahead: patterns may
2373		already have rendered against the previously planned chord.
2374
2375		Parameters:
2376			cadence: The cadence name.
2377			bar: The 1-based bar the cadence's final chord arrives at
2378				(required; in practice ≥ 2 — bar 1 cannot be approached).
2379
2380		Example::
2381
2382			composition.request_cadence("open", bar=16)    # hang on V at bar 16
2383		"""
2384
2385		spec = subsequence.cadences.cadence_formula(cadence)
2386
2387		if bar is None or not isinstance(bar, int) or isinstance(bar, bool) or bar < 1:
2388			raise ValueError(f"request_cadence needs bar= — the 1-based bar the cadence arrives at (got {bar!r})")
2389
2390		self._cadence_requests[bar] = spec.name
2391		self._harmony_horizon.invalidate_future()

Ask the live engine to approach a cadence arriving at a bar.

The request hook: where pin_chord() is fiat, this is a steered approach — at the next chord boundary the clock plans the remaining changes up to bar as a constrained walk through the engine's real weights, pinned to the cadence formula at the tail ("strong" arrives V→I, "soft" IV→I, "open" IV→V, "fakeout" V→vi; theory aliases accepted). The chords still commit one boundary at a time, so the journey continues through the close.

One-shot: the request is consumed when planned. Live harmony only — bound/section progressions are data and cannot be steered; a request whose bar passes unserved expires with a warning. If the formula is not walkable from where the harmony stands, the arrival lands by fiat (loudly). Ask at least a pattern-lookahead ahead: patterns may already have rendered against the previously planned chord.

Arguments:
  • cadence: The cadence name.
  • bar: The 1-based bar the cadence's final chord arrives at (required; in practice ≥ 2 — bar 1 cannot be approached).

Example::

    composition.request_cadence("open", bar=16)    # hang on V at bar 16
def section_cadence(self, section_name: str, cadence: Optional[str] = 'strong') -> None:
2393	def section_cadence (self, section_name: str, cadence: typing.Optional[str] = "strong") -> None:
2394
2395		"""Close every pass of a section with a cadence — the standing request.
2396
2397		Each time *section_name* is entered, the clock registers a
2398		:meth:`request_cadence` arriving at the section's final bar, so the
2399		harmony approaches the close as the section ends.  Live harmony
2400		only: a section with bound chords (:meth:`section_chords`) is data
2401		and ignores the registration — its closes are written, not steered.
2402		Pass ``None`` to unregister.
2403
2404		Example::
2405
2406			composition.form([("verse", 8), ("chorus", 8)])
2407			composition.section_cadence("verse", "open")     # every verse hangs on V
2408			composition.section_cadence("chorus", "strong")  # every chorus lands home
2409		"""
2410
2411		if cadence is None:
2412			self._section_cadences.pop(section_name, None)
2413			return
2414
2415		spec = subsequence.cadences.cadence_formula(cadence)
2416		self._section_cadences[section_name] = spec.name

Close every pass of a section with a cadence — the standing request.

Each time section_name is entered, the clock registers a request_cadence() arriving at the section's final bar, so the harmony approaches the close as the section ends. Live harmony only: a section with bound chords (section_chords()) is data and ignores the registration — its closes are written, not steered. Pass None to unregister.

Example::

    composition.form([("verse", 8), ("chorus", 8)])
    composition.section_cadence("verse", "open")     # every verse hangs on V
    composition.section_cadence("chorus", "strong")  # every chorus lands home
def section_motifs(self, section_name: str, value: Any, part: Optional[str] = None) -> None:
2418	def section_motifs (self, section_name: str, value: typing.Any, part: typing.Optional[str] = None) -> None:
2419
2420		"""Bind a Motif or Phrase to a named form section (per optional part).
2421
2422		Patterns read the binding back with ``p.section_motif(part)`` (or use
2423		the one-call :meth:`phrase_part`); a section with no binding for the
2424		part is silent for that part — bind material or don't, no fallback
2425		guessing.  Re-binding is idempotent, so the call is safe in a live
2426		file: re-executing on save is the desired rebind.
2427
2428		Parameters:
2429			section_name: Name of the section as defined in :meth:`form`.
2430			value: A ``Motif`` or ``Phrase`` (anything exposing
2431				``.length``/``.slice`` places).
2432			part: Optional part label, so one section can carry several
2433				bindings (``"lead"``, ``"bass"``, ...).
2434
2435		Raises:
2436			ValueError: If a graph-based form has been configured and
2437				*section_name* is not one of its sections.
2438
2439		Example::
2440
2441			composition.section_motifs("verse",  verse_line,  part="lead")
2442			composition.section_motifs("chorus", chorus_line, part="lead")
2443		"""
2444
2445		if not hasattr(value, "length") or not hasattr(value, "slice"):
2446			raise TypeError(
2447				f"section_motifs() binds Motif/Phrase values (.length/.slice) — got {type(value).__name__}"
2448			)
2449
2450		if (
2451			self._form_state is not None
2452			and self._form_state._section_bars is not None
2453			and section_name not in self._form_state._section_bars
2454		):
2455			known = ", ".join(sorted(self._form_state._section_bars))
2456			raise ValueError(
2457				f"Section '{section_name}' not found in form. "
2458				f"Known sections: {known}"
2459			)
2460
2461		self._section_motifs[(section_name, part)] = value

Bind a Motif or Phrase to a named form section (per optional part).

Patterns read the binding back with p.section_motif(part) (or use the one-call phrase_part()); a section with no binding for the part is silent for that part — bind material or don't, no fallback guessing. Re-binding is idempotent, so the call is safe in a live file: re-executing on save is the desired rebind.

Arguments:
  • section_name: Name of the section as defined in form().
  • value: A Motif or Phrase (anything exposing .length/.slice places).
  • part: Optional part label, so one section can carry several bindings ("lead", "bass", ...).
Raises:
  • ValueError: If a graph-based form has been configured and section_name is not one of its sections.

Example::

    composition.section_motifs("verse",  verse_line,  part="lead")
    composition.section_motifs("chorus", chorus_line, part="lead")
def on_event(self, event_name: str, callback: Callable[..., Any]) -> None:
2463	def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None:
2464
2465		"""
2466		Register a callback for a sequencer event (e.g., "bar", "start", "stop").
2467		"""
2468
2469		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:
2476	def hotkeys (self, enabled: bool = True) -> None:
2477
2478		"""Enable or disable the global hotkey listener.
2479
2480		Must be called **before** :meth:`play` to take effect.  When enabled, a
2481		background thread reads single keystrokes from stdin without requiring
2482		Enter.  The ``?`` key is always reserved and lists all active bindings.
2483
2484		Hotkeys have zero impact on playback when disabled — the listener
2485		thread is never started.
2486
2487		Args:
2488		    enabled: ``True`` (default) to enable hotkeys; ``False`` to disable.
2489
2490		Example::
2491
2492		    composition.hotkeys()
2493		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
2494		    composition.play()
2495		"""
2496
2497		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:
2500	def hotkey (
2501		self,
2502		key:      str,
2503		action:   typing.Callable[[], None],
2504		quantize: int = 0,
2505		label:    typing.Optional[str] = None,
2506	) -> None:
2507
2508		"""Register a single-key shortcut that fires during playback.
2509
2510		The listener must be enabled first with :meth:`hotkeys`.
2511
2512		Most actions — form jumps, ``composition.data`` writes, and
2513		:meth:`tweak` calls — should use ``quantize=0`` (the default).  Their
2514		musical effect is naturally delayed to the next pattern rebuild cycle,
2515		which provides automatic musical quantization without extra configuration.
2516
2517		Use ``quantize=N`` for actions where you want an explicit bar-boundary
2518		guarantee, such as :meth:`mute` / :meth:`unmute`.
2519
2520		The ``?`` key is reserved and cannot be overridden.
2521
2522		Args:
2523		    key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``).
2524		    action: Zero-argument callable to execute.
2525		    quantize: ``0`` = execute immediately (default).  ``N`` = execute
2526		        on the next global bar number divisible by *N*.
2527		    label: Display name for the ``?`` help listing.  Auto-derived from
2528		        the function name or lambda body if omitted.
2529
2530		Raises:
2531		    ValueError: If ``key`` is the reserved ``?`` character, or if
2532		        ``key`` is not exactly one character.
2533
2534		Example::
2535
2536		    composition.hotkeys()
2537
2538		    # Immediate — musical effect happens at next pattern rebuild
2539		    composition.hotkey("a", lambda: composition.form_jump("chorus"))
2540		    composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
2541
2542		    # Explicit 4-bar phrase boundary
2543		    composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
2544
2545		    # Named function — label is derived automatically
2546		    def drop_to_breakdown ():
2547		        composition.form_jump("breakdown")
2548		        composition.mute("lead")
2549
2550		    composition.hotkey("d", drop_to_breakdown)
2551
2552		    composition.play()
2553		"""
2554
2555		if len(key) != 1:
2556			raise ValueError(f"hotkey key must be a single character, got {key!r}")
2557
2558		if key == _HOTKEY_RESERVED:
2559			raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.")
2560
2561		derived = label if label is not None else _derive_label(action)
2562
2563		self._hotkey_bindings[key] = HotkeyBinding(
2564			key      = key,
2565			action   = action,
2566			quantize = quantize,
2567			label    = derived,
2568		)

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:
2571	def form_jump (self, section_name: str) -> None:
2572
2573		"""Jump the form to a named section immediately.
2574
2575		Delegates to :meth:`subsequence.form_state.FormState.jump_to`.  Only works when the
2576		composition uses graph-mode form (a dict passed to :meth:`form`).
2577
2578		The musical effect is heard at the *next pattern rebuild cycle* — already-
2579		queued MIDI notes are unaffected.  This natural delay means ``form_jump``
2580		is effective without needing explicit quantization.
2581
2582		Args:
2583		    section_name: The section to jump to.
2584
2585		Raises:
2586		    ValueError: If no form is configured, or the form is not in graph
2587		        mode, or *section_name* is unknown.
2588
2589		Example::
2590
2591		    composition.hotkey("c", lambda: composition.form_jump("chorus"))
2592		"""
2593
2594		if self._form_state is None:
2595			raise ValueError("form_jump() requires a form to be configured via composition.form().")
2596
2597		self._form_state.jump_to(section_name)
2598
2599		# The harmony horizon planned against the old section — revoke it.
2600		self._harmony_horizon.invalidate_future()

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:
2603	def form_next (self, section_name: str) -> None:
2604
2605		"""Queue the next section — takes effect when the current section ends.
2606
2607		Unlike :meth:`form_jump`, this does not interrupt the current section.
2608		The queued section replaces the automatically pre-decided next section
2609		and takes effect at the natural section boundary.  The performer can
2610		change their mind by calling ``form_next`` again before the boundary.
2611
2612		Delegates to :meth:`subsequence.form_state.FormState.queue_next`.  Only works when the
2613		composition uses graph-mode form (a dict passed to :meth:`form`).
2614
2615		Args:
2616		    section_name: The section to queue.
2617
2618		Raises:
2619		    ValueError: If no form is configured, or the form is not in graph
2620		        mode, or *section_name* is unknown.
2621
2622		Example::
2623
2624		    composition.hotkey("c", lambda: composition.form_next("chorus"))
2625		"""
2626
2627		if self._form_state is None:
2628			raise ValueError("form_next() requires a form to be configured via composition.form().")
2629
2630		self._form_state.queue_next(section_name)
2631
2632		# The harmony horizon planned against the old continuation — revoke it.
2633		self._harmony_horizon.invalidate_future()

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"))
seed: Optional[int]
2717	@property
2718	def seed (self) -> typing.Optional[int]:
2719
2720		"""
2721		The composition's random seed, or None when unseeded.
2722
2723		When set, every random decision derives deterministically from this
2724		value through named streams (see ``seed_for()``), so the same script
2725		produces the same music on every run.  Assign to set it::
2726
2727			comp.seed = 42
2728
2729		(Formerly the method ``comp.seed(42)`` — the call form is a hard
2730		break per the pre-1.0 rename policy.)
2731		"""
2732
2733		return self._seed

The composition's random seed, or None when unseeded.

When set, every random decision derives deterministically from this value through named streams (see seed_for()), so the same script produces the same music on every run. Assign to set it::

    comp.seed = 42

(Formerly the method comp.seed(42) — the call form is a hard break per the pre-1.0 rename policy.)

def seed_for(self, name: str) -> Optional[int]:
2767	def seed_for (self, name: str) -> typing.Optional[int]:
2768
2769		"""
2770		Surface the effective derived seed for a named stream.
2771
2772		Works for pattern names and equally for any name you invent for a
2773		standalone value generator (``seed=composition.seed_for("hook")``),
2774		so its randomness keys off the composition seed without sharing any
2775		other consumer's stream.  Reflects ``reroll()`` nonces.  Returns None
2776		when the composition is unseeded.
2777
2778		Example:
2779			```python
2780			hook_seed = composition.seed_for("hook")
2781			```
2782		"""
2783
2784		return self._stream_seed(name)

Surface the effective derived seed for a named stream.

Works for pattern names and equally for any name you invent for a standalone value generator (seed=composition.seed_for("hook")), so its randomness keys off the composition seed without sharing any other consumer's stream. Reflects reroll() nonces. Returns None when the composition is unseeded.

Example:
hook_seed = composition.seed_for("hook")
def reroll(self, name: str) -> None:
2786	def reroll (self, name: str) -> None:
2787
2788		"""
2789		Deal a named stream a fresh deterministic seed — try a new variation.
2790
2791		Bumps the per-name nonce and prints the new effective seed.  The
2792		nonce lives only in this process, so the printed seed is what lets a
2793		variation you like survive a restart: note it down, or ``lock()`` the
2794		name to pin it for the session.  Refuses on locked names.
2795
2796		Parameters:
2797			name: The stream name — usually a pattern name.
2798
2799		Example:
2800			```python
2801			comp.reroll("lead")    # prints: reroll('lead') -> effective seed ...
2802			```
2803		"""
2804
2805		if name in self._locked_names:
2806			print(f"reroll('{name}') refused: '{name}' is locked - call unlock('{name}') first")
2807			return
2808
2809		self._reroll_nonces[name] = self._reroll_nonces.get(name, 0) + 1
2810		effective = self._stream_seed(name)
2811
2812		if effective is None:
2813			print(f"reroll('{name}'): composition has no seed - randomness is unseeded")
2814			return
2815
2816		running = self._running_patterns.get(name)
2817
2818		if running is not None and hasattr(running, "_rng"):
2819			running._rng = random.Random(effective)
2820
2821		print(f"reroll('{name}') -> effective seed {effective} (nonce {self._reroll_nonces[name]})")

Deal a named stream a fresh deterministic seed — try a new variation.

Bumps the per-name nonce and prints the new effective seed. The nonce lives only in this process, so the printed seed is what lets a variation you like survive a restart: note it down, or lock() the name to pin it for the session. Refuses on locked names.

Arguments:
  • name: The stream name — usually a pattern name.
Example:
comp.reroll("lead")    # prints: reroll('lead') -> effective seed ...
def lock(self, name: str) -> None:
2823	def lock (self, name: str) -> None:
2824
2825		"""
2826		Pin a named stream: keep its current effective seed and realization.
2827
2828		Engine-side state, so it survives live reload (it is never a builder
2829		swap): a locked pattern re-deals its stream from the same effective
2830		seed on every rebuild, so every cycle realizes identically, and
2831		``reroll()`` refuses with a message until ``unlock()``.
2832
2833		Parameters:
2834			name: The stream name — usually a pattern name.
2835		"""
2836
2837		self._locked_names.add(name)

Pin a named stream: keep its current effective seed and realization.

Engine-side state, so it survives live reload (it is never a builder swap): a locked pattern re-deals its stream from the same effective seed on every rebuild, so every cycle realizes identically, and reroll() refuses with a message until unlock().

Arguments:
  • name: The stream name — usually a pattern name.
def unlock(self, name: str) -> None:
2839	def unlock (self, name: str) -> None:
2840
2841		"""Release a ``lock()``: the stream runs free and ``reroll()`` works again."""
2842
2843		self._locked_names.discard(name)

Release a lock(): the stream runs free and reroll() works again.

def tuning( self, source: Union[str, os.PathLike, NoneType] = None, *, cents: Optional[List[float]] = None, ratios: Optional[List[float]] = None, equal: Optional[int] = None, bend_range: float = 2.0, channels: Optional[List[int]] = None, reference_note: int = 60, exclude_drums: bool = True) -> None:
2845	def tuning (
2846		self,
2847		source: typing.Optional[typing.Union[str, "os.PathLike"]] = None,
2848		*,
2849		cents: typing.Optional[typing.List[float]] = None,
2850		ratios: typing.Optional[typing.List[float]] = None,
2851		equal: typing.Optional[int] = None,
2852		bend_range: float = 2.0,
2853		channels: typing.Optional[typing.List[int]] = None,
2854		reference_note: int = 60,
2855		exclude_drums: bool = True,
2856	) -> None:
2857
2858		"""Set a global microtonal tuning for the composition.
2859
2860		The tuning is applied automatically after each pattern rebuild (before
2861		the pattern is scheduled).  Drum patterns (those registered with a
2862		``drum_note_map``) are excluded by default.
2863
2864		Supply exactly one of the source parameters:
2865
2866		- ``source``: path to a Scala ``.scl`` file.
2867		- ``cents``: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).
2868		- ``ratios``: list of frequency ratios (e.g., ``[9/8, 5/4, 4/3, 3/2, 2]``).
2869		- ``equal``: integer for N-tone equal temperament (e.g., ``equal=19``).
2870
2871		For polyphonic parts, supply a ``channels`` pool.  Notes are spread
2872		across those MIDI channels so each can carry an independent pitch bend.
2873		The synth must be configured to match ``bend_range`` (its pitch-bend range
2874		setting in semitones).
2875
2876		Parameters:
2877			source: Path to a ``.scl`` file.
2878			cents: Cent offsets for scale degrees 1..N.
2879			ratios: Frequency ratios for scale degrees 1..N.
2880			equal: Number of equal divisions of the period.
2881			bend_range: Synth pitch-bend range in semitones (default ±2).
2882			channels: Channel pool for polyphonic rotation.
2883			reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).
2884			exclude_drums: When True (default), skip patterns that have a
2885			    ``drum_note_map`` (they use fixed GM pitches, not tuned ones).
2886
2887		Example:
2888			```python
2889			# Quarter-comma meantone from a Scala file
2890			comp.tuning("meanquar.scl")
2891
2892			# Just intonation from ratios
2893			comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2])
2894
2895			# 19-TET, monophonic
2896			comp.tuning(equal=19, bend_range=2.0)
2897
2898			# 31-TET with channel rotation for polyphony (channels 1-6)
2899			comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])
2900			```
2901		"""
2902		import subsequence.tuning as _tuning_mod
2903
2904		given = sum(x is not None for x in [source, cents, ratios, equal])
2905		if given == 0:
2906			raise ValueError("composition.tuning() requires one of: source, cents, ratios, or equal")
2907		if given > 1:
2908			raise ValueError("composition.tuning() accepts only one source parameter")
2909
2910		if source is not None:
2911			t = _tuning_mod.Tuning.from_scl(source)
2912		elif cents is not None:
2913			t = _tuning_mod.Tuning.from_cents(cents)
2914		elif ratios is not None:
2915			t = _tuning_mod.Tuning.from_ratios(ratios)
2916		else:
2917			t = _tuning_mod.Tuning.equal(equal)  # type: ignore[arg-type]
2918
2919		self._tuning = t
2920		self._tuning_bend_range = bend_range
2921		self._tuning_channels = channels
2922		self._tuning_reference_note = reference_note
2923		self._tuning_exclude_drums = exclude_drums

Set a global microtonal tuning for the composition.

The tuning is applied automatically after each pattern rebuild (before the pattern is scheduled). Drum patterns (those registered with a drum_note_map) are excluded by default.

Supply exactly one of the source parameters:

  • source: path to a Scala .scl file.
  • cents: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).
  • ratios: list of frequency ratios (e.g., [9/8, 5/4, 4/3, 3/2, 2]).
  • equal: integer for N-tone equal temperament (e.g., equal=19).

For polyphonic parts, supply a channels pool. Notes are spread across those MIDI channels so each can carry an independent pitch bend. The synth must be configured to match bend_range (its pitch-bend range setting in semitones).

Arguments:
  • source: Path to a .scl file.
  • cents: Cent offsets for scale degrees 1..N.
  • ratios: Frequency ratios for scale degrees 1..N.
  • equal: Number of equal divisions of the period.
  • bend_range: Synth pitch-bend range in semitones (default ±2).
  • channels: Channel pool for polyphonic rotation.
  • reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).
  • exclude_drums: When True (default), skip patterns that have a drum_note_map (they use fixed GM pitches, not tuned ones).
Example:
# Quarter-comma meantone from a Scala file
comp.tuning("meanquar.scl")

# Just intonation from ratios
comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2])

# 19-TET, monophonic
comp.tuning(equal=19, bend_range=2.0)

# 31-TET with channel rotation for polyphony (channels 1-6)
comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])
def display( self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
2925	def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None:
2926
2927		"""
2928		Enable or disable the live terminal dashboard.
2929
2930		When enabled, Subsequence uses a safe logging handler that allows a
2931		persistent status line (BPM, Key, Bar, Section, Chord) to stay at
2932		the bottom of the terminal while logs scroll above it.
2933
2934		Parameters:
2935			enabled: Whether to show the display (default True).
2936			grid: When True, render an ASCII grid visualisation of all
2937				running patterns above the status line. The grid updates
2938				once per bar, showing which steps have notes and at what
2939				velocity.
2940			grid_scale: Horizontal zoom factor for the grid (default
2941				``1.0``).  Higher values add visual columns between
2942				grid steps, revealing micro-timing from swing and groove.
2943				Snapped to the nearest integer internally for uniform
2944				marker spacing.
2945		"""
2946
2947		if enabled:
2948			self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale)
2949		else:
2950			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, http_host: str = '127.0.0.1', ws_host: str = '127.0.0.1') -> None:
2952	def web_ui (self, http_host: str = "127.0.0.1", ws_host: str = "127.0.0.1") -> None:
2953
2954		"""
2955		Enable the realtime Web UI Dashboard.
2956
2957		When enabled, Subsequence instantiates a WebSocket server that broadcasts
2958		the current state, signals, and active patterns (with high-res timing and
2959		note data) to any connected browser clients.
2960
2961		Both servers bind to localhost by default.  Pass ``http_host`` / ``ws_host``
2962		(e.g. "0.0.0.0") to opt into LAN exposure — the dashboard is read-only but
2963		broadcasts full composition state, so only do so on a trusted network.
2964		"""
2965
2966		self._web_ui_enabled = True
2967		self._web_ui_http_host = http_host
2968		self._web_ui_ws_host = ws_host

Enable the realtime Web UI Dashboard.

When enabled, Subsequence instantiates a WebSocket server that broadcasts the current state, signals, and active patterns (with high-res timing and note data) to any connected browser clients.

Both servers bind to localhost by default. Pass http_host / ws_host (e.g. "0.0.0.0") to opt into LAN exposure — the dashboard is read-only but broadcasts full composition state, so only do so on a trusted network.

def midi_input( self, device: str, clock_follow: bool = False, name: Optional[str] = None) -> None:
2970	def midi_input (self, device: str, clock_follow: bool = False, name: typing.Optional[str] = None) -> None:
2971
2972		"""
2973		Configure a MIDI input device for external sync and MIDI messages.
2974
2975		May be called multiple times to register additional input devices.
2976		The first call sets the primary input (device 0).  Subsequent calls
2977		add additional input devices (device 1, 2, …).  Only one device may
2978		have ``clock_follow=True``.
2979
2980		Parameters:
2981			device: Which MIDI input port to use, matched against
2982				``mido.get_input_names()``.  Treated as a pattern — ``*``
2983				and ``?`` are wildcards, matching is case-insensitive, and a
2984				name without wildcards is a substring.  See
2985				``Composition.__init__`` for why a pattern like
2986				``"*Launchpad *:0"`` survives an ALSA client id changing
2987				between runs.  Because a wrong input would desynchronise or
2988				mis-record a performance, a pattern matching nothing raises,
2989				and one matching several asks which you meant rather than
2990				guessing.
2991			clock_follow: If True, Subsequence will slave its clock to incoming
2992				MIDI Ticks. It will also follow MIDI Start/Stop/Continue
2993				commands. Only one device can have this enabled at a time.
2994			name: Optional alias for use with ``cc_map(input_device=…)`` and
2995				``cc_forward(input_device=…)``.  When omitted, the raw device
2996				name is used.
2997
2998		Example:
2999			```python
3000			# Single controller (unchanged usage)
3001			comp.midi_input("Scarlett 2i4", clock_follow=True)
3002
3003			# Multiple controllers
3004			comp.midi_input("Arturia KeyStep", name="keys")
3005			comp.midi_input("Faderfox EC4", name="faders")
3006			```
3007		"""
3008
3009		if clock_follow:
3010			if self.is_clock_following:
3011				raise ValueError("Only one input device can be configured to follow external clock (clock_follow=True)")
3012
3013		if self._input_device is None:
3014			# First call: set primary input device (device 0)
3015			self._input_device = device
3016			self._input_device_alias = name
3017			self._clock_follow = clock_follow
3018		else:
3019			# Subsequent calls: register additional input devices
3020			self._additional_inputs.append((device, name, clock_follow))

Configure a MIDI input device for external sync and MIDI messages.

May be called multiple times to register additional input devices. The first call sets the primary input (device 0). Subsequent calls add additional input devices (device 1, 2, …). Only one device may have clock_follow=True.

Arguments:
  • device: Which MIDI input port to use, matched against mido.get_input_names(). Treated as a pattern — * and ? are wildcards, matching is case-insensitive, and a name without wildcards is a substring. See Composition.__init__ for why a pattern like "*Launchpad *:0" survives an ALSA client id changing between runs. Because a wrong input would desynchronise or mis-record a performance, a pattern matching nothing raises, and one matching several asks which you meant rather than guessing.
  • clock_follow: If True, Subsequence will slave its clock to incoming MIDI Ticks. It will also follow MIDI Start/Stop/Continue commands. Only one device can have this enabled at a time.
  • name: Optional alias for use with cc_map(input_device=…) and cc_forward(input_device=…). When omitted, the raw device name is used.
Example:
# Single controller (unchanged usage)
comp.midi_input("Scarlett 2i4", clock_follow=True)

# Multiple controllers
comp.midi_input("Arturia KeyStep", name="keys")
comp.midi_input("Faderfox EC4", name="faders")
def midi_output( self, device: str, name: Optional[str] = None, latency_ms: float = 0.0) -> int:
3022	def midi_output (self, device: str, name: typing.Optional[str] = None, latency_ms: float = 0.0) -> int:
3023
3024		"""
3025		Register an additional MIDI output device.
3026
3027		The first output device is always the one passed to
3028		``Composition(output_device=…)`` — that is device 0.
3029		Each call to ``midi_output()`` adds the next device (1, 2, …).
3030
3031		Parameters:
3032			device: Which MIDI output port to add, matched against
3033				``mido.get_output_names()``.  Treated as a pattern —
3034				``*`` and ``?`` are wildcards, matching is
3035				case-insensitive, and a name without wildcards is a
3036				substring.  See ``Composition.__init__`` for the lookup
3037				snippet and why a pattern like ``"*U6MIDI Pro *:0"``
3038				survives an ALSA client id changing between runs.
3039			name: Optional alias for use with ``pattern(device=…)``,
3040				``cc_forward(output_device=…)``, etc.  When omitted, the raw
3041				device name is used.
3042			latency_ms: Physical output latency of this device in
3043				milliseconds, for delay compensation (default 0.0, must be
3044				non-negative). Set this when the device sounds late (e.g. a
3045				software sampler) so Subsequence delays faster devices to
3046				line everything up.
3047
3048		Returns:
3049			The integer device index assigned (1, 2, 3, …).
3050
3051		Example:
3052			```python
3053			comp = subsequence.Composition(bpm=120, output_device="MOTU Express")
3054
3055			# Returns 1 — use as device=1 or device="integra"
3056			comp.midi_output("Roland Integra", name="integra")
3057
3058			# A software sampler that sounds 20ms late
3059			comp.midi_output("Subsample", name="sampler", latency_ms=20)
3060
3061			@comp.pattern(channel=1, beats=4, device="integra")
3062			def strings (p):
3063				p.note(60, beat=0)
3064			```
3065		"""
3066
3067		if latency_ms < 0:
3068			raise ValueError(f"latency_ms must be non-negative — got {latency_ms}")
3069
3070		idx = 1 + len(self._additional_outputs)  # device 0 is always the primary
3071		self._additional_outputs.append(_AdditionalOutput(device=device, alias=name, latency_ms=latency_ms))
3072		return idx

Register an additional MIDI output device.

The first output device is always the one passed to Composition(output_device=…) — that is device 0. Each call to midi_output() adds the next device (1, 2, …).

Arguments:
  • device: Which MIDI output port to add, matched against mido.get_output_names(). Treated as a pattern — * and ? are wildcards, matching is case-insensitive, and a name without wildcards is a substring. See Composition.__init__ for the lookup snippet and why a pattern like "*U6MIDI Pro *:0" survives an ALSA client id changing between runs.
  • name: Optional alias for use with pattern(device=…), cc_forward(output_device=…), etc. When omitted, the raw device name is used.
  • latency_ms: Physical output latency of this device in milliseconds, for delay compensation (default 0.0, must be non-negative). Set this when the device sounds late (e.g. a software sampler) so Subsequence delays faster devices to line everything up.
Returns:

The integer device index assigned (1, 2, 3, …).

Example:
comp = subsequence.Composition(bpm=120, output_device="MOTU Express")

# Returns 1 — use as device=1 or device="integra"
comp.midi_output("Roland Integra", name="integra")

# A software sampler that sounds 20ms late
comp.midi_output("Subsample", name="sampler", latency_ms=20)

@comp.pattern(channel=1, beats=4, device="integra")
def strings (p):
        p.note(60, beat=0)
def clock_output(self, enabled: bool = True) -> None:
3095	def clock_output (self, enabled: bool = True) -> None:
3096
3097		"""
3098		Send MIDI timing clock to connected hardware.
3099
3100		When enabled, Subsequence acts as a MIDI clock master and sends
3101		standard clock messages on the output port: a Start message (0xFA)
3102		when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN),
3103		and a Stop message (0xFC) when playback ends.
3104
3105		This allows hardware synthesizers, drum machines, and effect units to
3106		slave their tempo to Subsequence automatically.
3107
3108		**Note:** Clock output is automatically disabled when ``midi_input()``
3109		is called with ``clock_follow=True``, to prevent a clock feedback loop.
3110
3111		Parameters:
3112			enabled: Whether to send MIDI clock (default True).
3113
3114		Example:
3115			```python
3116			comp = subsequence.Composition(bpm=120, output_device="...")
3117			comp.clock_output()   # hardware will follow Subsequence tempo
3118			```
3119		"""
3120
3121		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, data_key: str, channel: Optional[int] = None, min_val: float = 0.0, max_val: float = 1.0, input_device: Union[int, str, NoneType] = None) -> None:
3169	def cc_map (
3170		self,
3171		cc: int,
3172		data_key: str,
3173		channel: typing.Optional[int] = None,
3174		min_val: float = 0.0,
3175		max_val: float = 1.0,
3176		input_device: subsequence.midi_utils.DeviceId = None,
3177	) -> None:
3178
3179		"""
3180		Map an incoming MIDI CC to a ``composition.data`` key.
3181
3182		When the composition receives a CC message on the configured MIDI
3183		input port, the value is scaled from the CC range (0–127) to
3184		*[min_val, max_val]* and stored in ``composition.data[data_key]``.
3185
3186		This lets hardware knobs, faders, and expression pedals control live
3187		parameters without writing any callback code.
3188
3189		**Requires** ``midi_input()`` to be called first to open an input port.
3190
3191		Parameters:
3192			cc: MIDI Control Change number (0–127).
3193			data_key: The ``composition.data`` key to write.
3194			channel: If given, only respond to CC messages on this channel.
3195				Uses the same numbering convention as ``pattern()`` (1-16
3196				by default, or 0-15 with ``zero_indexed_channels=True``).
3197				``None`` matches any channel (default).
3198			min_val: Scaled minimum — written when CC value is 0 (default 0.0).
3199			max_val: Scaled maximum — written when CC value is 127 (default 1.0).
3200			input_device: Only respond to CC messages from this input device
3201				(index or name).  ``None`` responds to any input device (default).
3202
3203		Example:
3204			```python
3205			comp.midi_input("Arturia KeyStep")
3206			comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
3207			comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader
3208
3209			# Multi-device: only listen to CC 74 from the "faders" controller
3210			comp.cc_map(74, "filter", input_device="faders")
3211			```
3212		"""
3213
3214		resolved_channel = self._resolve_channel(channel) if channel is not None else None
3215
3216		self._cc_mappings.append({
3217			'cc': cc,
3218			'data_key': data_key,
3219			'channel': resolved_channel,
3220			'min_val': min_val,
3221			'max_val': max_val,
3222			'input_device': input_device,  # resolved to int index in _run()
3223		})

Map an incoming MIDI CC to a composition.data key.

When the composition receives a CC message on the configured MIDI input port, the value is scaled from the CC range (0–127) to [min_val, max_val] and stored in composition.data[data_key].

This lets hardware knobs, faders, and expression pedals control live parameters without writing any callback code.

Requires midi_input() to be called first to open an input port.

Arguments:
  • cc: MIDI Control Change number (0–127).
  • data_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() (1-16 by default, or 0-15 with zero_indexed_channels=True). None matches any channel (default).
  • min_val: Scaled minimum — written when CC value is 0 (default 0.0).
  • max_val: Scaled maximum — written when CC value is 127 (default 1.0).
  • input_device: Only respond to CC messages from this input device (index or name). None responds to any input device (default).
Example:
comp.midi_input("Arturia KeyStep")
comp.cc_map(74, "filter_cutoff")           # knob → 0.0–1.0
comp.cc_map(7, "volume", min_val=0, max_val=127)  # volume fader

# Multi-device: only listen to CC 74 from the "faders" controller
comp.cc_map(74, "filter", input_device="faders")
def note_input( self, channel: Optional[int] = None, release_ms: float = 30.0, latch: bool = False, input_device: Union[int, str, NoneType] = None) -> None:
3226	def note_input (
3227		self,
3228		channel: typing.Optional[int] = None,
3229		release_ms: float = 30.0,
3230		latch: bool = False,
3231		input_device: subsequence.midi_utils.DeviceId = None,
3232	) -> None:
3233
3234		"""Track notes held on a MIDI keyboard for live arpeggiation.
3235
3236		Incoming note-on/note-off messages build a live "currently held" set
3237		that any pattern reads via ``p.held_notes()`` — typically fed straight
3238		to ``p.arpeggio()``.  The composition still authors the rhythm and
3239		motion; the player's hands supply the pitch set.  This is a live
3240		*performance* layer over the deterministic, seeded composition: when
3241		rendering headlessly there is no input, so ``p.held_notes()`` is empty
3242		and seeded output is unchanged.
3243
3244		**Requires** ``midi_input()`` to be called first to open an input port.
3245
3246		Parameters:
3247			channel: If given, only track notes on this channel.  Uses the same
3248				numbering convention as ``pattern()`` (1-16 by default, or 0-15
3249				with ``zero_indexed_channels=True``).  ``None`` tracks any
3250				channel (default).
3251			release_ms: How long (milliseconds) a released note keeps counting
3252				as held.  This smooths the momentary all-keys-up gap during a
3253				hand-position change so the arp does not drop to silence.
3254				Default 30.0; set 0.0 to release instantly.  Ignored when
3255				``latch`` is True.
3256			latch: When True, the held set persists after you lift your hands
3257				until you play a new chord (the first key after every key is up
3258				replaces it) — like a hardware arp's latch.
3259			input_device: Only track notes from this input device (index or
3260				name).  ``None`` tracks any input device (default).
3261
3262		Example:
3263			```python
3264			comp.midi_input("Arturia KeyStep")
3265			comp.note_input(channel=1, release_ms=30)
3266
3267			@comp.pattern(channel=6, beats=4)
3268			def arp (p):
3269			    p.arpeggio(p.held_notes(), direction="up")  # rests when silent
3270			```
3271		"""
3272
3273		if self._note_input is not None:
3274			raise RuntimeError("only one note_input source is supported — named multi-source is not yet available")
3275
3276		resolved_channel = self._resolve_channel(channel) if channel is not None else None
3277
3278		self._note_input = {
3279			'channel': resolved_channel,
3280			'release_ms': release_ms,
3281			'latch': latch,
3282			'input_device': input_device,  # resolved to int index in _run()
3283		}

Track notes held on a MIDI keyboard for live arpeggiation.

Incoming note-on/note-off messages build a live "currently held" set that any pattern reads via p.held_notes() — typically fed straight to p.arpeggio(). The composition still authors the rhythm and motion; the player's hands supply the pitch set. This is a live performance layer over the deterministic, seeded composition: when rendering headlessly there is no input, so p.held_notes() is empty and seeded output is unchanged.

Requires midi_input() to be called first to open an input port.

Arguments:
  • channel: If given, only track notes on this channel. Uses the same numbering convention as pattern() (1-16 by default, or 0-15 with zero_indexed_channels=True). None tracks any channel (default).
  • release_ms: How long (milliseconds) a released note keeps counting as held. This smooths the momentary all-keys-up gap during a hand-position change so the arp does not drop to silence. Default 30.0; set 0.0 to release instantly. Ignored when latch is True.
  • latch: When True, the held set persists after you lift your hands until you play a new chord (the first key after every key is up replaces it) — like a hardware arp's latch.
  • input_device: Only track notes from this input device (index or name). None tracks any input device (default).
Example:
comp.midi_input("Arturia KeyStep")
comp.note_input(channel=1, release_ms=30)

@comp.pattern(channel=6, beats=4)
def arp (p):
    p.arpeggio(p.held_notes(), direction="up")  # rests when silent
def cc_forward( self, cc: int, output: Union[str, Callable], *, channel: Optional[int] = None, output_channel: Optional[int] = None, mode: str = 'instant', input_device: Union[int, str, NoneType] = None, output_device: Union[int, str, NoneType] = None) -> None:
3347	def cc_forward (
3348		self,
3349		cc: int,
3350		output: typing.Union[str, typing.Callable],
3351		*,
3352		channel: typing.Optional[int] = None,
3353		output_channel: typing.Optional[int] = None,
3354		mode: str = "instant",
3355		input_device: subsequence.midi_utils.DeviceId = None,
3356		output_device: subsequence.midi_utils.DeviceId = None,
3357	) -> None:
3358
3359		"""
3360		Forward an incoming MIDI CC to the MIDI output in real-time.
3361
3362		Unlike ``cc_map()`` which writes incoming CC values to ``composition.data``
3363		for use at pattern rebuild time, ``cc_forward()`` routes the signal
3364		directly to the MIDI output — bypassing the pattern cycle entirely.
3365
3366		Both ``cc_map()`` and ``cc_forward()`` may be registered for the same CC
3367		number; they operate independently.
3368
3369		Parameters:
3370			cc: Incoming CC number to listen for (0–127).
3371			output: What to send. Either a **preset string**:
3372
3373				- ``"cc"`` — identity forward, same CC number and value.
3374				- ``"cc:N"`` — forward as CC number N (e.g. ``"cc:74"``).
3375				- ``"pitchwheel"`` — scale 0–127 to -8192..8191 and send as pitch bend.
3376
3377				Or a **callable** with signature
3378				``(value: int, channel: int) -> Optional[mido.Message]``.
3379				Return a fully formed ``mido.Message`` to send, or ``None`` to suppress.
3380				``channel`` is 0-indexed (the incoming channel).
3381			channel: If given, only respond to CC messages on this channel.
3382				Uses the same numbering convention as ``cc_map()``.
3383				``None`` matches any channel (default).
3384			output_channel: Override the output channel. ``None`` uses the
3385				incoming channel. Uses the same numbering convention as ``pattern()``.
3386			input_device: Only respond to CC from this input device — an index,
3387				a registered name, or ``None`` for any input (default), the
3388				same convention as ``cc_map()``.
3389			output_device: Send to this output device — an index, a registered
3390				name, or ``None`` for the primary output (default).
3391			mode: Dispatch mode:
3392
3393				- ``"instant"`` *(default)* — send immediately on the MIDI input
3394				  callback thread. Lowest latency (~1–5 ms). Instant forwards are
3395				  **not** recorded when recording is enabled.
3396				- ``"queued"`` — inject into the sequencer event queue and send at
3397				  the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards
3398				  **are** recorded when recording is enabled.
3399
3400		Example:
3401			```python
3402			comp.midi_input("Arturia KeyStep")
3403
3404			# CC 1 → CC 1 (identity, instant)
3405			comp.cc_forward(1, "cc")
3406
3407			# CC 1 → pitch bend on channel 1, queued (recordable)
3408			comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued")
3409
3410			# CC 1 → CC 74, custom channel
3411			comp.cc_forward(1, "cc:74", output_channel=2)
3412
3413			# Custom transform — remap CC range 0–127 to CC 74 range 40–100
3414			import subsequence.midi as midi
3415			comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch))
3416
3417			# Forward AND map to data simultaneously — both active on the same CC
3418			comp.cc_map(1, "mod_wheel")
3419			comp.cc_forward(1, "cc:74")
3420			```
3421		"""
3422
3423		if not 0 <= cc <= 127:
3424			raise ValueError(f"cc_forward(): cc {cc} out of range 0–127")
3425
3426		if mode not in ('instant', 'queued'):
3427			raise ValueError(f"cc_forward(): mode must be 'instant' or 'queued', got '{mode}'")
3428
3429		resolved_in_channel = self._resolve_channel(channel) if channel is not None else None
3430		resolved_out_channel = self._resolve_channel(output_channel) if output_channel is not None else None
3431
3432		transform = self._make_cc_forward_transform(output, cc, resolved_out_channel)
3433
3434		self._cc_forwards.append({
3435			'cc': cc,
3436			'channel': resolved_in_channel,
3437			'output_channel': resolved_out_channel,
3438			'mode': mode,
3439			'transform': transform,
3440			'input_device': input_device,   # resolved to int index in _run()
3441			'output_device': output_device, # resolved to int index in _run()
3442		})

Forward an incoming MIDI CC to the MIDI output in real-time.

Unlike cc_map() which writes incoming CC values to composition.data for use at pattern rebuild time, cc_forward() routes the signal directly to the MIDI output — bypassing the pattern cycle entirely.

Both cc_map() and cc_forward() may be registered for the same CC number; they operate independently.

Arguments:
  • cc: Incoming CC number to listen for (0–127).
  • output: What to send. Either a preset string:

    • "cc" — identity forward, same CC number and value.
    • "cc:N" — forward as CC number N (e.g. "cc:74").
    • "pitchwheel" — scale 0–127 to -8192..8191 and send as pitch bend.

    Or a callable with signature (value: int, channel: int) -> Optional[mido.Message]. Return a fully formed mido.Message to send, or None to suppress. channel is 0-indexed (the incoming channel).

  • channel: If given, only respond to CC messages on this channel. Uses the same numbering convention as cc_map(). None matches any channel (default).
  • output_channel: Override the output channel. None uses the incoming channel. Uses the same numbering convention as pattern().
  • input_device: Only respond to CC from this input device — an index, a registered name, or None for any input (default), the same convention as cc_map().
  • output_device: Send to this output device — an index, a registered name, or None for the primary output (default).
  • mode: Dispatch mode:

    • "instant" (default) — send immediately on the MIDI input callback thread. Lowest latency (~1–5 ms). Instant forwards are not recorded when recording is enabled.
    • "queued" — inject into the sequencer event queue and send at the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards are recorded when recording is enabled.
Example:
comp.midi_input("Arturia KeyStep")

# CC 1 → CC 1 (identity, instant)
comp.cc_forward(1, "cc")

# CC 1 → pitch bend on channel 1, queued (recordable)
comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued")

# CC 1 → CC 74, custom channel
comp.cc_forward(1, "cc:74", output_channel=2)

# Custom transform — remap CC range 0–127 to CC 74 range 40–100
import subsequence.midi as midi
comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch))

# Forward AND map to data simultaneously — both active on the same CC
comp.cc_map(1, "mod_wheel")
comp.cc_forward(1, "cc:74")
def live(self, port: int = 5555) -> None:
3445	def live (self, port: int = 5555) -> None:
3446
3447		"""
3448		Enable the live coding eval server.
3449
3450		This allows you to connect to a running composition using the
3451		``subsequence.live_client`` REPL and hot-swap pattern code or
3452		modify variables in real-time.
3453
3454		Security:
3455			The server executes arbitrary Python in this process — it is **not** a
3456			sandbox.  It binds to localhost only and is opt-in, but any process on
3457			the same machine that can reach the port gains full code execution here.
3458			Do not enable it on shared or multi-user hosts, and never expose the
3459			port to a network.
3460
3461		Parameters:
3462			port: The TCP port to listen on (default 5555).
3463		"""
3464
3465		self._live_server = subsequence.live_server.LiveServer(self, port=port)
3466		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.

Security:

The server executes arbitrary Python in this process — it is not a sandbox. It binds to localhost only and is opt-in, but any process on the same machine that can reach the port gains full code execution here. Do not enable it on shared or multi-user hosts, and never expose the port to a network.

Arguments:
  • port: The TCP port to listen on (default 5555).
def watch( self, path: Union[str, pathlib.Path], poll_interval: float = 0.25) -> None:
3468	def watch (self, path: typing.Union[str, pathlib.Path], poll_interval: float = 0.25) -> None:
3469
3470		"""Watch a Python file and reload it into the composition on every save.
3471
3472		The watched file is exec'd into a namespace with ``composition`` and
3473		``subsequence`` available.  ``@composition.pattern`` decorators inside
3474		the file hot-swap their corresponding running patterns in place;
3475		patterns whose function bodies have been deleted from the file are
3476		unregistered automatically on the next reload (notes stopped,
3477		removed from the running-pattern set).
3478
3479		An **initial synchronous load** happens here — if the file has a
3480		``SyntaxError`` or doesn't exist at this moment, the exception
3481		propagates so the user knows immediately.  Subsequent reloads
3482		happen on the composition's event loop and tolerate transient
3483		errors (logged, skipped).
3484
3485		Call BEFORE ``composition.play()``.  Reloads happen on the
3486		composition's event loop, so all mutations are thread-safe.
3487
3488		See the "Live coding via file watching" section of the README for
3489		the recommended wrapper-script + live-file split.
3490
3491		Parameters:
3492			path: Path to the Python file to watch.
3493			poll_interval: Seconds between ``mtime`` polls (default 0.25 s).
3494
3495		Example::
3496
3497			# live_init.py — runs once
3498			composition = subsequence.Composition(bpm=120, key="E")
3499			composition.harmony(style="aeolian_minor")
3500			composition.watch("live_patterns.py")
3501			composition.play()
3502		"""
3503
3504		# Required for the decorator hot-swap path to fire on re-decoration.
3505		self._is_live = True
3506
3507		# Detect the single-file workflow: if watch() is called from inside
3508		# the very file being watched, the outer Python script execution will
3509		# already register the patterns (the decorators sit at module level
3510		# below ``watch(__file__)``).  In that case, _load_initial's re-exec
3511		# would double-register every pattern, so skip it.  For the two-file
3512		# workflow (path != caller's __file__) the initial exec is essential
3513		# — it's the only way the watched file's patterns ever reach the
3514		# composition.
3515		caller_file = self._caller_module_file()
3516		self_watch = False
3517		if caller_file is not None:
3518			try:
3519				self_watch = pathlib.Path(caller_file).resolve() == pathlib.Path(path).resolve()
3520			except OSError:
3521				self_watch = False
3522
3523		self._live_reloader = subsequence.live_reloader.LiveReloader(
3524			composition = self,
3525			path = path,
3526			poll_interval = poll_interval,
3527			skip_initial_exec = self_watch,
3528		)
3529		self._live_reloader.start()

Watch a Python file and reload it into the composition on every save.

The watched file is exec'd into a namespace with composition and subsequence available. @composition.pattern decorators inside the file hot-swap their corresponding running patterns in place; patterns whose function bodies have been deleted from the file are unregistered automatically on the next reload (notes stopped, removed from the running-pattern set).

An initial synchronous load happens here — if the file has a SyntaxError or doesn't exist at this moment, the exception propagates so the user knows immediately. Subsequent reloads happen on the composition's event loop and tolerate transient errors (logged, skipped).

Call BEFORE composition.play(). Reloads happen on the composition's event loop, so all mutations are thread-safe.

See the "Live coding via file watching" section of the README for the recommended wrapper-script + live-file split.

Arguments:
  • path: Path to the Python file to watch.
  • poll_interval: Seconds between mtime polls (default 0.25 s).

Example::

    # live_init.py — runs once
    composition = subsequence.Composition(bpm=120, key="E")
    composition.harmony(style="aeolian_minor")
    composition.watch("live_patterns.py")
    composition.play()
def load_patterns(self, source: str, source_label: str = '<string>') -> None:
3548	def load_patterns (
3549		self,
3550		source:       str,
3551		source_label: str = "<string>",
3552	) -> None:
3553
3554		"""Compile and apply a pattern-source string to the composition.
3555
3556		Equivalent to one ``watch()`` reload triggered by save, but with the
3557		source presented in-memory rather than on disk.  Useful for web /
3558		REST handlers that accept pattern uploads from a trusted contributor,
3559		or for one-shot session loads with no file backing.
3560
3561		Behaviour mirrors ``watch()``:
3562
3563		* The source is exec'd into a fresh namespace with ``composition``
3564		  and ``subsequence`` in scope.
3565		* ``@composition.pattern`` decorators in the source hot-swap their
3566		  corresponding running patterns in place.
3567		* Patterns currently running but **not** declared in the source are
3568		  unregistered — the source is treated as the full new truth.
3569		* If the composition is already playing, the swap happens on the
3570		  event loop thread; the call blocks until it completes.
3571		* If the composition has not yet called ``play()``, the source runs
3572		  on the caller's thread; decorators populate ``_pending_patterns``
3573		  and ``play()`` picks them up in the usual way.
3574
3575		Errors are raised so the caller can act on them:
3576
3577		* ``SyntaxError`` if ``source`` fails to compile.
3578		* The exception raised inside ``exec()`` for any runtime error.
3579		* ``RuntimeError`` if called from inside the composition's own
3580		  event loop thread (would deadlock — see Threading below).
3581
3582		In either failure case, existing composition state is preserved —
3583		the diff-and-unregister phase is skipped if exec raised, so a
3584		half-broken upload cannot tear down working patterns.
3585
3586		Threading:
3587			Designed to be called from a thread DIFFERENT from the
3588			composition's event loop — typically a web-handler worker.
3589			Cannot be called from inside the loop itself (a pattern
3590			callback, an asyncio task spawned by the composition).  From
3591			there, ``await composition._apply_source_async(...)`` directly.
3592
3593		SECURITY WARNING: ``exec()`` is not sandboxed.  The source has full
3594		Python access in this process.  Only pass source from trusted
3595		senders.  The built-in blocklist (``help``, ``input``, ``breakpoint``,
3596		``exit``, ``quit``) prevents calls that would stall the event loop;
3597		it is not a security boundary.
3598
3599		Parameters:
3600			source:       Python source declaring ``@composition.pattern``
3601				functions.
3602			source_label: Identifier used in compile errors and tracebacks
3603				(appears as the filename in ``SyntaxError`` and ``__file__``-
3604				style traceback lines).  Default ``"<string>"``.
3605		"""
3606
3607		# Required for the decorator hot-swap path to fire on re-decoration.
3608		self._is_live = True
3609
3610		# Compile on the caller's thread so SyntaxError comes back fast,
3611		# before any cross-thread scheduling.
3612		compiled = compile(source, source_label, "exec")
3613		namespace = self._build_live_namespace(source_label = source_label)
3614
3615		loop = self._sequencer._event_loop
3616
3617		if loop is not None and loop.is_running():
3618
3619			# Refuse to deadlock: calling load_patterns() from inside the
3620			# composition's own event loop (e.g. from a pattern callback or
3621			# an asyncio task spawned by the composition) would have us
3622			# block waiting for a coroutine that can only run when this
3623			# thread yields.  Tell the caller exactly what to do instead.
3624			try:
3625				current_loop: typing.Optional[asyncio.AbstractEventLoop] = asyncio.get_running_loop()
3626			except RuntimeError:
3627				current_loop = None
3628
3629			if current_loop is loop:
3630				raise RuntimeError(
3631					"load_patterns() cannot be called from inside the composition's "
3632					"event loop thread — it would deadlock waiting for the "
3633					"scheduled coroutine to run on the very thread that's blocked. "
3634					"From a worker thread, call it normally.  From an async "
3635					"coroutine already on the loop, "
3636					"`await composition._apply_source_async(compile(source, label, 'exec'), "
3637					"composition._build_live_namespace())` instead."
3638				)
3639
3640			# Composition is playing — mutation must happen on the loop thread.
3641			# future.result() blocks the caller until the coroutine finishes
3642			# and re-raises any exception it threw.
3643			future = asyncio.run_coroutine_threadsafe(
3644				self._apply_source_async(compiled, namespace, source_key = source_label),
3645				loop = loop,
3646			)
3647			future.result()
3648
3649		else:
3650			# Pre-play: no event loop yet.  Decorators populate
3651			# _pending_patterns; play() graduates them in the usual way.
3652			# Diff-and-unregister is unnecessary here — nothing is running,
3653			# but RECORD what this source declares so a later post-play
3654			# reload under the same label can tear down its deletions.
3655			self._declared_names = set()
3656			exec(compiled, namespace)
3657			self._source_declared[source_label] = set(self._declared_names)

Compile and apply a pattern-source string to the composition.

Equivalent to one watch() reload triggered by save, but with the source presented in-memory rather than on disk. Useful for web / REST handlers that accept pattern uploads from a trusted contributor, or for one-shot session loads with no file backing.

Behaviour mirrors watch():

  • The source is exec'd into a fresh namespace with composition and subsequence in scope.
  • @composition.pattern decorators in the source hot-swap their corresponding running patterns in place.
  • Patterns currently running but not declared in the source are unregistered — the source is treated as the full new truth.
  • If the composition is already playing, the swap happens on the event loop thread; the call blocks until it completes.
  • If the composition has not yet called play(), the source runs on the caller's thread; decorators populate _pending_patterns and play() picks them up in the usual way.

Errors are raised so the caller can act on them:

  • SyntaxError if source fails to compile.
  • The exception raised inside exec() for any runtime error.
  • RuntimeError if called from inside the composition's own event loop thread (would deadlock — see Threading below).

In either failure case, existing composition state is preserved — the diff-and-unregister phase is skipped if exec raised, so a half-broken upload cannot tear down working patterns.

Threading:

Designed to be called from a thread DIFFERENT from the composition's event loop — typically a web-handler worker. Cannot be called from inside the loop itself (a pattern callback, an asyncio task spawned by the composition). From there, await composition._apply_source_async(...) directly.

SECURITY WARNING: exec() is not sandboxed. The source has full Python access in this process. Only pass source from trusted senders. The built-in blocklist (help, input, breakpoint, exit, quit) prevents calls that would stall the event loop; it is not a security boundary.

Arguments:
  • source: Python source declaring @composition.pattern functions.
  • source_label: Identifier used in compile errors and tracebacks (appears as the filename in SyntaxError and __file__- style traceback lines). Default "<string>".
def osc( self, receive_port: int = 9000, send_port: int = 9001, send_host: str = '127.0.0.1', receive_host: str = '0.0.0.0') -> None:
3772	def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1", receive_host: str = "0.0.0.0") -> None:
3773
3774		"""
3775		Enable bi-directional Open Sound Control (OSC).
3776
3777		Subsequence will listen for commands (like ``/bpm`` or ``/mute``) and
3778		broadcast its internal state (like ``/chord`` or ``/bar``) over UDP.
3779
3780		Parameters:
3781			receive_port: Port to listen for incoming OSC messages (default 9000).
3782			send_port: Port to send state updates to (default 9001).
3783			send_host: The IP address to send updates to (default "127.0.0.1").
3784			receive_host: Interface to listen on (default "0.0.0.0" — all
3785				interfaces, so external OSC controllers on the LAN can reach it).
3786				The listener can change tempo, mute patterns, and write data, so on
3787				an untrusted network restrict it with ``receive_host="127.0.0.1"``.
3788		"""
3789
3790		self._osc_server = subsequence.osc.OscServer(
3791			self,
3792			receive_port = receive_port,
3793			send_port = send_port,
3794			send_host = send_host,
3795			receive_host = receive_host
3796		)

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").
  • receive_host: Interface to listen on (default "0.0.0.0" — all interfaces, so external OSC controllers on the LAN can reach it). The listener can change tempo, mute patterns, and write data, so on an untrusted network restrict it with receive_host="127.0.0.1".
def osc_map(self, address: str, handler: Callable) -> None:
3798	def osc_map (self, address: str, handler: typing.Callable) -> None:
3799
3800		"""
3801		Register a custom OSC handler.
3802
3803		Must be called after :meth:`osc` has been configured.
3804
3805		Parameters:
3806			address: OSC address pattern to match (e.g. ``"/my/param"``).
3807			handler: Callable invoked with ``(address, *args)`` when a
3808				matching message arrives.
3809
3810		Example::
3811
3812			composition.osc()
3813
3814			def on_intensity (address, value):
3815				composition.data["intensity"] = float(value)
3816
3817			composition.osc_map("/intensity", on_intensity)
3818		"""
3819
3820		if self._osc_server is None:
3821			raise RuntimeError("Call composition.osc() before composition.osc_map()")
3822
3823		self._osc_server.map(address, handler)

Register a custom OSC handler.

Must be called after osc() has been configured.

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

Example::

    composition.osc()

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

    composition.osc_map("/intensity", on_intensity)
def set_bpm(self, bpm: float) -> None:
3825	def set_bpm (self, bpm: float) -> None:
3826
3827		"""
3828		Instantly change the tempo.
3829
3830		Parameters:
3831			bpm: The new tempo in beats per minute.
3832
3833		When Ableton Link is active, this proposes the new tempo to the Link
3834		network instead of applying it locally.  The network-authoritative tempo
3835		is picked up on the next pulse.
3836		"""
3837
3838		self._sequencer.set_bpm(bpm)
3839
3840		if not self.is_clock_following and self._link_quantum is None:
3841			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:
3843	def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None:
3844
3845		"""
3846		Smoothly ramp the tempo to a target value over a number of bars.
3847
3848		Parameters:
3849			bpm: Target tempo in beats per minute.
3850			bars: Duration of the transition in bars.
3851			shape: Easing curve name.  Defaults to ``"linear"``.
3852			       ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural-
3853			       sounding tempo changes.  See :mod:`subsequence.easing` for all
3854			       available shapes.
3855
3856		Example:
3857			```python
3858			# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve
3859			comp.target_bpm(140, bars=8, shape="ease_in_out")
3860			```
3861
3862		Note:
3863			Ignored while Ableton Link is active — the shared session tempo is
3864			authoritative.  Use ``set_bpm()`` to propose a tempo to the Link network.
3865		"""
3866
3867		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")
Note:

Ignored while Ableton Link is active — the shared session tempo is authoritative. Use set_bpm() to propose a tempo to the Link network.

def live_info(self) -> Dict[str, Any]:
3869	def live_info (self) -> typing.Dict[str, typing.Any]:
3870
3871		"""
3872		Return a dictionary containing the current state of the composition.
3873		
3874		Includes BPM, key, current bar, active section, current chord, 
3875		running patterns, and custom data.
3876		"""
3877
3878		section_info = None
3879		if self._form_state is not None:
3880			section = self._form_state.get_section_info()
3881			if section is not None:
3882				section_info = {
3883					"name": section.name,
3884					"bar": section.bar,
3885					"bars": section.bars,
3886					"progress": section.progress
3887				}
3888
3889		chord_name = None
3890		sounding_chord = self.current_chord()
3891		if sounding_chord is not None:
3892			chord_name = sounding_chord.name()
3893
3894		pattern_list = []
3895		channel_offset = 0 if self._zero_indexed_channels else 1
3896		for name, pat in self._running_patterns.items():
3897			pattern_list.append({
3898				"name": name,
3899				"channel": pat.channel + channel_offset,
3900				"length": pat.length,
3901				"cycle": pat._cycle_count,
3902				"muted": pat._muted,
3903				"tweaks": dict(pat._tweaks)
3904			})
3905
3906		return {
3907			"bpm": self._sequencer.current_bpm,
3908			"key": self.key,
3909			"bar": self._builder_bar,
3910			"section": section_info,
3911			"chord": chord_name,
3912			"patterns": pattern_list,
3913			"input_device": self._input_device,
3914			"clock_follow": self.is_clock_following,
3915			"data": self.data
3916		}

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:
3918	def mute (self, name: str) -> None:
3919
3920		"""
3921		Mute a running pattern by name.
3922		
3923		The pattern continues to 'run' and increment its cycle count in 
3924		the background, but it will not produce any MIDI notes until unmuted.
3925
3926		Parameters:
3927			name: The function name of the pattern to mute.
3928		"""
3929
3930		if name not in self._running_patterns:
3931			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3932
3933		# The performer takes ownership: if a transition's approach window had
3934		# muted this pattern, drop it from that set so the section boundary
3935		# does not silently unmute it ("performer mutes win").
3936		self._transition_muted.discard(name)
3937
3938		self._running_patterns[name]._muted = True
3939		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:
3941	def unmute (self, name: str) -> None:
3942
3943		"""
3944		Unmute a previously muted pattern.
3945		"""
3946
3947		if name not in self._running_patterns:
3948			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3949
3950		# Symmetric ownership claim: an explicit unmute means the transition
3951		# machinery should no longer manage this pattern at the boundary.
3952		self._transition_muted.discard(name)
3953
3954		self._running_patterns[name]._muted = False
3955		logger.info(f"Unmuted pattern: {name}")

Unmute a previously muted pattern.

def unregister(self, name: str) -> None:
3957	def unregister (self, name: str) -> None:
3958
3959		"""Fully remove a running pattern from rotation.
3960
3961		Unlike ``mute()`` (which keeps the pattern alive but silent),
3962		``unregister()`` tears the pattern down entirely.  It sets
3963		``pattern._removed = True`` so the sequencer's reschedule loop
3964		skips re-adding it on the next pulse; sends ``note_off`` for any
3965		of the pattern's currently-sounding notes on the primary
3966		destination AND on every mirror destination (so drones and
3967		sustaining notes stop immediately); and removes the entry from
3968		``_running_patterns`` so it no longer appears in ``live_info()``,
3969		the terminal grid, or any other consumer that enumerates running
3970		patterns.
3971
3972		Already-queued events in the sequencer's event queue play out —
3973		note_offs are paired with their note_ons at queue time, so notes
3974		end at their natural duration; only drones rely on the targeted
3975		``_stop_pattern_notes`` pass.
3976
3977		Idempotent: silently logs a ``debug`` and returns if the pattern
3978		is already absent.  Useful from both the live REPL
3979		(``composition.live()``) and the file watcher
3980		(``composition.watch()``), which calls this for any pattern
3981		removed from the watched file between reloads.
3982
3983		Parameters:
3984			name: Function name of the pattern to remove.
3985		"""
3986
3987		if name not in self._running_patterns:
3988			logger.debug(f"unregister() no-op: pattern '{name}' not running")
3989			return
3990
3991		pattern = self._running_patterns[name]
3992
3993		# Mark for removal first so the reschedule loop sees the flag even if
3994		# it fires concurrently with the note-off pass below.
3995		pattern._removed = True
3996
3997		# Stop sustaining notes (including drones) on every destination this
3998		# pattern outputs to.  Fire-and-forget across threads via the event
3999		# loop; ``_stop_pattern_notes`` acquires the queue lock internally.
4000		if self._sequencer._event_loop is not None:
4001			asyncio.run_coroutine_threadsafe(
4002				self._sequencer._stop_pattern_notes(pattern),
4003				loop = self._sequencer._event_loop,
4004			)
4005
4006		def _finalise_removal () -> None:
4007			self._running_patterns.pop(name, None)
4008
4009			# Forget any pending (not-yet-graduated) declaration too, so a
4010			# later live reload cannot resurrect the pattern.
4011			self._pending_patterns = [
4012				pending for pending in self._pending_patterns
4013				if pending.builder_fn.__name__ != name
4014			]
4015
4016			logger.info(f"Unregistered pattern: {name}")
4017
4018		# The running-patterns dict is iterated by the display, web UI, and
4019		# reschedule loop on the event loop thread — mutate it there when this
4020		# call arrives from another thread (e.g. the live TCP server).
4021		loop = self._sequencer._event_loop
4022
4023		try:
4024			on_loop = loop is not None and asyncio.get_running_loop() is loop
4025		except RuntimeError:
4026			on_loop = False
4027
4028		if loop is not None and loop.is_running() and not on_loop:
4029			loop.call_soon_threadsafe(_finalise_removal)
4030		else:
4031			_finalise_removal()

Fully remove a running pattern from rotation.

Unlike mute() (which keeps the pattern alive but silent), unregister() tears the pattern down entirely. It sets pattern._removed = True so the sequencer's reschedule loop skips re-adding it on the next pulse; sends note_off for any of the pattern's currently-sounding notes on the primary destination AND on every mirror destination (so drones and sustaining notes stop immediately); and removes the entry from _running_patterns so it no longer appears in live_info(), the terminal grid, or any other consumer that enumerates running patterns.

Already-queued events in the sequencer's event queue play out — note_offs are paired with their note_ons at queue time, so notes end at their natural duration; only drones rely on the targeted _stop_pattern_notes pass.

Idempotent: silently logs a debug and returns if the pattern is already absent. Useful from both the live REPL (composition.live()) and the file watcher (composition.watch()), which calls this for any pattern removed from the watched file between reloads.

Arguments:
  • name: Function name of the pattern to remove.
def mirror( self, name: str, device: int, channel: int, drum_note_map: Optional[Dict[str, int]] = None) -> None:
4033	def mirror (self, name: str, device: int, channel: int, drum_note_map: typing.Optional[typing.Dict[str, int]] = None) -> None:
4034
4035		"""
4036		Add a mirror destination to a running pattern.
4037
4038		Every note, CC, pitch bend, NRPN/RPN, program change, SysEx, and drone
4039		event the pattern emits will also be sent to ``(device, channel)``,
4040		starting from the next cycle rebuild.  Idempotent on ``(device, channel)``
4041		— calling with the same destination twice does not double-fan; calling
4042		again with a different ``drum_note_map`` re-points it in place.
4043
4044		Parameters:
4045			name: Function name of the pattern to mirror.
4046			device: Output device index (the integer returned from
4047				``midi_output()``; 0 = primary device).
4048			channel: MIDI channel using this composition's numbering convention
4049				(1-16 by default; 0-15 if ``zero_indexed_channels=True``).
4050			drum_note_map: Optional per-destination drum map.  When set, mirrored
4051				drum hits are re-resolved by name through it, so a named voice
4052				lands on this device's own note number — see the README
4053				"MIDI mirroring" section.
4054
4055		Bandwidth: each mirror adds another full copy of the pattern's events.
4056		See the README "MIDI mirroring" section for the tradeoffs.
4057		"""
4058
4059		if name not in self._running_patterns:
4060			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4061
4062		resolved_channel = self._resolve_channel(channel)
4063		prefix = (device, resolved_channel)
4064		entry: subsequence.pattern.MirrorSpec = prefix if drum_note_map is None else (device, resolved_channel, drum_note_map)
4065
4066		pattern = self._running_patterns[name]
4067
4068		# Mirror-to-self check: comparing the (device, channel) prefix against the
4069		# live pattern's resolved destination.  Unlike the decorator path this is
4070		# always concrete.
4071		if prefix == (pattern.device, pattern.channel):
4072			logger.warning(
4073				f"Mirror destination {prefix} matches '{name}'s primary destination "
4074				f"— every event will double-fire on this (device, channel).  This is almost "
4075				f"certainly unintended."
4076			)
4077
4078		# Idempotent on (device, channel): replace any existing entry for the same
4079		# destination (so its map can be re-pointed), else append.
4080		existing_index = next((idx for idx, e in enumerate(pattern.mirrors) if (e[0], e[1]) == prefix), None)
4081		if existing_index is None:
4082			pattern.mirrors.append(entry)
4083			logger.info(f"Mirror added: {name} -> device={device}, channel={resolved_channel}")
4084		elif pattern.mirrors[existing_index] != entry:
4085			pattern.mirrors[existing_index] = entry
4086			logger.info(f"Mirror updated: {name} -> device={device}, channel={resolved_channel}")
4087		else:
4088			logger.debug(f"Mirror already present on {name}: device={device}, channel={resolved_channel}")

Add a mirror destination to a running pattern.

Every note, CC, pitch bend, NRPN/RPN, program change, SysEx, and drone event the pattern emits will also be sent to (device, channel), starting from the next cycle rebuild. Idempotent on (device, channel) — calling with the same destination twice does not double-fan; calling again with a different drum_note_map re-points it in place.

Arguments:
  • name: Function name of the pattern to mirror.
  • device: Output device index (the integer returned from midi_output(); 0 = primary device).
  • channel: MIDI channel using this composition's numbering convention (1-16 by default; 0-15 if zero_indexed_channels=True).
  • drum_note_map: Optional per-destination drum map. When set, mirrored drum hits are re-resolved by name through it, so a named voice lands on this device's own note number — see the README "MIDI mirroring" section.

Bandwidth: each mirror adds another full copy of the pattern's events. See the README "MIDI mirroring" section for the tradeoffs.

def unmirror(self, name: str, device: int, channel: int) -> None:
4090	def unmirror (self, name: str, device: int, channel: int) -> None:
4091
4092		"""
4093		Remove a single mirror destination from a running pattern.
4094
4095		Matches on ``(device, channel)`` only — any attached ``drum_note_map`` is
4096		ignored.  Idempotent: silently does nothing if the destination is not
4097		currently mirrored.  The change applies on the next cycle rebuild.
4098		"""
4099
4100		if name not in self._running_patterns:
4101			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4102
4103		resolved_channel = self._resolve_channel(channel)
4104		prefix = (device, resolved_channel)
4105
4106		pattern = self._running_patterns[name]
4107
4108		filtered = [e for e in pattern.mirrors if (e[0], e[1]) != prefix]
4109		if len(filtered) != len(pattern.mirrors):
4110			pattern.mirrors[:] = filtered
4111			logger.info(f"Mirror removed: {name} -> device={device}, channel={resolved_channel}")
4112		else:
4113			logger.debug(f"unmirror() no-op on {name}: device={device}, channel={resolved_channel} not in mirrors")

Remove a single mirror destination from a running pattern.

Matches on (device, channel) only — any attached drum_note_map is ignored. Idempotent: silently does nothing if the destination is not currently mirrored. The change applies on the next cycle rebuild.

def unmirror_all(self, name: str) -> None:
4115	def unmirror_all (self, name: str) -> None:
4116
4117		"""
4118		Remove every mirror destination from a running pattern.
4119		"""
4120
4121		if name not in self._running_patterns:
4122			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4123
4124		pattern = self._running_patterns[name]
4125
4126		if pattern.mirrors:
4127			pattern.mirrors.clear()
4128			logger.info(f"All mirrors cleared on pattern: {name}")

Remove every mirror destination from a running pattern.

def tweak(self, name: str, **kwargs: Any) -> None:
4130	def tweak (self, name: str, **kwargs: typing.Any) -> None:
4131
4132		"""Override parameters for a running pattern.
4133
4134		Values set here are available inside the pattern's builder
4135		function via ``p.param()``.  They persist across rebuilds
4136		until explicitly changed or cleared.  Changes take effect
4137		on the next rebuild cycle.
4138
4139		Parameters:
4140			name: The function name of the pattern.
4141			``**kwargs``: Parameter names and their new values.
4142
4143		Example (from the live REPL)::
4144
4145			composition.tweak("bass", pitches=[48, 52, 55, 60])
4146		"""
4147
4148		if name not in self._running_patterns:
4149			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4150
4151		self._running_patterns[name]._tweaks.update(kwargs)
4152		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:
4154	def clear_tweak (self, name: str, *param_names: str) -> None:
4155
4156		"""Remove tweaked parameters from a running pattern.
4157
4158		If no parameter names are given, all tweaks for the pattern
4159		are cleared and every ``p.param()`` call reverts to its
4160		default.
4161
4162		Parameters:
4163			name: The function name of the pattern.
4164			*param_names: Specific parameter names to clear.  If
4165				omitted, all tweaks are removed.
4166		"""
4167
4168		if name not in self._running_patterns:
4169			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4170
4171		if not param_names:
4172			self._running_patterns[name]._tweaks.clear()
4173			logger.info(f"Cleared all tweaks for pattern '{name}'")
4174		else:
4175			for param_name in param_names:
4176				self._running_patterns[name]._tweaks.pop(param_name, None)
4177			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]:
4179	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
4180
4181		"""Return a copy of the current tweaks for a running pattern.
4182
4183		Parameters:
4184			name: The function name of the pattern.
4185		"""
4186
4187		if name not in self._running_patterns:
4188			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4189
4190		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:
4192	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
4193
4194		"""
4195		Register a custom function to run on a repeating beat-based cycle.
4196
4197		Subsequence automatically runs synchronous functions in a thread pool
4198		so they don't block the timing-critical MIDI clock. Async functions
4199		are run directly on the event loop.
4200
4201		Parameters:
4202			fn: The function to call.
4203			cycle_beats: How often to call it (e.g., 4 = every bar).
4204			reschedule_lookahead: How far in advance to schedule the next call.
4205			wait_for_initial: If True, run the function once during startup
4206				and wait for it to complete before playback begins. This
4207				ensures ``composition.data`` is populated before patterns
4208				first build. Implies ``defer=True`` for the repeating
4209				schedule.
4210			defer: If True, skip the pulse-0 fire and defer the first
4211				repeating call to just before the second cycle boundary.
4212
4213		Raises:
4214			RuntimeError: If called after ``play()`` has started — scheduled
4215				tasks register at startup, so a late registration would be
4216				silently ignored otherwise.
4217		"""
4218
4219		if self._sequencer.running:
4220			raise RuntimeError("schedule() must be called before play() - scheduled tasks register at startup")
4221
4222		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.
Raises:
  • RuntimeError: If called after play() has started — scheduled tasks register at startup, so a late registration would be silently ignored otherwise.
def form( self, sections: Union[Form, List[Any], Iterator[Tuple[str, int]], Dict[str, Tuple[int, Optional[List[Tuple[str, int]]]]]], loop: bool = False, start: Optional[str] = None, at_end: str = 'stop', key: Optional[str] = None, scale: Optional[str] = None) -> None:
4224	def form (
4225		self,
4226		sections: typing.Union[
4227			"subsequence.forms.Form",
4228			typing.List[typing.Any],
4229			typing.Iterator[typing.Tuple[str, int]],
4230			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
4231		],
4232		loop: bool = False,
4233		start: typing.Optional[str] = None,
4234		at_end: str = "stop",
4235		key: typing.Optional[str] = None,
4236		scale: typing.Optional[str] = None,
4237	) -> None:
4238
4239		"""
4240		Define the structure (sections) of the composition.
4241
4242		You can define form in four ways:
4243
4244		1. **Form value**: a frozen :class:`~subsequence.forms.Form` of
4245		   :class:`~subsequence.forms.Section` values — the payload home
4246		   (energy, key per section); editable, navigable.
4247		2. **Sequence (List)**: a fixed order of ``(name, bars)`` tuples
4248		   or Sections (lists coerce — they are the same form).
4249		3. **Graph (Dict)**: dynamic transitions based on weights.
4250		4. **Generator**: a Python generator that yields ``(name, bars)`` pairs.
4251
4252		Form-value and list forms are **navigable**: ``form_jump()`` and
4253		``form_next()`` work on them (the jump lands on the next occurrence
4254		of the name, wrapping).
4255
4256		Re-binding ``form()`` during playback takes effect at the next bar —
4257		the clock reads the current form state on every bar, so the new form
4258		advances from there (its first section plays from its first bar).
4259
4260		Parameters:
4261			sections: The form definition (Form, List, Dict, or Generator).
4262			loop: Sugar for ``at_end="loop"``.
4263			start: The section to start with (Graph mode only).
4264			at_end: What happens when a sequence form runs out —
4265				``"stop"`` (the form finishes and patterns see no section;
4266				default), ``"hold"`` (the final section repeats until
4267				navigated away from), or ``"loop"`` (start over).  Graphs
4268				end via their terminal sections instead.
4269			key: A form-level key — the **form tier** of the key-source
4270				chain (``Section.key`` overrides it; it overrides the
4271				composition key).  Re-anchors key-relative content for the
4272				whole form.  When *sections* is a ``Form`` value carrying its
4273				own ``key``, that value is used unless this argument overrides.
4274			scale: A form-level scale/mode, paired with ``key``.
4275
4276		Example:
4277			```python
4278			# A simple pop structure
4279			comp.form([
4280				("verse", 8),
4281				("chorus", 8),
4282				("verse", 8),
4283				("chorus", 16)
4284			])
4285
4286			# The same structure with payloads, held open at the end
4287			S = subsequence.Section
4288			comp.form(subsequence.Form([
4289				S("verse", 8, energy=0.5), S("chorus", 8, energy=0.9),
4290			]), at_end="hold")
4291			```
4292		"""
4293
4294		# Seed FormState at form() time (per-call salt) so build-time walks —
4295		# the frozen clones form_freeze will take — are deterministic without
4296		# play(); the play-time stream is re-dealt name-keyed in _run().
4297		self._form_count += 1
4298
4299		self._form_state = subsequence.form_state.FormState(
4300			sections,
4301			loop = loop,
4302			start = start,
4303			rng = self._stream(f"form:{self._form_count}"),
4304			at_end = at_end,
4305		)
4306
4307		# A Form value carries energy payloads — that counts as an energy
4308		# source for the min_energy registration check in _run().
4309		self._form_has_payload = isinstance(sections, subsequence.forms.Form) or (
4310			isinstance(sections, list) and any(isinstance(element, subsequence.forms.Section) for element in sections)
4311		)
4312
4313		# Form-tier key/scale: an explicit argument wins; otherwise a Form
4314		# value's own key/scale seeds the tier.  Re-binding the form drops any
4315		# stale per-section resolution cache.
4316		if isinstance(sections, subsequence.forms.Form):
4317			self._form_key = key if key is not None else sections.key
4318			self._form_scale = scale if scale is not None else sections.scale
4319		else:
4320			self._form_key = key
4321			self._form_scale = scale
4322
4323		self._resolved_section_cache = {}

Define the structure (sections) of the composition.

You can define form in four ways:

  1. Form value: a frozen ~subsequence.forms.Form of ~subsequence.forms.Section values — the payload home (energy, key per section); editable, navigable.
  2. Sequence (List): a fixed order of (name, bars) tuples or Sections (lists coerce — they are the same form).
  3. Graph (Dict): dynamic transitions based on weights.
  4. Generator: a Python generator that yields (name, bars) pairs.

Form-value and list forms are navigable: form_jump() and form_next() work on them (the jump lands on the next occurrence of the name, wrapping).

Re-binding form() during playback takes effect at the next bar — the clock reads the current form state on every bar, so the new form advances from there (its first section plays from its first bar).

Arguments:
  • sections: The form definition (Form, List, Dict, or Generator).
  • loop: Sugar for at_end="loop".
  • start: The section to start with (Graph mode only).
  • at_end: What happens when a sequence form runs out — "stop" (the form finishes and patterns see no section; default), "hold" (the final section repeats until navigated away from), or "loop" (start over). Graphs end via their terminal sections instead.
  • key: A form-level key — the form tier of the key-source chain (Section.key overrides it; it overrides the composition key). Re-anchors key-relative content for the whole form. When sections is a Form value carrying its own key, that value is used unless this argument overrides.
  • scale: A form-level scale/mode, paired with key.
Example:
# A simple pop structure
comp.form([
        ("verse", 8),
        ("chorus", 8),
        ("verse", 8),
        ("chorus", 16)
])

# The same structure with payloads, held open at the end
S = subsequence.Section
comp.form(subsequence.Form([
        S("verse", 8, energy=0.5), S("chorus", 8, energy=0.9),
]), at_end="hold")
def form_freeze(self, sections: Optional[int] = None) -> Form:
4325	def form_freeze (self, sections: typing.Optional[int] = None) -> "subsequence.forms.Form":
4326
4327		"""Freeze the graph form's walk into an editable :class:`~subsequence.forms.Form`.
4328
4329		Walks a **clone** of the live form state — the same RNG state, so the
4330		frozen path is exactly the path the live graph would have played —
4331		and returns it as a Form value: inspect it, edit it
4332		(``path.replace(3, bars=16)``), and rebind it with
4333		``composition.form(path, at_end=...)``.  The live form state is
4334		untouched (rebinding replaces it).
4335
4336		Parameters:
4337			sections: Number of sections to freeze.  Without it, the walk
4338				runs until a terminal section; a graph with no terminal
4339				sections requires ``sections=`` explicitly.
4340
4341		Raises:
4342			ValueError: If no graph form is bound (a list form is already a
4343				frozen sequence), the form has already finished, or the walk
4344				cannot terminate.
4345
4346		Example::
4347
4348			composition.form({...}, start="intro")
4349			path = composition.form_freeze()          # the walk, frozen
4350			composition.form(path, at_end="stop")     # rebind the editable value
4351		"""
4352
4353		fs = self._form_state
4354
4355		if fs is None or fs._graph is None or fs._section_bars is None:
4356			raise ValueError(
4357				"form_freeze() freezes a graph form's walk — call form() with a dict first "
4358				"(a list form is already a frozen sequence)"
4359			)
4360
4361		if fs._current is None:
4362			raise ValueError("the form has already finished — nothing left to freeze")
4363
4364		if sections is not None and sections < 1:
4365			raise ValueError("sections must be at least 1")
4366
4367		if sections is None and not fs._terminal_sections:
4368			raise ValueError(
4369				"this graph has no terminal section, so the walk would never end — "
4370				"pass sections=n to bound it"
4371			)
4372
4373		# Clone the RNG state: the frozen walk reproduces the live form's
4374		# future draws without consuming them.
4375		rng = random.Random()
4376		rng.setstate(fs._rng.getstate())
4377
4378		walked = [fs._current]
4379		next_name = fs._next_section_name		# already decided by the live state
4380
4381		while next_name is not None:
4382			if sections is not None and len(walked) >= sections:
4383				break
4384			if sections is None and len(walked) >= 10000:
4385				raise ValueError(
4386					"form_freeze() walked 10000 sections without reaching a terminal — "
4387					"the terminals look unreachable; pass sections=n to bound the walk"
4388				)
4389
4390			walked.append(subsequence.forms.Section(name = next_name, bars = fs._section_bars[next_name]))
4391			next_name = None if next_name in fs._terminal_sections else fs._graph.choose_next(next_name, rng)
4392
4393		# Carry the form-tier key/scale onto the frozen value so a freeze →
4394		# rebind round-trip is lossless (an explicit form(key=) on rebind
4395		# still overrides).
4396		return subsequence.forms.Form(walked, key = self._form_key, scale = self._form_scale)

Freeze the graph form's walk into an editable ~subsequence.forms.Form.

Walks a clone of the live form state — the same RNG state, so the frozen path is exactly the path the live graph would have played — and returns it as a Form value: inspect it, edit it (path.replace(3, bars=16)), and rebind it with composition.form(path, at_end=...). The live form state is untouched (rebinding replaces it).

Arguments:
  • sections: Number of sections to freeze. Without it, the walk runs until a terminal section; a graph with no terminal sections requires sections= explicitly.
Raises:
  • ValueError: If no graph form is bound (a list form is already a frozen sequence), the form has already finished, or the walk cannot terminate.

Example::

    composition.form({...}, start="intro")
    path = composition.form_freeze()          # the walk, frozen
    composition.form(path, at_end="stop")     # rebind the editable value
def energy(self, energies: Dict[str, Union[float, Tuple[float, float]]]) -> None:
4398	def energy (self, energies: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]]) -> None:
4399
4400		"""Set per-section energy — the arranging dial, as one plain dict.
4401
4402		``{"verse": 0.5, "chorus": 0.9, "build": (0.3, 1.0)}`` — a float is
4403		the section's level; a ``(start, end)`` tuple interpolates across the
4404		section (a build).  Patterns read ``p.energy`` (0.5 when nothing is
4405		configured) and gate themselves, or declare ``min_energy=`` on
4406		``pattern()`` for automatic muting.
4407
4408		The dict **overrides** any energy payload carried by bound
4409		:class:`~subsequence.forms.Section` values — it is the later,
4410		performance-level dial.  Re-calling replaces the whole mapping
4411		(idempotent, live-reload friendly).
4412
4413		Example::
4414
4415			composition.energy({"intro": 0.2, "verse": 0.55, "drop": 0.95})
4416		"""
4417
4418		validated: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]] = {}
4419
4420		for name, value in energies.items():
4421			if isinstance(value, tuple):
4422				if len(value) != 2:
4423					raise ValueError(f"energy ramp for {name!r} must be (start, end), got {value!r}")
4424				start_level, end_level = float(value[0]), float(value[1])
4425				for level in (start_level, end_level):
4426					if not 0.0 <= level <= 1.0:
4427						raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4428				validated[name] = (start_level, end_level)
4429			else:
4430				level = float(value)
4431				if not 0.0 <= level <= 1.0:
4432					raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4433				validated[name] = level
4434
4435		self._energy_map = validated

Set per-section energy — the arranging dial, as one plain dict.

{"verse": 0.5, "chorus": 0.9, "build": (0.3, 1.0)} — a float is the section's level; a (start, end) tuple interpolates across the section (a build). Patterns read p.energy (0.5 when nothing is configured) and gate themselves, or declare min_energy= on pattern() for automatic muting.

The dict overrides any energy payload carried by bound ~subsequence.forms.Section values — it is the later, performance-level dial. Re-calling replaces the whole mapping (idempotent, live-reload friendly).

Example::

    composition.energy({"intro": 0.2, "verse": 0.55, "drop": 0.95})
def on_section(self, callback: Callable[..., Any]) -> None:
4467	def on_section (self, callback: typing.Callable[..., typing.Any]) -> None:
4468
4469		"""Register a callback fired on every section change.
4470
4471		The callback receives the new :class:`~subsequence.form_state.SectionInfo`
4472		(or ``None`` when the form finishes).  It fires from the form clock,
4473		one lookahead-beat **early** — in time to affect the new section's
4474		first patterns — and once at play start for the opening section.
4475
4476		Example::
4477
4478			composition.on_section(lambda info: print(f"now: {info.name if info else 'end'}"))
4479		"""
4480
4481		self.on_event("section", callback)

Register a callback fired on every section change.

The callback receives the new ~subsequence.form_state.SectionInfo (or None when the form finishes). It fires from the form clock, one lookahead-beat early — in time to affect the new section's first patterns — and once at play start for the opening section.

Example::

    composition.on_section(lambda info: print(f"now: {info.name if info else 'end'}"))
def transition( self, before: str, fill: Optional[Any] = None, channel: Optional[int] = None, beat: float = 0.0, mute: Optional[List[str]] = None, beats: Optional[float] = None, drum_note_map: Optional[Dict[str, int]] = None, device: Union[int, str, NoneType] = None) -> None:
4483	def transition (
4484		self,
4485		before: str,
4486		fill: typing.Optional[typing.Any] = None,
4487		channel: typing.Optional[int] = None,
4488		beat: float = 0.0,
4489		mute: typing.Optional[typing.List[str]] = None,
4490		beats: typing.Optional[float] = None,
4491		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4492		device: subsequence.midi_utils.DeviceId = None,
4493	) -> None:
4494
4495		"""Declare boundary material — the automatic fill or mute, one line.
4496
4497		``before`` names the incoming section (``"chorus"``), or ``"*"`` for
4498		any *different* section (repeats don't fire it).  Two actions,
4499		combinable:
4500
4501		- ``fill=`` (+ ``channel=``, ``beat=``): a Motif played in the last
4502		  bar before the boundary, starting at ``beat`` of that bar.  Drum
4503		  names resolve through ``drum_note_map=`` if given, otherwise the
4504		  map is borrowed from a registered pattern on the same channel.
4505		- ``mute=`` (+ ``beats=``): pattern names muted over the approach
4506		  and unmuted at the boundary.  Muting is **bar-granular** (the
4507		  existing rule), so ``beats`` rounds up to whole bars.  Performer
4508		  mutes win: a pattern you muted yourself stays muted.
4509
4510		Transitions stack — call once per rule.  Registration is additive
4511		and idempotent per identical rule.
4512
4513		Example::
4514
4515			composition.transition(before="*", fill=FILL, channel=10, beat=2.0)
4516			composition.transition(before="drop", mute=["pads"], beats=4)
4517		"""
4518
4519		if fill is None and mute is None:
4520			raise ValueError("transition() needs fill= and/or mute= — it declares what happens at the boundary")
4521
4522		if fill is not None:
4523			if channel is None:
4524				raise ValueError("transition(fill=) needs channel= — the fill must land somewhere")
4525			if not hasattr(fill, "events") or not hasattr(fill, "length"):
4526				raise TypeError(f"fill must be a Motif-like value with .events/.length, got {type(fill).__name__}")
4527
4528		if mute is not None and beats is None:
4529			beats = float(self.time_signature[0])		# one bar by default
4530
4531		rule = _Transition(
4532			before = before,
4533			fill = fill,
4534			channel = self._resolve_channel(channel) if channel is not None else None,
4535			beat = float(beat),
4536			mute = list(mute) if mute is not None else None,
4537			beats = beats,
4538			drum_note_map = drum_note_map,
4539			device = device,			# resolved at fire time — names aren't known until play()
4540		)
4541
4542		if rule not in self._transitions:
4543			self._transitions.append(rule)

Declare boundary material — the automatic fill or mute, one line.

before names the incoming section ("chorus"), or "*" for any different section (repeats don't fire it). Two actions, combinable:

  • fill= (+ channel=, beat=): a Motif played in the last bar before the boundary, starting at beat of that bar. Drum names resolve through drum_note_map= if given, otherwise the map is borrowed from a registered pattern on the same channel.
  • mute= (+ beats=): pattern names muted over the approach and unmuted at the boundary. Muting is bar-granular (the existing rule), so beats rounds up to whole bars. Performer mutes win: a pattern you muted yourself stays muted.

Transitions stack — call once per rule. Registration is additive and idempotent per identical rule.

Example::

    composition.transition(before="*", fill=FILL, channel=10, beat=2.0)
    composition.transition(before="drop", mute=["pads"], beats=4)
def pattern( self, channel: int, beats: Optional[float] = None, bars: Optional[float] = None, steps: Optional[float] = None, step_duration: Optional[float] = None, drum_note_map: Optional[Dict[str, int]] = None, cc_name_map: Optional[Dict[str, int]] = None, nrpn_name_map: Optional[Dict[str, int]] = None, reschedule_lookahead: float = 1, voice_leading: bool = False, device: Union[int, str, NoneType] = None, mirrors: Optional[Iterable[Union[Tuple[int, int], Tuple[int, int, Optional[Dict[str, int]]]]]] = None, min_energy: Optional[float] = None) -> Callable:
4722	def pattern (
4723		self,
4724		channel: int,
4725		beats: typing.Optional[float] = None,
4726		bars: typing.Optional[float] = None,
4727		steps: typing.Optional[float] = None,
4728		step_duration: typing.Optional[float] = None,
4729		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4730		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4731		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4732		reschedule_lookahead: float = 1,
4733		voice_leading: bool = False,
4734		device: subsequence.midi_utils.DeviceId = None,
4735		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4736		min_energy: typing.Optional[float] = None,
4737	) -> typing.Callable:
4738
4739		"""
4740		Register a function as a repeating MIDI pattern.
4741
4742		The decorated function will be called once per cycle to 'rebuild' its
4743		content. This allows for generative logic that evolves over time.
4744
4745		Two ways to specify pattern length:
4746
4747		- **Duration mode** (default): use ``beats=`` or ``bars=``.
4748		  The grid defaults to sixteenth-note resolution.
4749		- **Step mode**: use ``steps=`` paired with ``step_duration=``.
4750		  The grid equals the step count, so ``p.hit_steps()`` indices map
4751		  directly to steps.
4752
4753		Parameters:
4754			channel: MIDI channel. By default uses 1-based numbering (1-16).
4755				Set ``zero_indexed_channels=True`` on the ``Composition`` to use
4756				0-based numbering (0-15), matching the raw MIDI protocol, instead.
4757			beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar.
4758			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4). ``bars=2`` = 8 beats.
4759			steps: Step count for step mode. Requires ``step_duration=``.
4760			step_duration: Duration of one step in beats (e.g. ``dur.SIXTEENTH``).
4761				Requires ``steps=``.
4762			drum_note_map: Optional mapping for drum instruments.
4763			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4764				Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``.
4765			nrpn_name_map: Optional mapping of NRPN parameter names (strings) to
4766				14-bit parameter numbers (0–16383).  Enables string-based names
4767				in ``p.nrpn()`` and ``p.nrpn_ramp()`` — typically a
4768				device-specific dictionary (e.g. Sequential Take 5's
4769				``Osc1FreqFine`` → 9).
4770			reschedule_lookahead: Beats in advance to compute the next cycle.
4771			voice_leading: If True, chords in this pattern will automatically
4772				use inversions that minimize voice movement.
4773			mirrors: Optional list of additional ``(device, channel)`` destinations
4774				to duplicate every event from this pattern onto.  Notes, CCs, pitch
4775				bend, NRPN/RPN bursts, program changes, SysEx, and drone events are
4776				all mirrored; OSC events are not (OSC is not bound to a MIDI port).
4777				``device`` is the integer index returned by ``midi_output()`` (0 =
4778				primary).  ``channel`` follows this composition's channel-numbering
4779				convention.  See also ``mirror()`` / ``unmirror()`` for live toggling.
4780			min_energy: Automatic energy gating — the pattern is silent while
4781				the current section's energy (``composition.energy()`` dict,
4782				or the bound Section payload) is below this threshold.
4783				Composes with ``mute()``: a performer mute always wins.
4784
4785		Example:
4786			```python
4787			@comp.pattern(channel=1, beats=4)
4788			def chords (p):
4789				p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)
4790
4791			@comp.pattern(channel=1, bars=2)
4792			def long_phrase (p):
4793				...
4794
4795			@comp.pattern(channel=1, steps=6, step_duration=dur.SIXTEENTH)
4796			def riff (p):
4797				p.sequence(steps=[0, 1, 3, 5], pitches=60)
4798			```
4799		"""
4800
4801		channel = self._resolve_channel(channel)
4802
4803		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4804
4805		# Resolve device string name to index if possible now; otherwise store
4806		# the raw DeviceId and resolve it in _run() once all devices are open.
4807		resolved_device: subsequence.midi_utils.DeviceId = device
4808
4809		# Mirror-to-self check is only reliable when the primary device is a
4810		# concrete integer at decoration time.  ``None`` resolves to device 0
4811		# downstream, so we treat it as 0 here too.  Strings are deferred to
4812		# ``_run()`` and we skip the check for them.
4813		primary: typing.Optional[typing.Tuple[int, int]]
4814		if isinstance(resolved_device, str):
4815			primary = None
4816		else:
4817			primary = (resolved_device if resolved_device is not None else 0, channel)
4818		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4819
4820		def decorator (fn: typing.Callable) -> typing.Callable:
4821
4822			"""
4823			Wrap the builder function and register it as a pending pattern.
4824			During live sessions, hot-swap an existing pattern's builder instead.
4825			"""
4826
4827			# Record this declaration so the live-reload deletion diff knows the
4828			# pattern is still present in the source (see _apply_source_async).
4829			self._declared_names.add(fn.__name__)
4830
4831			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
4832			if self._is_live and fn.__name__ in self._running_patterns:
4833				running = self._running_patterns[fn.__name__]
4834				running._builder_fn = fn
4835				running._wants_chord = _fn_has_parameter(fn, "chord")
4836				logger.info(f"Hot-swapped pattern: {fn.__name__}")
4837				return fn
4838
4839			# Names key the seeded stream, mutes, tweaks, and reroll/lock — a
4840			# duplicate means two scheduled copies sharing one stream with
4841			# only one reachable by name.  Warn loudly at registration.
4842			if any(existing.builder_fn.__name__ == fn.__name__ for existing in self._pending_patterns):
4843				logger.warning(
4844					f"Duplicate pattern name '{fn.__name__}': both copies will be "
4845					f"scheduled, they share one seeded stream, and only one is "
4846					f"reachable by name — rename one of them."
4847				)
4848
4849			pending = _PendingPattern(
4850				builder_fn = fn,
4851				channel = channel,  # already resolved to 0-indexed
4852				length = beat_length,
4853				default_grid = default_grid,
4854				drum_note_map = drum_note_map,
4855				cc_name_map = cc_name_map,
4856				nrpn_name_map = nrpn_name_map,
4857				reschedule_lookahead = reschedule_lookahead,
4858				voice_leading = voice_leading,
4859				# For int/None: resolve immediately.  For str: store 0 as
4860				# placeholder; _resolve_pending_devices() fixes it in _run().
4861				device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device,
4862				raw_device = resolved_device,
4863				mirrors = resolved_mirrors,
4864				min_energy = min_energy,
4865			)
4866
4867			self._pending_patterns.append(pending)
4868
4869			return fn
4870
4871		return decorator

Register a function as a repeating MIDI pattern.

The decorated function will be called once per cycle to 'rebuild' its content. This allows for generative logic that evolves over time.

Two ways to specify pattern length:

  • Duration mode (default): use beats= or bars=. The grid defaults to sixteenth-note resolution.
  • Step mode: use steps= paired with step_duration=. The grid equals the step count, so p.hit_steps() indices map directly to steps.
Arguments:
  • channel: MIDI channel. By default uses 1-based numbering (1-16). Set zero_indexed_channels=True on the Composition to use 0-based numbering (0-15), matching the raw MIDI protocol, instead.
  • beats: Duration in beats (quarter notes). beats=4 = 1 bar.
  • bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4). bars=2 = 8 beats.
  • steps: Step count for step mode. Requires step_duration=.
  • step_duration: Duration of one step in beats (e.g. dur.SIXTEENTH). Requires steps=.
  • drum_note_map: Optional mapping for drum instruments.
  • cc_name_map: Optional mapping of CC names to MIDI CC numbers. Enables string-based CC names in p.cc() and p.cc_ramp().
  • nrpn_name_map: Optional mapping of NRPN parameter names (strings) to 14-bit parameter numbers (0–16383). Enables string-based names in p.nrpn() and p.nrpn_ramp() — typically a device-specific dictionary (e.g. Sequential Take 5's Osc1FreqFine → 9).
  • reschedule_lookahead: Beats in advance to compute the next cycle.
  • voice_leading: If True, chords in this pattern will automatically use inversions that minimize voice movement.
  • mirrors: Optional list of additional (device, channel) destinations to duplicate every event from this pattern onto. Notes, CCs, pitch bend, NRPN/RPN bursts, program changes, SysEx, and drone events are all mirrored; OSC events are not (OSC is not bound to a MIDI port). device is the integer index returned by midi_output() (0 = primary). channel follows this composition's channel-numbering convention. See also mirror() / unmirror() for live toggling.
  • min_energy: Automatic energy gating — the pattern is silent while the current section's energy (composition.energy() dict, or the bound Section payload) is below this threshold. Composes with mute(): a performer mute always wins.
Example:
@comp.pattern(channel=1, beats=4)
def chords (p):
        p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)

@comp.pattern(channel=1, bars=2)
def long_phrase (p):
        ...

@comp.pattern(channel=1, steps=6, step_duration=dur.SIXTEENTH)
def riff (p):
        p.sequence(steps=[0, 1, 3, 5], pitches=60)
def layer( self, *builder_fns: Callable, channel: int, beats: Optional[float] = None, bars: Optional[float] = None, steps: Optional[float] = None, step_duration: Optional[float] = None, drum_note_map: Optional[Dict[str, int]] = None, cc_name_map: Optional[Dict[str, int]] = None, nrpn_name_map: Optional[Dict[str, int]] = None, reschedule_lookahead: float = 1, voice_leading: bool = False, device: Union[int, str, NoneType] = None, mirrors: Optional[Iterable[Union[Tuple[int, int], Tuple[int, int, Optional[Dict[str, int]]]]]] = None) -> None:
4873	def layer (
4874		self,
4875		*builder_fns: typing.Callable,
4876		channel: int,
4877		beats: typing.Optional[float] = None,
4878		bars: typing.Optional[float] = None,
4879		steps: typing.Optional[float] = None,
4880		step_duration: typing.Optional[float] = None,
4881		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4882		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4883		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4884		reschedule_lookahead: float = 1,
4885		voice_leading: bool = False,
4886		device: subsequence.midi_utils.DeviceId = None,
4887		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4888	) -> None:
4889
4890		"""
4891		Combine multiple functions into a single MIDI pattern.
4892
4893		This is useful for composing complex patterns out of reusable
4894		building blocks (e.g., a 'kick' function and a 'snare' function).
4895
4896		See ``pattern()`` for the full description of ``beats``, ``bars``,
4897		``steps``, and ``step_duration``.
4898
4899		Parameters:
4900			builder_fns: One or more pattern builder functions.
4901			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
4902			beats: Duration in beats (quarter notes).
4903			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
4904			steps: Step count for step mode. Requires ``step_duration=``.
4905			step_duration: Duration of one step in beats. Requires ``steps=``.
4906			drum_note_map: Optional mapping for drum instruments.
4907			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4908			nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit
4909				parameter numbers.
4910			reschedule_lookahead: Beats in advance to compute the next cycle.
4911			voice_leading: If True, chords use smooth voice leading.
4912			mirrors: Optional list of additional ``(device, channel)`` destinations
4913				to duplicate every event onto.  See ``pattern()`` for details.
4914		"""
4915
4916		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4917
4918		# Resolve channel up-front so the mirror-to-self check has the canonical
4919		# primary form to compare against.
4920		resolved_channel = self._resolve_channel(channel)
4921
4922		# See pattern() for the same comment about None / str handling.
4923		primary: typing.Optional[typing.Tuple[int, int]]
4924		if isinstance(device, str):
4925			primary = None
4926		else:
4927			primary = (device if device is not None else 0, resolved_channel)
4928		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4929
4930		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
4931
4932		if wants_chord:
4933
4934			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
4935
4936				for fn in builder_fns:
4937					if _fn_has_parameter(fn, "chord"):
4938						fn(p, chord)
4939					else:
4940						fn(p)
4941
4942		else:
4943
4944			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
4945
4946				for fn in builder_fns:
4947					fn(p)
4948
4949		# Give the merged builder a stable, unique name derived from its
4950		# components so multiple layer() calls don't all register under
4951		# "merged_builder" and collide in _running_patterns (which made
4952		# mute/tweak/unregister/live_info reach only the LAST layer).  "+" can't
4953		# appear in a Python identifier, so this never clashes with a real
4954		# pattern function's name.
4955		base_name = ("+".join(fn.__name__ for fn in builder_fns) or "layer") + f"@ch{resolved_channel}"
4956		merged_name = base_name
4957		suffix = 2
4958
4959		# Two layers with the same components (e.g. on different saves of a
4960		# live file) must map to the same names pass-over-pass, while two
4961		# DIFFERENT layers sharing components in one pass must not collide.
4962		while merged_name in self._declared_names:
4963			merged_name = f"{base_name}#{suffix}"
4964			suffix += 1
4965
4966		merged_builder.__name__ = merged_name
4967
4968		# Record the declaration for the live-reload deletion diff, and hot-swap
4969		# in place when this layer is already running so a reload picks up edits
4970		# to the component functions without losing the pattern's cycle count,
4971		# tweaks, or mirrors (mirrors the pattern() decorator's hot-swap).
4972		self._declared_names.add(merged_builder.__name__)
4973
4974		if self._is_live and merged_builder.__name__ in self._running_patterns:
4975			running = self._running_patterns[merged_builder.__name__]
4976			running._builder_fn = merged_builder
4977			running._wants_chord = wants_chord
4978			logger.info(f"Hot-swapped layer: {merged_builder.__name__}")
4979			return
4980
4981		pending = _PendingPattern(
4982			builder_fn = merged_builder,
4983			channel = resolved_channel,  # already resolved to 0-indexed above
4984			length = beat_length,
4985			default_grid = default_grid,
4986			drum_note_map = drum_note_map,
4987			cc_name_map = cc_name_map,
4988			nrpn_name_map = nrpn_name_map,
4989			reschedule_lookahead = reschedule_lookahead,
4990			voice_leading = voice_leading,
4991			mirrors = resolved_mirrors,
4992			device = 0 if (device is None or isinstance(device, str)) else device,
4993			raw_device = device,
4994		)
4995
4996		self._pending_patterns.append(pending)

Combine multiple functions into a single MIDI pattern.

This is useful for composing complex patterns out of reusable building blocks (e.g., a 'kick' function and a 'snare' function).

See pattern() for the full description of beats, bars, steps, and step_duration.

Arguments:
  • builder_fns: One or more pattern builder functions.
  • channel: MIDI channel (1-16, or 0-15 with zero_indexed_channels=True).
  • beats: Duration in beats (quarter notes).
  • bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
  • steps: Step count for step mode. Requires step_duration=.
  • step_duration: Duration of one step in beats. Requires steps=.
  • drum_note_map: Optional mapping for drum instruments.
  • cc_name_map: Optional mapping of CC names to MIDI CC numbers.
  • nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit parameter numbers.
  • reschedule_lookahead: Beats in advance to compute the next cycle.
  • voice_leading: If True, chords use smooth voice leading.
  • mirrors: Optional list of additional (device, channel) destinations to duplicate every event onto. See pattern() for details.
def chords( self, *, channel: int, progression: Union[str, Progression, Sequence[Any]], harmonic_rhythm: Union[int, float, List[float], subsequence.harmonic_rhythm.HarmonicRhythm], bars: Optional[float] = None, beats: Optional[float] = None, voicing: Union[int, Tuple[int, int]] = (3, 4), velocity: Union[int, Tuple[int, int]] = 90, detached: Optional[float] = None, root: int = 60, key: Optional[str] = None, seed: Optional[int] = None, device: Union[int, str, NoneType] = None, mirrors: Optional[Iterable[Union[Tuple[int, int], Tuple[int, int, Optional[Dict[str, int]]]]]] = None) -> Progression:
4998	def chords (
4999		self,
5000		*,
5001		channel: int,
5002		progression: subsequence.progressions.ProgressionSource,
5003		harmonic_rhythm: subsequence.progressions.HarmonicRhythmSpec,
5004		bars: typing.Optional[float] = None,
5005		beats: typing.Optional[float] = None,
5006		voicing: subsequence.progressions.VoicingSpec = (3, 4),
5007		velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY,
5008		detached: typing.Optional[float] = None,
5009		root: int = 60,
5010		key: typing.Optional[str] = None,
5011		seed: typing.Optional[int] = None,
5012		device: subsequence.midi_utils.DeviceId = None,
5013		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5014	) -> subsequence.progressions.Progression:
5015
5016		"""Declare a self-contained chord part: a progression at a chosen harmonic rhythm.
5017
5018		The one-call form of ``p.progression()`` — it registers a pattern on
5019		*channel* that plays *progression* across *bars* (or *beats*), each chord
5020		lasting a length drawn from *harmonic_rhythm* (the musical term for how often
5021		the chords change).  It needs no ``composition.harmony()`` call and, with an
5022		explicit chord list or a ``key=``, no composition key either — so a
5023		drums-plus-one-chord-part sketch stays simple.
5024
5025		The progression is realised once, up front, and the same timeline plays every
5026		cycle (a stable phrase).  That timeline is returned so you can see exactly what
5027		was chosen — ``print(comp.chords(...))``.
5028
5029		Parameters:
5030			channel: MIDI channel for the chord part.
5031			progression: A chord-graph style name to generate from, or an explicit list
5032				of chords (``Chord`` objects or names like ``["Cm7", "Dbmaj7"]``).
5033			harmonic_rhythm: How long each chord lasts — a number, a list of lengths,
5034				or ``between(low, high, step=...)``.  See ``p.progression()``.
5035			bars / beats: Length of the part (defaults to 4 beats if neither is given).  ``bars`` uses the
5036				composition's time signature.
5037			voicing: Notes per chord — an int, or a ``(low, high)`` range (e.g. ``(3, 4)``).
5038			velocity: MIDI velocity, or a ``(low, high)`` tuple for per-voice humanisation.
5039			detached: Beats of silence before each next chord (``duration = length - detached``).
5040			root: MIDI root the voicings are centred on (e.g. 48 = C3).
5041			key: Key for a generated progression; defaults to the composition key.
5042			seed: Seed for the (otherwise fixed) realisation; defaults to the
5043				composition seed, so the part is reproducible.
5044			device: Optional output-device override.
5045			mirrors: Optional additional ``(device, channel)`` destinations.
5046
5047		Returns:
5048			The realised :class:`~subsequence.progressions.Progression`.
5049		"""
5050
5051		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5052		resolved_channel = self._resolve_channel(channel)
5053		resolved_key = key if key is not None else self.key
5054
5055		rng = random.Random(seed if seed is not None else self._seed)
5056		timeline = subsequence.progressions.realize(
5057			source = progression,
5058			harmonic_rhythm = harmonic_rhythm,
5059			key = resolved_key,
5060			length = beat_length,
5061			rng = rng,
5062			scale = self.scale or "ionian",
5063		)
5064
5065		captured_root = root
5066		captured_velocity = velocity
5067		captured_detached = detached
5068		captured_voicing = voicing
5069
5070		def chords_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5071
5072			"""Replay the realised timeline as block chords each cycle (voicing per chord)."""
5073
5074			for chord, start, length in timeline:
5075				ring = length - captured_detached if (captured_detached and captured_detached < length) else length
5076				voices = subsequence.progressions.resolve_voices(captured_voicing, p.rng)
5077				p.chord(chord, root=captured_root, beat=start, duration=ring, count=voices, velocity=captured_velocity)
5078
5079		# Unique, stable name so multiple chord parts don't collide in
5080		# _running_patterns — including two parts on the SAME channel, which
5081		# get a deterministic #2/#3 suffix in declaration order.
5082		base_name = f"chords@ch{resolved_channel}"
5083		chords_name = base_name
5084		suffix = 2
5085
5086		while chords_name in self._declared_names:
5087			chords_name = f"{base_name}#{suffix}"
5088			suffix += 1
5089
5090		chords_builder.__name__ = chords_name
5091		self._declared_names.add(chords_name)
5092
5093		primary: typing.Optional[typing.Tuple[int, int]]
5094		if isinstance(device, str):
5095			primary = None
5096		else:
5097			primary = (device if device is not None else 0, resolved_channel)
5098		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5099
5100		if self._is_live and chords_builder.__name__ in self._running_patterns:
5101			running = self._running_patterns[chords_builder.__name__]
5102			running._builder_fn = chords_builder
5103			running._wants_chord = False
5104			logger.info(f"Hot-swapped chords: {chords_builder.__name__}")
5105			return timeline
5106
5107		pending = _PendingPattern(
5108			builder_fn = chords_builder,
5109			channel = resolved_channel,
5110			length = beat_length,
5111			default_grid = default_grid,
5112			drum_note_map = None,
5113			reschedule_lookahead = 1,
5114			voice_leading = False,
5115			mirrors = resolved_mirrors,
5116			device = 0 if (device is None or isinstance(device, str)) else device,
5117			raw_device = device,
5118		)
5119		self._pending_patterns.append(pending)
5120		return timeline

Declare a self-contained chord part: a progression at a chosen harmonic rhythm.

The one-call form of p.progression() — it registers a pattern on channel that plays progression across bars (or beats), each chord lasting a length drawn from harmonic_rhythm (the musical term for how often the chords change). It needs no composition.harmony() call and, with an explicit chord list or a key=, no composition key either — so a drums-plus-one-chord-part sketch stays simple.

The progression is realised once, up front, and the same timeline plays every cycle (a stable phrase). That timeline is returned so you can see exactly what was chosen — print(comp.chords(...)).

Arguments:
  • channel: MIDI channel for the chord part.
  • progression: A chord-graph style name to generate from, or an explicit list of chords (Chord objects or names like ["Cm7", "Dbmaj7"]).
  • harmonic_rhythm: How long each chord lasts — a number, a list of lengths, or between(low, high, step=...). See p.progression().
  • bars / beats: Length of the part (defaults to 4 beats if neither is given). bars uses the composition's time signature.
  • voicing: Notes per chord — an int, or a (low, high) range (e.g. (3, 4)).
  • velocity: MIDI velocity, or a (low, high) tuple for per-voice humanisation.
  • detached: Beats of silence before each next chord (duration = length - detached).
  • root: MIDI root the voicings are centred on (e.g. 48 = C3).
  • key: Key for a generated progression; defaults to the composition key.
  • seed: Seed for the (otherwise fixed) realisation; defaults to the composition seed, so the part is reproducible.
  • device: Optional output-device override.
  • mirrors: Optional additional (device, channel) destinations.
Returns:

The realised ~subsequence.progressions.Progression.

def phrase_part( self, *, channel: int, part: Optional[str] = None, root: int = 60, bars: Optional[float] = None, beats: Optional[float] = None, velocity: Union[int, Tuple[int, int], NoneType] = None, fit: Optional[float] = None, device: Union[int, str, NoneType] = None, mirrors: Optional[Iterable[Union[Tuple[int, int], Tuple[int, int, Optional[Dict[str, int]]]]]] = None) -> None:
5122	def phrase_part (
5123		self,
5124		*,
5125		channel: int,
5126		part: typing.Optional[str] = None,
5127		root: int = 60,
5128		bars: typing.Optional[float] = None,
5129		beats: typing.Optional[float] = None,
5130		velocity: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
5131		fit: typing.Optional[float] = None,
5132		device: subsequence.midi_utils.DeviceId = None,
5133		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5134	) -> None:
5135
5136		"""Declare a part that plays each section's bound Motif/Phrase.
5137
5138		The one-call consumer for :meth:`section_motifs` — it registers a
5139		pattern on *channel* that walks whatever value is bound to the
5140		current section for *part* (stateless position from the cycle
5141		counter, via ``p.phrase()``).  A section with no binding for the
5142		part is **silent** for that part — bind material or don't; no
5143		fallback guessing.
5144
5145		Parameters:
5146			channel: MIDI channel for the part.
5147			part: The part label to read from the registry (``None`` = the
5148				unlabelled binding).
5149			root: Register anchor for degree resolution.
5150			bars / beats: Cycle length of the part (defaults to 4 beats);
5151				the phrase is sliced one cycle window at a time.
5152			velocity: Optional override applied to every note.
5153			fit: Passed through (active with the melody engine stage).
5154			device: Optional output-device override.
5155			mirrors: Optional additional ``(device, channel)`` destinations.
5156
5157		Example::
5158
5159			composition.section_motifs("verse",  verse_line,  part="lead")
5160			composition.section_motifs("chorus", chorus_line, part="lead")
5161			composition.phrase_part(channel=4, part="lead", root=72, bars=2)
5162		"""
5163
5164		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5165		resolved_channel = self._resolve_channel(channel)
5166
5167		captured_part = part
5168		captured_root = root
5169		captured_velocity = velocity
5170		captured_fit = fit
5171
5172		def phrase_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5173
5174			"""Walk the current section's bound value (silent when unbound)."""
5175
5176			value = p.section_motif(captured_part)
5177
5178			if value is None:
5179				return	# unbound section: silence for this part, by design
5180
5181			p.phrase(value, root=captured_root, velocity=captured_velocity, fit=captured_fit)
5182
5183		# Unique, stable name so multiple phrase parts don't collide —
5184		# including two parts on the SAME channel (deterministic #2/#3
5185		# suffixes in declaration order, the chords() convention).
5186		base_name = f"phrase@{captured_part}@ch{resolved_channel}" if captured_part else f"phrase@ch{resolved_channel}"
5187		phrase_name = base_name
5188		suffix = 2
5189
5190		while phrase_name in self._declared_names:
5191			phrase_name = f"{base_name}#{suffix}"
5192			suffix += 1
5193
5194		phrase_builder.__name__ = phrase_name
5195		self._declared_names.add(phrase_name)
5196
5197		primary: typing.Optional[typing.Tuple[int, int]]
5198		if isinstance(device, str):
5199			primary = None
5200		else:
5201			primary = (device if device is not None else 0, resolved_channel)
5202		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5203
5204		if self._is_live and phrase_builder.__name__ in self._running_patterns:
5205			running = self._running_patterns[phrase_builder.__name__]
5206			running._builder_fn = phrase_builder
5207			running._wants_chord = False
5208			logger.info(f"Hot-swapped phrase part: {phrase_builder.__name__}")
5209			return
5210
5211		pending = _PendingPattern(
5212			builder_fn = phrase_builder,
5213			channel = resolved_channel,
5214			length = beat_length,
5215			default_grid = default_grid,
5216			drum_note_map = None,
5217			reschedule_lookahead = 1,
5218			voice_leading = False,
5219			mirrors = resolved_mirrors,
5220			device = 0 if (device is None or isinstance(device, str)) else device,
5221			raw_device = device,
5222		)
5223		self._pending_patterns.append(pending)

Declare a part that plays each section's bound Motif/Phrase.

The one-call consumer for section_motifs() — it registers a pattern on channel that walks whatever value is bound to the current section for part (stateless position from the cycle counter, via p.phrase()). A section with no binding for the part is silent for that part — bind material or don't; no fallback guessing.

Arguments:
  • channel: MIDI channel for the part.
  • part: The part label to read from the registry (None = the unlabelled binding).
  • root: Register anchor for degree resolution.
  • bars / beats: Cycle length of the part (defaults to 4 beats); the phrase is sliced one cycle window at a time.
  • velocity: Optional override applied to every note.
  • fit: Passed through (active with the melody engine stage).
  • device: Optional output-device override.
  • mirrors: Optional additional (device, channel) destinations.

Example::

    composition.section_motifs("verse",  verse_line,  part="lead")
    composition.section_motifs("chorus", chorus_line, part="lead")
    composition.phrase_part(channel=4, part="lead", root=72, bars=2)
def trigger( self, fn: Callable, channel: int, beats: Optional[float] = None, bars: Optional[float] = None, steps: Optional[float] = None, step_duration: Optional[float] = None, quantize: float = 0, drum_note_map: Optional[Dict[str, int]] = None, cc_name_map: Optional[Dict[str, int]] = None, nrpn_name_map: Optional[Dict[str, int]] = None, chord: bool = False, device: Union[int, str, NoneType] = None, mirrors: Optional[Iterable[Union[Tuple[int, int], Tuple[int, int, Optional[Dict[str, int]]]]]] = None) -> None:
5225	def trigger (
5226		self,
5227		fn: typing.Callable,
5228		channel: int,
5229		beats: typing.Optional[float] = None,
5230		bars: typing.Optional[float] = None,
5231		steps: typing.Optional[float] = None,
5232		step_duration: typing.Optional[float] = None,
5233		quantize: float = 0,
5234		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
5235		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
5236		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
5237		chord: bool = False,
5238		device: subsequence.midi_utils.DeviceId = None,
5239		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5240	) -> None:
5241
5242		"""
5243		Trigger a one-shot pattern immediately or on a quantized boundary.
5244
5245		This is useful for real-time response to sensors, OSC messages, or other
5246		external events. The builder function is called immediately with a fresh
5247		PatternBuilder, and the generated events are injected into the queue at
5248		the specified quantize boundary.
5249
5250		The builder function has the same API as a ``@composition.pattern``
5251		decorated function and can use all PatternBuilder methods: ``p.note()``,
5252		``p.euclidean()``, ``p.arpeggio()``, and so on.
5253
5254		See ``pattern()`` for the full description of ``beats``, ``bars``,
5255		``steps``, and ``step_duration``. Default is 1 beat.
5256
5257		Parameters:
5258			fn: The pattern builder function (same signature as ``@comp.pattern``).
5259			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
5260			beats: Duration in beats (quarter notes, default 1).
5261			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
5262			steps: Step count for step mode. Requires ``step_duration=``.
5263			step_duration: Duration of one step in beats. Requires ``steps=``.
5264			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
5265				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
5266				constants from ``subsequence.constants.durations``.
5267			drum_note_map: Optional drum name mapping for this pattern.
5268			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
5269			nrpn_name_map: Optional mapping of NRPN parameter names to
5270				14-bit parameter numbers.
5271			chord: If ``True``, the builder function receives the current chord as
5272				a second parameter (same as ``@composition.pattern``).
5273			mirrors: Optional list of additional ``(device, channel)`` destinations
5274				to fire this one-shot onto in parallel with the primary destination.
5275
5276		Example:
5277			```python
5278			# Immediate single note (channels are 1-16 by default)
5279			composition.trigger(
5280				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
5281				channel=1
5282			)
5283
5284			# Quantized fill (next bar) — channel 10 is the GM drum channel
5285			import subsequence.constants.durations as dur
5286			composition.trigger(
5287				lambda p: p.euclidean("snare", pulses=7, velocity=90),
5288				channel=10,
5289				drum_note_map=gm_drums.GM_DRUM_MAP,
5290				quantize=dur.WHOLE
5291			)
5292
5293			# With chord context — the builder receives the chord as a second
5294			# argument when chord=True.
5295			composition.trigger(
5296				lambda p, chord: p.arpeggio(chord.tones(root=60), spacing=dur.SIXTEENTH),
5297				channel=1,
5298				quantize=dur.QUARTER,
5299				chord=True
5300			)
5301			```
5302		"""
5303
5304		# Resolve channel numbering
5305		resolved_channel = self._resolve_channel(channel)
5306
5307		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, default=1.0, beats_per_bar=self.time_signature[0])
5308
5309		# Resolve device index — for trigger() this is always concrete by call time,
5310		# so the mirror-to-self check has the full primary tuple available.
5311		resolved_device_idx = self._resolve_device_id(device)
5312		resolved_mirrors = self._resolve_mirrors(mirrors, primary=(resolved_device_idx, resolved_channel))
5313
5314		# Create a temporary Pattern
5315		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx, mirrors=resolved_mirrors)
5316
5317		# Resolve the section context once: the one-shot inherits the section's
5318		# effective key/scale (so a triggered degree resolves like everywhere
5319		# else) and a harmony view at the current playhead (so ChordTone /
5320		# Approach resolve too).
5321		trigger_section = self._form_state.get_section_info() if self._form_state else None
5322		trigger_key, trigger_scale = self._effective_key_scale(trigger_section)
5323
5324		trigger_harmony: typing.Optional[HarmonyView] = None
5325		if not self._harmony_horizon.is_empty:
5326			trigger_harmony = HarmonyView(self._harmony_horizon, self._sequencer.pulse_count / self._sequencer.pulses_per_beat)
5327
5328		# Create a PatternBuilder
5329		builder = subsequence.pattern_builder.PatternBuilder(
5330			pattern=pattern,
5331			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
5332			drum_note_map=drum_note_map,
5333			cc_name_map=cc_name_map,
5334			nrpn_name_map=nrpn_name_map,
5335			section=trigger_section,
5336			bar=self._builder_bar,
5337			conductor=self.conductor,
5338			rng=random.Random(),  # Fresh random state for each trigger
5339			tweaks={},
5340			default_grid=default_grid,
5341			data=self.data,
5342			# A one-shot resolves key-relative content against the same
5343			# effective key/scale as the section it fires into (previously
5344			# omitted entirely — degrees raised even in a keyed composition).
5345			key=trigger_key,
5346			scale=trigger_scale,
5347			time_signature=self.time_signature,
5348			held_notes=self._sequencer._held_notes,
5349			harmony=trigger_harmony,
5350			energy=self._current_energy(trigger_section)
5351		)
5352
5353		# Call the builder function
5354		try:
5355
5356			current_chord = self.current_chord() if chord else None
5357
5358			if current_chord is not None:
5359				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
5360				fn(builder, injected)
5361
5362			else:
5363				fn(builder)
5364
5365		except Exception:
5366			logger.exception("Error in trigger builder — pattern will be silent")
5367			return
5368
5369		# Calculate the start pulse based on quantize
5370		current_pulse = self._sequencer.pulse_count
5371		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
5372
5373		if quantize == 0:
5374			# Immediate: use current pulse
5375			start_pulse = current_pulse
5376
5377		else:
5378			# Quantize to the next multiple of (quantize * pulses_per_beat)
5379			quantize_pulses = int(quantize * pulses_per_beat)
5380			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
5381
5382		self._schedule_one_shot(pattern, start_pulse)

Trigger a one-shot pattern immediately or on a quantized boundary.

This is useful for real-time response to sensors, OSC messages, or other external events. The builder function is called immediately with a fresh PatternBuilder, and the generated events are injected into the queue at the specified quantize boundary.

The builder function has the same API as a @composition.pattern decorated function and can use all PatternBuilder methods: p.note(), p.euclidean(), p.arpeggio(), and so on.

See pattern() for the full description of beats, bars, steps, and step_duration. Default is 1 beat.

Arguments:
  • fn: The pattern builder function (same signature as @comp.pattern).
  • channel: MIDI channel (1-16, or 0-15 with zero_indexed_channels=True).
  • beats: Duration in beats (quarter notes, default 1).
  • bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
  • steps: Step count for step mode. Requires step_duration=.
  • step_duration: Duration of one step in beats. Requires steps=.
  • quantize: Snap the trigger to a beat boundary: 0 = immediate (default), 1 = next beat (quarter note), 4 = next bar. Use dur.* constants from subsequence.constants.durations.
  • drum_note_map: Optional drum name mapping for this pattern.
  • cc_name_map: Optional mapping of CC names to MIDI CC numbers.
  • nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit parameter numbers.
  • chord: If True, the builder function receives the current chord as a second parameter (same as @composition.pattern).
  • mirrors: Optional list of additional (device, channel) destinations to fire this one-shot onto in parallel with the primary destination.
Example:
# Immediate single note (channels are 1-16 by default)
composition.trigger(
        lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
        channel=1
)

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

# With chord context — the builder receives the chord as a second
# argument when chord=True.
composition.trigger(
        lambda p, chord: p.arpeggio(chord.tones(root=60), spacing=dur.SIXTEENTH),
        channel=1,
        quantize=dur.QUARTER,
        chord=True
)
is_clock_following: bool
5403	@property
5404	def is_clock_following (self) -> bool:
5405
5406		"""True if either the primary or any additional device is following external clock."""
5407
5408		return self._clock_follow or any(cf for _, _, cf in self._additional_inputs)

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

def play(self) -> None:
5411	def play (self) -> None:
5412
5413		"""
5414		Start the composition.
5415
5416		This call blocks until the program is interrupted (e.g., via Ctrl+C).
5417		It initializes the MIDI hardware, launches the background sequencer,
5418		and begins playback.
5419		"""
5420
5421		try:
5422			asyncio.run(self._run())
5423
5424		except KeyboardInterrupt:
5425			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:
5428	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
5429
5430		"""Render the composition to a MIDI file without real-time playback.
5431
5432		Runs the sequencer as fast as possible (no timing delays) and stops
5433		when the first active limit is reached.  The result is saved as a
5434		standard MIDI file that can be imported into any DAW.
5435
5436		All patterns, scheduled callbacks, and harmony logic run exactly as
5437		they would during live playback — BPM transitions, generative fills,
5438		and probabilistic gates all work in render mode.  The only difference
5439		is that time is simulated rather than wall-clock driven.
5440
5441		Parameters:
5442			bars: Number of bars to render, or ``None`` for no bar limit
5443			      (default ``None``).  When both *bars* and *max_minutes* are
5444			      active, playback stops at whichever limit is reached first.
5445			filename: Output MIDI filename (default ``"render.mid"``).
5446			max_minutes: Safety cap on the length of rendered MIDI in minutes
5447			             (default ``60.0``).  Pass ``None`` to disable the time
5448			             cap — you must then provide an explicit *bars* value.
5449
5450		Raises:
5451			ValueError: If both *bars* and *max_minutes* are ``None``, which
5452			            would produce an infinite render.
5453
5454		Examples:
5455			```python
5456			# Default: renders up to 60 minutes of MIDI content.
5457			composition.render()
5458
5459			# Render exactly 64 bars (time cap still active as backstop).
5460			composition.render(bars=64, filename="demo.mid")
5461
5462			# Render up to 5 minutes of an infinite generative composition.
5463			composition.render(max_minutes=5, filename="five_min.mid")
5464
5465			# Remove the time cap — must supply bars instead.
5466			composition.render(bars=128, max_minutes=None, filename="long.mid")
5467			```
5468		"""
5469
5470		if bars is None and max_minutes is None:
5471			raise ValueError(
5472				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
5473				"Passing both as None would produce an infinite render."
5474			)
5475
5476		self._sequencer.recording = True
5477		self._sequencer.record_filename = filename
5478		self._sequencer.render_mode = True
5479		self._sequencer.render_bars = bars if bars is not None else 0
5480		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
5481		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(frozen=True)
class Motif:
 374@dataclasses.dataclass(frozen=True)
 375class Motif:
 376
 377	"""
 378	An immutable musical figure: timed note events + control gestures + a length in beats.
 379
 380	Construct via the classmethods (:meth:`degrees`, :meth:`notes`,
 381	:meth:`hits`, :meth:`steps`, :meth:`euclidean`, the control-gesture
 382	constructors, or :meth:`from_events`) rather than positionally.
 383	``length`` is explicit — a trailing rest is meaningful.
 384	"""
 385
 386	events: typing.Tuple[MotifEvent, ...]
 387	length: float
 388	controls: typing.Tuple[ControlEvent, ...] = ()
 389	fit: typing.Optional[float] = None		# placement default for the fit dial; set by generate()
 390
 391	def __post_init__ (self) -> None:
 392
 393		"""Validate, and normalise both streams to canonical order."""
 394
 395		if self.length < 0:
 396			raise ValueError(f"Motif length must be non-negative — got {self.length}")
 397
 398		object.__setattr__(self, "events", tuple(sorted(self.events, key=MotifEvent._sort_key)))
 399		object.__setattr__(self, "controls", tuple(sorted(self.controls, key=ControlEvent._sort_key)))
 400
 401	# ── constructors ────────────────────────────────────────────────────
 402
 403	@classmethod
 404	def empty (cls) -> "Motif":
 405
 406		"""The empty motif (zero events, zero length) — the identity for ``then``."""
 407
 408		return cls(events=(), length=0.0)
 409
 410	@classmethod
 411	def from_events (
 412		cls,
 413		events: typing.Iterable[MotifEvent],
 414		length: typing.Optional[float] = None,
 415		controls: typing.Iterable[ControlEvent] = (),
 416	) -> "Motif":
 417
 418		"""Build a motif from explicit events (power use; length defaults to the next whole beat)."""
 419
 420		events = tuple(events)
 421		controls = tuple(controls)
 422
 423		return cls(
 424			events = events,
 425			length = _computed_length(events, controls) if length is None else length,
 426			controls = controls,
 427		)
 428
 429	@classmethod
 430	def _from_sequence (
 431		cls,
 432		pitches: typing.List[PitchSpec],
 433		beats: typing.Optional[typing.List[float]],
 434		velocities: typing.Any,
 435		durations: typing.Any,
 436		probabilities: typing.Any,
 437		length: typing.Optional[float],
 438	) -> "Motif":
 439
 440		"""Shared core: one event per element, None = rest (slot still advances)."""
 441
 442		n = len(pitches)
 443		onsets = list(beats) if beats is not None else [float(i) for i in range(n)]
 444
 445		if len(onsets) != n:
 446			raise ValueError(f"beats has {len(onsets)} onsets for {n} elements — parallel lists must match")
 447
 448		velocity_list = _expand("velocities", velocities, n)
 449		duration_list = _expand("durations", durations, n)
 450		probability_list = _expand("probabilities", probabilities, n)
 451
 452		events = tuple(
 453			MotifEvent(
 454				beat = float(onsets[i]),
 455				pitch = pitches[i],
 456				velocity = velocity_list[i],
 457				duration = float(duration_list[i]),
 458				probability = float(probability_list[i]),
 459			)
 460			for i in range(n)
 461			if pitches[i] is not None
 462		)
 463
 464		return cls(
 465			events = events,
 466			length = _computed_length(events, ()) if length is None else float(length),
 467		)
 468
 469	@classmethod
 470	def degrees (
 471		cls,
 472		degrees: typing.List[typing.Union[int, Degree, None]],
 473		beats: typing.Optional[typing.List[float]] = None,
 474		velocities: typing.Any = _DEFAULT_VELOCITY,
 475		durations: typing.Any = 1.0,
 476		probabilities: typing.Any = 1.0,
 477		length: typing.Optional[float] = None,
 478	) -> "Motif":
 479
 480		"""
 481		A melody written as 1-based scale degrees, one per beat by default.
 482
 483		Elements are ints (1 = tonic, 8 = tonic an octave up), ``None`` for a
 484		rest (the beat slot still advances), or :class:`Degree` for octave/
 485		chromatic detail.  Resolved against key + scale at placement.
 486		Durations default to a full beat (each note holds its slot).
 487		"""
 488
 489		converted: typing.List[PitchSpec] = []
 490
 491		for element in degrees:
 492			if isinstance(element, int):
 493				if element > _MAX_PLAUSIBLE_DEGREE:
 494					raise ValueError(
 495						f"Degree {element} is implausibly large — scale degrees are 1-based "
 496						f"(8 = tonic an octave up). For MIDI note numbers use Motif.notes()."
 497					)
 498				converted.append(Degree(element))
 499			elif isinstance(element, Degree) or element is None:
 500				converted.append(element)
 501			else:
 502				raise TypeError(f"Motif.degrees takes ints, Degree, or None — got {type(element).__name__}")
 503
 504		return cls._from_sequence(converted, beats, velocities, durations, probabilities, length)
 505
 506	@classmethod
 507	def notes (
 508		cls,
 509		notes: typing.List[typing.Union[int, None]],
 510		beats: typing.Optional[typing.List[float]] = None,
 511		velocities: typing.Any = _DEFAULT_VELOCITY,
 512		durations: typing.Any = 1.0,
 513		probabilities: typing.Any = 1.0,
 514		length: typing.Optional[float] = None,
 515	) -> "Motif":
 516
 517		"""A melody written as absolute MIDI note numbers (60 = middle C); ``None`` = rest."""
 518
 519		for element in notes:
 520			# bool is a subclass of int, but True/False are never MIDI notes.
 521			if isinstance(element, bool) or not (isinstance(element, int) or element is None):
 522				raise TypeError(f"Motif.notes takes MIDI ints or None — got {type(element).__name__}")
 523
 524		return cls._from_sequence(list(notes), beats, velocities, durations, probabilities, length)
 525
 526	@classmethod
 527	def hits (
 528		cls,
 529		pitch: typing.Union[int, str],
 530		beats: typing.List[float],
 531		length: typing.Optional[float] = None,
 532		velocities: typing.Any = _DEFAULT_VELOCITY,
 533		durations: typing.Any = 0.1,
 534		probabilities: typing.Any = 1.0,
 535	) -> "Motif":
 536
 537		"""One pitch (usually a drum name) at a list of beat positions — the ``hit()`` convention."""
 538
 539		return cls._from_sequence([pitch] * len(beats), list(beats), velocities, durations, probabilities, length)
 540
 541	@classmethod
 542	def steps (
 543		cls,
 544		steps: typing.List[int],
 545		pitches: typing.Any,
 546		velocities: typing.Any = _DEFAULT_VELOCITY,
 547		durations: typing.Any = 0.1,
 548		probabilities: typing.Any = 1.0,
 549		step_duration: float = 0.25,
 550		length: typing.Optional[float] = None,
 551	) -> "Motif":
 552
 553		"""
 554		Grid placement — the ``sequence()`` convention: ``steps`` are 0-based
 555		grid indices (sixteenths by default), ``pitches`` a scalar or
 556		parallel list of MIDI ints or drum names.
 557		"""
 558
 559		n = len(steps)
 560		pitch_list = _expand("pitches", pitches, n)
 561		onsets = [s * step_duration for s in steps]
 562
 563		if length is None and n:
 564			length = float(math.ceil((max(steps) + 1) * step_duration))
 565
 566		return cls._from_sequence(pitch_list, onsets, velocities, durations, probabilities, length)
 567
 568	@classmethod
 569	def euclidean (
 570		cls,
 571		pulses: int,
 572		steps: int,
 573		pitch: typing.Union[int, str],
 574		length: float = 4.0,
 575		velocities: typing.Any = _DEFAULT_VELOCITY,
 576		durations: typing.Any = 0.1,
 577		probabilities: typing.Any = 1.0,
 578	) -> "Motif":
 579
 580		"""A euclidean rhythm as a value: *pulses* spread evenly across *steps* over *length* beats."""
 581
 582		# bool is a subclass of int, but True/False are never MIDI notes.
 583		if isinstance(pitch, bool):
 584			raise TypeError(f"Motif.euclidean takes a MIDI int or drum name for pitch — got {pitch!r}")
 585
 586		# The kernel returns one 0/1 flag per grid step; onsets are the 1s.
 587		# It validates pulses first, so pulses > steps still raises clearly.
 588		flags = subsequence.sequence_utils.generate_euclidean_sequence(steps=steps, pulses=pulses)
 589
 590		if steps <= 0:
 591			# A grid of zero steps holds no onsets — an empty motif of the given
 592			# length, matching pulses=0 on a real grid (the empty-input policy).
 593			return cls._from_sequence([], [], velocities, durations, probabilities, length)
 594
 595		step_duration = length / steps
 596		onsets = [i * step_duration for i, flag in enumerate(flags) if flag]
 597
 598		return cls._from_sequence(
 599			[pitch] * len(onsets),
 600			onsets,
 601			velocities, durations, probabilities, length,
 602		)
 603
 604	@classmethod
 605	def preset (
 606		cls,
 607		name: str,
 608		pitch: typing.Optional[typing.Union[int, str]] = None,
 609		length: float = 4.0,
 610		velocities: typing.Any = _DEFAULT_VELOCITY,
 611		durations: typing.Any = 0.1,
 612		probabilities: typing.Any = 1.0,
 613	) -> "Motif":
 614
 615		"""A named world-rhythm timeline as a value — ``Motif.preset("son_clave_3_2")``.
 616
 617		Looks a curated timeline up in the world-rhythm table (clave family,
 618		West-African bell patterns, tresillo/cinquillo, samba) and lays its
 619		onsets across *length* beats.  Onset positions are exact pulse indices
 620		from Toussaint's "The Geometry of Musical Rhythm"; each preset declares
 621		its own grid (16 for the clave/4-4 timelines, 12 for the bell
 622		patterns) and a default drum voice.
 623
 624		Parameters:
 625			name: A preset name (``KeyError``-style ValueError lists them all).
 626			pitch: The voice — a drum name or MIDI int; defaults to the
 627				preset's General-MIDI voice (``"claves"``, ``"cowbell"``,
 628				``"side_stick"``, ``"low_conga"``), so it sounds against the
 629				standard GM drum map without a ``pitch=``.
 630			length: Total beats the cycle spans (4 = one common-time bar).
 631			velocities / durations / probabilities: The parallel-list params.
 632
 633		Returns:
 634			A drum/pitched :class:`Motif` of the timeline's onsets.
 635
 636		Raises:
 637			ValueError: If *name* is not a known preset.
 638
 639		Example:
 640			```python
 641			clave = subsequence.Motif.preset("son_clave_3_2")              # GM "claves"
 642			bell  = subsequence.Motif.preset("bembe", pitch="cowbell")     # 12-pulse
 643			```
 644		"""
 645
 646		if name not in _WORLD_RHYTHMS:
 647			known = ", ".join(sorted(_WORLD_RHYTHMS))
 648			raise ValueError(f"Unknown rhythm preset {name!r}. Known presets: {known}.")
 649
 650		steps, grid, voice = _WORLD_RHYTHMS[name]
 651
 652		return cls.steps(
 653			steps = list(steps),
 654			pitches = pitch if pitch is not None else voice,
 655			velocities = velocities,
 656			durations = durations,
 657			probabilities = probabilities,
 658			step_duration = length / grid,
 659			length = length,
 660		)
 661
 662	# ── control-gesture constructors (mirror the pattern_midi verbs) ────
 663
 664	@classmethod
 665	def _control_writes (
 666		cls,
 667		signal: ControlSignal,
 668		values: typing.List[float],
 669		beats: typing.List[float],
 670		length: typing.Optional[float],
 671		probabilities: typing.Any = 1.0,
 672	) -> "Motif":
 673
 674		"""Shared core for discrete control writes."""
 675
 676		if len(values) != len(beats):
 677			raise ValueError(f"values has {len(values)} entries for {len(beats)} beats — parallel lists must match")
 678
 679		probability_list = _expand("probabilities", probabilities, len(values))
 680
 681		controls = tuple(
 682			ControlEvent(beat=float(beats[i]), signal=signal, start=float(values[i]), probability=float(probability_list[i]))
 683			for i in range(len(values))
 684		)
 685
 686		return cls(
 687			events = (),
 688			length = _computed_length((), controls) if length is None else float(length),
 689			controls = controls,
 690		)
 691
 692	@classmethod
 693	def _control_ramp (
 694		cls,
 695		signal: ControlSignal,
 696		start: float,
 697		end: float,
 698		beat_start: float,
 699		beat_end: typing.Optional[float],
 700		shape: typing.Union[str, "subsequence.easing.EasingFn"],
 701		length: typing.Optional[float],
 702		probability: float = 1.0,
 703	) -> "Motif":
 704
 705		"""Shared core for shaped control ramps."""
 706
 707		if beat_end is None:
 708			if length is None:
 709				raise ValueError("A ramp needs beat_end= (or length=, which beat_end defaults to)")
 710			beat_end = float(length)
 711
 712		if beat_end <= beat_start:
 713			raise ValueError(f"beat_end ({beat_end}) must be after beat_start ({beat_start})")
 714
 715		controls = (
 716			ControlEvent(
 717				beat = float(beat_start),
 718				signal = signal,
 719				start = float(start),
 720				end = float(end),
 721				span = float(beat_end) - float(beat_start),
 722				shape = shape,
 723				probability = probability,
 724			),
 725		)
 726
 727		return cls(
 728			events = (),
 729			length = float(math.ceil(beat_end)) if length is None else float(length),
 730			controls = controls,
 731		)
 732
 733	@classmethod
 734	def cc (cls, control: typing.Union[int, str], values: typing.List[int], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
 735
 736		"""Discrete CC writes at beat positions — mirrors ``p.cc()``; names resolve at placement."""
 737
 738		return cls._control_writes(CC(control), list(values), list(beats), length, probabilities)
 739
 740	@classmethod
 741	def cc_ramp (cls, control: typing.Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 742
 743		"""A CC value swept ``start`` → ``end`` over a beat range — mirrors ``p.cc_ramp()``."""
 744
 745		return cls._control_ramp(CC(control), start, end, beat_start, beat_end, shape, length, probability)
 746
 747	@classmethod
 748	def pitch_bend (cls, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
 749
 750		"""Discrete pitch-bend writes (-1.0 to 1.0) at beat positions — mirrors ``p.pitch_bend()``."""
 751
 752		return cls._control_writes(PitchBend(), list(values), list(beats), length, probabilities)
 753
 754	@classmethod
 755	def pitch_bend_ramp (cls, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 756
 757		"""Pitch bend swept ``start`` → ``end`` (-1.0 to 1.0) over a beat range — mirrors ``p.pitch_bend_ramp()``."""
 758
 759		return cls._control_ramp(PitchBend(), start, end, beat_start, beat_end, shape, length, probability)
 760
 761	@classmethod
 762	def nrpn (cls, parameter: typing.Union[int, str], values: typing.List[int], beats: typing.List[float], fine: bool = False, null_reset: bool = True, length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
 763
 764		"""Discrete NRPN parameter writes at beat positions — mirrors ``p.nrpn()``."""
 765
 766		return cls._control_writes(NRPN(parameter, fine=fine, null_reset=null_reset), list(values), list(beats), length, probabilities)
 767
 768	@classmethod
 769	def nrpn_ramp (cls, parameter: typing.Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 770
 771		"""An NRPN value swept over a beat range — mirrors ``p.nrpn_ramp()``."""
 772
 773		return cls._control_ramp(NRPN(parameter, fine=fine, null_reset=null_reset), start, end, beat_start, beat_end, shape, length, probability)
 774
 775	@classmethod
 776	def rpn (cls, parameter: typing.Union[int, str], values: typing.List[int], beats: typing.List[float], fine: bool = False, null_reset: bool = True, length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
 777
 778		"""Discrete RPN parameter writes at beat positions — mirrors ``p.rpn()``."""
 779
 780		return cls._control_writes(RPN(parameter, fine=fine, null_reset=null_reset), list(values), list(beats), length, probabilities)
 781
 782	@classmethod
 783	def rpn_ramp (cls, parameter: typing.Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 784
 785		"""An RPN value swept over a beat range — mirrors ``p.rpn_ramp()``."""
 786
 787		return cls._control_ramp(RPN(parameter, fine=fine, null_reset=null_reset), start, end, beat_start, beat_end, shape, length, probability)
 788
 789	@classmethod
 790	def osc (cls, address: str, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
 791
 792		"""Discrete OSC float sends at beat positions — mirrors ``p.osc()``."""
 793
 794		return cls._control_writes(OSC(address), list(values), list(beats), length, probabilities)
 795
 796	@classmethod
 797	def osc_ramp (cls, address: str, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 798
 799		"""An OSC float swept over a beat range — mirrors ``p.osc_ramp()``."""
 800
 801		return cls._control_ramp(OSC(address), start, end, beat_start, beat_end, shape, length, probability)
 802
 803	# ── the algebra ─────────────────────────────────────────────────────
 804
 805	def then (self, other: "Motif") -> "Motif":
 806
 807		"""Closed sequential concat: glue *other* after this motif into ONE longer motif."""
 808
 809		if not isinstance(other, Motif):
 810			raise TypeError(f"then() takes a Motif — got {type(other).__name__}")
 811
 812		return Motif(
 813			events = self.events + tuple(dataclasses.replace(e, beat=e.beat + self.length) for e in other.events),
 814			length = self.length + other.length,
 815			controls = self.controls + tuple(dataclasses.replace(c, beat=c.beat + self.length) for c in other.controls),
 816			# fit is a dial, not content: keep ours, inherit the other's when
 817			# we have none — join()/tiling folds from empty() (fit=None), and
 818			# must not silently strip a generated motif's chord-snapping.
 819			fit = self.fit if self.fit is not None else other.fit,
 820		)
 821
 822	@classmethod
 823	def join (cls, motifs: typing.Iterable["Motif"]) -> "Motif":
 824
 825		"""Fold a list of motifs into one with ``then`` (empty list → ``Motif.empty()``)."""
 826
 827		result = cls.empty()
 828
 829		for m in motifs:
 830			result = result.then(m)
 831
 832		return result
 833
 834	@classmethod
 835	def generate (
 836		cls,
 837		rhythm: typing.Any,
 838		length: typing.Optional[float] = None,
 839		scale: typing.Optional[typing.Union[str, typing.Sequence[int]]] = None,
 840		contour: typing.Optional[str] = None,
 841		end_on: typing.Optional[typing.Union[int, Degree]] = None,
 842		cadence: typing.Optional[str] = None,
 843		pins: typing.Optional[typing.Dict[int, typing.Union[int, Degree]]] = None,
 844		max_pitches: typing.Optional[int] = None,
 845		velocities: typing.Any = _DEFAULT_VELOCITY,
 846		durations: typing.Any = 0.25,
 847		seed: typing.Optional[int] = None,
 848		rng: typing.Optional[random.Random] = None,
 849		state: typing.Optional[typing.Any] = None,
 850		nir_strength: float = 0.5,
 851		pitch_diversity: float = 0.6,
 852		tessitura_strength: float = 0.6,
 853	) -> "Motif":
 854
 855		"""Generate a melodic motif — rhythm first, pitches walked, a value out.
 856
 857		The melody engine emitting a value: you give the **rhythm** (an onset
 858		list in beats, or another motif whose rhythm to borrow — cross-pattern
 859		rhythm reuse is shared values); the engine walks pitches over it
 860		through the soft scoring factors (NIR expectation, contour envelope,
 861		tessitura regression, diversity), honouring any pins.
 862
 863		The result emits **scale degrees** (resolved at placement against the
 864		composition key/scale), so a generated hook transposes, varies, and
 865		develops like a hand-written one.  ``scale=`` constrains *candidate
 866		choice only*: a name or interval list masks which pitches the walk
 867		may use, spelled relative to its best-fit reference (major or minor)
 868		— bind it in a composition whose scale matches that family and
 869		resolution is exact.  An explicit MIDI pitch pool (a list of note
 870		numbers) switches to absolute output (the sieve/atonal path).
 871
 872		Parameters:
 873			rhythm: Onset beats (``[0, 1, 1.5, 1.75, 2.5]``) or a Motif
 874				(its onsets are borrowed).
 875			length: Motif length in beats; defaults to the onsets rounded
 876				up to a whole 4-beat bar.
 877			scale: A scale name, an interval list, or an explicit MIDI
 878				pitch pool.  ``None`` = the plain seven degrees.
 879			contour: Envelope shaping the line's height over its span —
 880				``"arch"``, ``"valley"``, ``"ascending"``, ``"descending"``.
 881			end_on: Degree the line must end on — sugar for ``pins={-1: ...}``.
 882				Degree semantics: raises with an explicit MIDI pool (pin the
 883				exact note instead).
 884			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
 885				``"fakeout"``) — the line closes on that cadence's melodic
 886				degree (1 for the full closes and the fakeout, 5 for the
 887				open half).  Sugar for ``end_on=``; conflicts with it, and
 888				raises with an explicit MIDI pool like ``end_on=``.
 889			pins: ``{position: degree}`` — 1-based note positions (``-1`` =
 890				the last, the Python idiom); the engine fills between.  With
 891				an explicit MIDI pool there are no degrees to read, so each
 892				pin is the exact MIDI note to play (``Degree`` pins raise).
 893			max_pitches: Cap on distinct pitches (a tight pool is a hook);
 894				keeps the most central candidates.
 895			velocities / durations: Scalar or per-note list (the parallel-
 896				list convention).
 897			seed: Seed for the walk (required or warned — module-level
 898				nondeterminism breaks live reload).
 899			rng: Explicit stream (overrides ``seed``).
 900			state: A ``MelodicState`` whose dials, scoring factors, and
 901				melodic history seed the walk.  It is **copied** — building
 902				a value never mutates a module-level live object.  The
 903				candidate pool is not carried over: it is always rebuilt
 904				from ``scale=`` (pass an explicit pool there instead),
 905				though the state's key still sets the tonic that the NIR
 906				closure rule lands on.
 907			nir_strength / pitch_diversity / tessitura_strength: The walk's
 908				dials when no ``state`` is given.
 909
 910		Example:
 911			```python
 912			hook = subsequence.Motif.generate(
 913				rhythm=[0, 1, 1.5, 1.75, 2.5], scale="minor_pentatonic",
 914				contour="arch", end_on=1, seed=7,
 915			)
 916			```
 917		"""
 918
 919		import subsequence.melodic_state
 920
 921		onsets = list(rhythm.onsets()) if hasattr(rhythm, "onsets") else [float(b) for b in rhythm]
 922
 923		if cadence is not None:
 924			if end_on is not None:
 925				raise ValueError("cadence= already names the close degree — it conflicts with end_on=")
 926			end_on = subsequence.cadences.cadence_formula(cadence).close_degree
 927
 928		if not onsets:
 929			raise ValueError("generate() needs at least one onset — the rhythm comes first")
 930		if sorted(onsets) != onsets:
 931			raise ValueError("rhythm onsets must ascend")
 932
 933		if length is None:
 934			length = max(4.0, math.ceil((onsets[-1] + 1e-9) / 4.0) * 4.0)
 935		if onsets[-1] >= length:
 936			raise ValueError(f"the last onset ({onsets[-1]:g}) falls outside length={length:g}")
 937
 938		if rng is None:
 939			if seed is None:
 940				warnings.warn(
 941					"generate() without seed= is nondeterministic — pass seed= so the "
 942					"value survives live reload",
 943					stacklevel = 2,
 944				)
 945				rng = random.Random()
 946			else:
 947				rng = random.Random(seed)
 948
 949		# --- The candidate pool ------------------------------------------------
 950		absolute_pool: typing.Optional[typing.List[int]] = None
 951		intervals: typing.List[int]
 952
 953		if scale is None:
 954			intervals = list(subsequence.intervals.scale_pitch_classes(0, "ionian"))
 955		elif isinstance(scale, str):
 956			intervals = list(subsequence.intervals.scale_pitch_classes(0, scale))
 957		else:
 958			values = [int(v) for v in scale]
 959			if values and (min(values) != 0 or max(values) > 11):
 960				absolute_pool = sorted(values)		# an explicit MIDI pool: absolute output
 961				intervals = []
 962			else:
 963				intervals = sorted(set(values))
 964
 965		# Best-fit reference scale for degree spelling: whichever of major/
 966		# minor contains more of the pool (ties to major).  Bound under a
 967		# matching composition scale, resolution is exact.
 968		if absolute_pool is None:
 969			ionian = set(subsequence.intervals.scale_pitch_classes(0, "ionian"))
 970			aeolian = set(subsequence.intervals.scale_pitch_classes(0, "minor"))
 971			reference_name = "minor" if sum(i in aeolian for i in intervals) > sum(i in ionian for i in intervals) else "ionian"
 972			reference = list(subsequence.intervals.scale_pitch_classes(0, reference_name))
 973
 974		# --- The walking state (copied, never mutated in place) ----------------
 975		if state is not None:
 976			walker = state.clone()
 977			walker.rest_probability = 0.0		# generate is rhythm-first: every onset gets a
 978												# note, so the walker never rests (and never falls
 979												# back to a stuck repeat) — rests come from the rhythm
 980		else:
 981			walker = subsequence.melodic_state.MelodicState(
 982				nir_strength = nir_strength,
 983				pitch_diversity = pitch_diversity,
 984				tessitura_strength = tessitura_strength,
 985				chord_weight = 0.0,		# values have no chord context; fit applies at placement
 986			)
 987
 988		if absolute_pool is not None:
 989			walker.set_pool(absolute_pool)
 990		else:
 991			# Offsets over ~1.5 octaves anchored at 60 — register is decided
 992			# at placement (root=), so the anchor is arbitrary and erased.
 993			walker.set_pool([60 + octave * 12 + interval for octave in (0, 1) for interval in intervals if octave * 12 + interval <= 19])
 994
 995		if max_pitches is not None:
 996			if max_pitches < 1:
 997				raise ValueError("max_pitches must be at least 1")
 998			pool = sorted(walker._pitch_pool)
 999			centre = pool[len(pool) // 2]
1000			walker.set_pool(sorted(sorted(pool, key = lambda p: (abs(p - centre), p))[:max_pitches]))
1001
1002		# --- Pins ---------------------------------------------------------------
1003		resolved_pins: typing.Dict[int, int] = {}
1004		combined = dict(pins or {})
1005
1006		# cadence=/end_on= name scale DEGREES — meaningless against an explicit
1007		# MIDI pool, where they would silently land as raw (sub-audio) note
1008		# numbers.
1009		if absolute_pool is not None and end_on is not None:
1010			raise ValueError(
1011				"cadence=/end_on= name scale degrees, but this motif uses an "
1012				"explicit MIDI pool — pin the exact closing note instead: "
1013				"pins={-1: <midi note>}"
1014			)
1015
1016		if end_on is not None:
1017			if -1 in combined or len(onsets) in combined:
1018				raise ValueError("end_on conflicts with a pin on the last note — they name the same position")
1019			combined[-1] = end_on
1020
1021		for pin_position, pin_spec in combined.items():
1022			if not isinstance(pin_position, int) or isinstance(pin_position, bool):
1023				raise ValueError(f"pin positions are 1-based ints (or -1 for last), got {pin_position!r}")
1024			index = pin_position - 1 if pin_position >= 1 else len(onsets) + pin_position
1025			if not 0 <= index < len(onsets):
1026				raise ValueError(f"pin position {pin_position} is outside the {len(onsets)}-note rhythm")
1027			if absolute_pool is not None:
1028				# A raw int pins the exact MIDI note; a Degree has no meaning
1029				# here (the pool defines no scale to read it against).
1030				if not isinstance(pin_spec, int) or isinstance(pin_spec, bool):
1031					raise ValueError(
1032						f"pin {pin_spec!r} is a scale degree, but this motif uses an "
1033						"explicit MIDI pool — pin the exact MIDI note instead "
1034						"(e.g. pins={-1: 52})"
1035					)
1036				resolved_pins[index] = int(pin_spec)
1037			else:
1038				degree = pin_spec if isinstance(pin_spec, Degree) else Degree(int(pin_spec))
1039				step_index = (degree.step - 1) % len(reference)
1040				carry = (degree.step - 1) // len(reference)
1041				resolved_pins[index] = 60 + reference[step_index] + 12 * (carry + degree.octave) + degree.chroma
1042
1043		# --- The walk -----------------------------------------------------------
1044		envelopes: typing.Dict[str, typing.Callable[[float], float]] = {
1045			"arch": lambda pos: 0.15 + 0.8 * math.sin(math.pi * pos),
1046			"valley": lambda pos: 0.95 - 0.8 * math.sin(math.pi * pos),
1047			"ascending": lambda pos: 0.1 + 0.85 * pos,
1048			"descending": lambda pos: 0.95 - 0.85 * pos,
1049		}
1050
1051		if contour is not None and contour not in envelopes:
1052			known = ", ".join(sorted(envelopes))
1053			raise ValueError(f"unknown contour {contour!r} — expected one of: {known}")
1054
1055		chosen_pitches: typing.List[int] = []
1056
1057		for index, onset in enumerate(onsets):
1058
1059			if index in resolved_pins:
1060				pitch = resolved_pins[index]
1061				walker.record(pitch)	# pins enter the NIR context like chosen notes
1062			else:
1063				span_position = index / (len(onsets) - 1) if len(onsets) > 1 else 0.0
1064				target = envelopes[contour](span_position) if contour is not None else None
1065				picked = walker.choose_next(None, rng, beat = onset, position = span_position, contour_target = target)
1066				pitch = picked if picked is not None else walker._pitch_pool[0]
1067
1068			chosen_pitches.append(pitch)
1069
1070		# --- Emission ------------------------------------------------------------
1071		velocity_values = _expand("velocities", velocities, len(onsets))
1072		duration_values = _expand("durations", durations, len(onsets))
1073
1074		events = []
1075
1076		for index, (onset, pitch) in enumerate(zip(onsets, chosen_pitches)):
1077
1078			spec: PitchSpec
1079
1080			if absolute_pool is not None:
1081				spec = pitch
1082			else:
1083				offset = pitch - 60
1084				octave, pc = divmod(offset, 12)
1085				if pc in reference:
1086					spec = Degree(reference.index(pc) + 1, octave = octave)
1087				elif (pc + 1) % 12 in reference and pc + 1 <= 11:
1088					spec = Degree(reference.index(pc + 1) + 1, octave = octave, chroma = -1)
1089				else:
1090					spec = Degree(reference.index(pc - 1) + 1, octave = octave, chroma = 1)
1091
1092			events.append(MotifEvent(
1093				beat = onset,
1094				pitch = spec,
1095				velocity = velocity_values[index],
1096				duration = float(duration_values[index]),
1097			))
1098
1099		return cls(events = tuple(events), length = float(length), fit = 0.7)
1100
1101	def stack (self, other: typing.Union["Motif", "Phrase"]) -> "Motif":
1102
1103		"""
1104		Parallel merge (the spelled form of ``&``): event union, length = max.
1105
1106		No implicit tiling — a short gesture stacked under a long figure
1107		plays once.  Phrase operands flatten first.
1108		"""
1109
1110		if isinstance(other, Phrase):
1111			merged = other.flatten()
1112		elif isinstance(other, Motif):
1113			merged = other
1114		else:
1115			raise TypeError(f"stack() takes a Motif or Phrase — got {type(other).__name__}")
1116
1117		return Motif(
1118			events = self.events + merged.events,
1119			length = max(self.length, merged.length),
1120			controls = self.controls + merged.controls,
1121			fit = self.fit,
1122		)
1123
1124	def slice (self, start: float, end: float) -> "Motif":
1125
1126		"""
1127		A window onto the motif, on its own authority: events starting outside
1128		are dropped; durations and ramp spans truncate at the cut (a truncated
1129		ramp ends at its interpolated cut value).  Beats shift so the window
1130		starts at 0.
1131		"""
1132
1133		if end <= start:
1134			raise ValueError(f"slice end ({end}) must be after start ({start})")
1135
1136		events = tuple(
1137			dataclasses.replace(e, beat=e.beat - start, duration=min(e.duration, end - e.beat))
1138			for e in self.events
1139			if start <= e.beat < end
1140		)
1141
1142		controls = []
1143
1144		for c in self.controls:
1145			if not (start <= c.beat < end):
1146				continue
1147			if c.end is not None and c.beat + c.span > end:
1148				kept = end - c.beat
1149				controls.append(dataclasses.replace(
1150					c, beat=c.beat - start, span=kept, end=c._value_at(kept / c.span),
1151				))
1152			else:
1153				controls.append(dataclasses.replace(c, beat=c.beat - start))
1154
1155		return Motif(events=events, length=end - start, controls=tuple(controls), fit=self.fit)
1156
1157	def __add__ (self, other: typing.Any) -> "Phrase":
1158
1159		"""``a + b`` — sequential: a two-segment Phrase (segmentation preserved)."""
1160
1161		if isinstance(other, Motif):
1162			return Phrase((self, other))
1163
1164		return NotImplemented
1165
1166	def __mul__ (self, count: int) -> typing.Union["Motif", "Phrase"]:
1167
1168		"""``m * n`` — repetition: a Phrase of n segments; ``m * 1`` is ``m``; ``m * 0`` is empty."""
1169
1170		if not isinstance(count, int):
1171			return NotImplemented
1172		if count < 0:
1173			raise ValueError(f"Repetition count must be non-negative — got {count}")
1174		if count == 0:
1175			return Motif.empty()
1176		if count == 1:
1177			return self
1178
1179		return Phrase((self,) * count)
1180
1181	__rmul__ = __mul__
1182
1183	def __and__ (self, other: typing.Any) -> "Motif":
1184
1185		"""``a & b`` — parallel merge; the spelled form is :meth:`stack`."""
1186
1187		if isinstance(other, (Motif, Phrase)):
1188			return self.stack(other)
1189
1190		return NotImplemented
1191
1192	# ── transforms (pure; control gestures ride time, ignore pitch) ─────
1193
1194	def reverse (self) -> "Motif":
1195
1196		"""Mirror the figure in time; ramps swap direction (a rising sweep falls)."""
1197
1198		events = tuple(
1199			dataclasses.replace(e, beat=max(0.0, self.length - e.beat - e.duration))
1200			for e in self.events
1201		)
1202		controls = tuple(
1203			dataclasses.replace(
1204				c,
1205				beat = max(0.0, self.length - c.beat - c.span),
1206				start = c.start if c.end is None else c.end,
1207				end = c.end if c.end is None else c.start,
1208			)
1209			for c in self.controls
1210		)
1211
1212		return Motif(events=events, length=self.length, controls=controls, fit=self.fit)
1213
1214	def rotate (self, beats: float) -> "Motif":
1215
1216		"""Shift every onset by *beats*, wrapping modulo the length (spans ride along)."""
1217
1218		if self.length == 0:
1219			return self
1220
1221		events = tuple(dataclasses.replace(e, beat=(e.beat + beats) % self.length) for e in self.events)
1222		controls = tuple(dataclasses.replace(c, beat=(c.beat + beats) % self.length) for c in self.controls)
1223
1224		return Motif(events=events, length=self.length, controls=controls, fit=self.fit)
1225
1226	def stretch (self, factor: float) -> "Motif":
1227
1228		"""Scale time by *factor* (2.0 = half-time feel): beats, durations, spans, and length."""
1229
1230		if factor <= 0:
1231			raise ValueError(f"Stretch factor must be positive — got {factor}")
1232
1233		events = tuple(
1234			dataclasses.replace(e, beat=e.beat * factor, duration=e.duration * factor)
1235			for e in self.events
1236		)
1237		controls = tuple(
1238			dataclasses.replace(c, beat=c.beat * factor, span=c.span * factor)
1239			for c in self.controls
1240		)
1241
1242		return Motif(events=events, length=self.length * factor, controls=controls, fit=self.fit)
1243
1244	def quantize (self, grid: float) -> "Motif":
1245
1246		"""Snap note onsets to the nearest multiple of *grid* beats (control gestures untouched).
1247
1248		An onset exactly midway between grid lines snaps LATER (round half
1249		up) — every midpoint moves the same way, the predictable behaviour
1250		for a musician.  (Python's own ``round()`` is half-to-even, which
1251		made exact midpoints snap in alternating directions.)
1252		"""
1253
1254		if grid <= 0:
1255			raise ValueError(f"Quantize grid must be positive — got {grid}")
1256
1257		events = tuple(
1258			dataclasses.replace(e, beat=math.floor(e.beat / grid + 0.5) * grid)
1259			for e in self.events
1260		)
1261
1262		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1263
1264	def accent (self, beat: float, amount: int = 20) -> "Motif":
1265
1266		"""Add *amount* velocity to every note at the given beat position (0-based beats)."""
1267
1268		def boost (velocity: typing.Union[int, typing.Tuple[int, int]]) -> typing.Union[int, typing.Tuple[int, int]]:
1269			# Clamp both ends: a negative amount (a de-accent) must not store
1270			# a velocity below 1, which MIDI cannot play.
1271			if isinstance(velocity, tuple):
1272				return (max(1, min(127, velocity[0] + amount)), max(1, min(127, velocity[1] + amount)))
1273			return max(1, min(127, velocity + amount))
1274
1275		events = tuple(
1276			dataclasses.replace(e, velocity=boost(e.velocity)) if abs(e.beat - beat) < 1e-9 else e
1277			for e in self.events
1278		)
1279
1280		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1281
1282	def with_velocity (self, velocity: typing.Union[int, typing.Tuple[int, int]]) -> "Motif":
1283
1284		"""Replace every note's velocity (an int, or a ``(low, high)`` random range)."""
1285
1286		events = tuple(dataclasses.replace(e, velocity=velocity) for e in self.events)
1287
1288		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1289
1290	def _nudged_pitch (self, pitch: PitchSpec, rng: random.Random) -> PitchSpec:
1291
1292		"""One varied pitch: a small melodic nudge that always changes the note.
1293
1294		Degrees move by scale steps, MIDI ints by semitones, chord tones by
1295		index; an Approach's target is nudged.  Drum names raise — a varied
1296		drum is a different instrument, not a variation.
1297		"""
1298
1299		if isinstance(pitch, Degree):
1300			steps = [pitch.step + delta for delta in (-2, -1, 1, 2) if pitch.step + delta >= 1]
1301			return dataclasses.replace(pitch, step = rng.choice(steps))
1302		if isinstance(pitch, ChordTone):
1303			indices = [pitch.index + delta for delta in (-1, 1) if pitch.index + delta >= 1]
1304			return ChordTone(rng.choice(indices), octave = pitch.octave)
1305		if isinstance(pitch, Approach):
1306			nudged = self._nudged_pitch(pitch.target, rng)
1307			if not isinstance(nudged, (int, Degree, ChordTone)):
1308				raise TypeError(f"cannot vary an Approach aimed at {type(nudged).__name__} content")
1309			return Approach(nudged)
1310		if isinstance(pitch, int):
1311			return pitch + rng.choice((-2, -1, 1, 2))
1312
1313		raise TypeError(
1314			f"vary() moves pitches — {type(pitch).__name__} content cannot vary "
1315			"(a varied drum is a different instrument)"
1316		)
1317
1318	def vary (
1319		self,
1320		notes: int = 1,
1321		position: str = "end",
1322		seed: typing.Optional[int] = None,
1323		rng: typing.Optional[random.Random] = None,
1324		keep_contour: bool = False,
1325	) -> "Motif":
1326
1327		"""Replace a few pitches, preserving the rhythm — the smallest variation.
1328
1329		Rhythm, velocities, durations, rests, and control gestures are
1330		untouched; only the chosen notes' pitches move (by a small melodic
1331		nudge: scale steps for degrees, semitones for MIDI ints).
1332
1333		Parameters:
1334			notes: How many pitched notes to vary (clamped to what exists).
1335			position: Which notes — ``"end"`` (the tail, the default),
1336				``"start"``, or ``"anywhere"`` (drawn from the stream).
1337			seed: Seed for the variation.  A standalone vary without a seed
1338				warns — module-level nondeterminism breaks live reload.
1339			rng: An explicit random stream (overrides ``seed``; used by
1340				recipe machinery).
1341			keep_contour: When True, the variation preserves the line's
1342				CSEG — every varied note keeps its rank relations with
1343				every other note, so the melodic shape is identical (the
1344				motif-identity guard).  Where no nudge can preserve the
1345				contour, that note stays unchanged — shape wins over
1346				motion.
1347
1348		Example:
1349			```python
1350			answer = call.vary(notes=1, seed=4)     # same figure, new tail note
1351			```
1352		"""
1353
1354		if notes < 0:
1355			raise ValueError(f"notes must be at least 0, got {notes}")
1356		if position not in ("end", "start", "anywhere"):
1357			raise ValueError(f'position must be "end", "start", or "anywhere" — got {position!r}')
1358
1359		if rng is None:
1360			if seed is None:
1361				warnings.warn(
1362					"vary() without seed= is nondeterministic — pass seed= so the "
1363					"value survives live reload",
1364					stacklevel = 2,
1365				)
1366				rng = random.Random()
1367			else:
1368				rng = random.Random(seed)
1369
1370		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1371		count = min(notes, len(pitched_indices))
1372
1373		if count == 0:
1374			return self
1375
1376		if position == "end":
1377			chosen = pitched_indices[-count:]
1378		elif position == "start":
1379			chosen = pitched_indices[:count]
1380		else:
1381			chosen = sorted(rng.sample(pitched_indices, count))
1382
1383		events = list(self.events)
1384
1385		for index in chosen:
1386			if keep_contour:
1387				replacement = self._contour_safe_nudge(events, index, pitched_indices, rng)
1388				if replacement is not None:
1389					events[index] = dataclasses.replace(events[index], pitch = replacement)
1390			else:
1391				events[index] = dataclasses.replace(events[index], pitch = self._nudged_pitch(events[index].pitch, rng))
1392
1393		return Motif(events = tuple(events), length = self.length, controls = self.controls, fit = self.fit)
1394
1395	@staticmethod
1396	def _rank_value (pitch: PitchSpec) -> float:
1397
1398		"""A comparable height for contour ranking (uniform content only)."""
1399
1400		if isinstance(pitch, Degree):
1401			return pitch.octave * 7 + pitch.step + 0.4 * pitch.chroma
1402		if isinstance(pitch, ChordTone):
1403			return pitch.octave * 4 + pitch.index
1404		if isinstance(pitch, int):
1405			return float(pitch)
1406
1407		raise TypeError(f"keep_contour needs rankable pitches — {type(pitch).__name__} content has no height")
1408
1409	def _contour_safe_nudge (
1410		self,
1411		events: typing.List[MotifEvent],
1412		index: int,
1413		pitched_indices: typing.List[int],
1414		rng: random.Random,
1415	) -> typing.Optional[PitchSpec]:
1416
1417		"""A nudge for events[index] that preserves its CSEG rank relations.
1418
1419		Candidates are the usual small nudges, filtered to those keeping the
1420		note's above/below/equal relation to every other pitched note.  One
1421		rng draw happens regardless (stream stability); ``None`` means no
1422		candidate preserves the shape — leave the note alone.
1423		"""
1424
1425		pitch = events[index].pitch
1426
1427		if isinstance(pitch, Degree):
1428			candidates: typing.List[PitchSpec] = [
1429				dataclasses.replace(pitch, step = pitch.step + delta)
1430				for delta in (-2, -1, 1, 2) if pitch.step + delta >= 1
1431			]
1432		elif isinstance(pitch, int):
1433			candidates = [pitch + delta for delta in (-2, -1, 1, 2)]
1434		else:
1435			raise TypeError(f"keep_contour cannot vary {type(pitch).__name__} content")
1436
1437		original = self._rank_value(pitch)
1438		others = [
1439			(self._rank_value(events[other].pitch), other)
1440			for other in pitched_indices if other != index
1441		]
1442
1443		def preserves (candidate: PitchSpec) -> bool:
1444			height = self._rank_value(candidate)
1445			for other_height, _ in others:
1446				before = (original > other_height) - (original < other_height)
1447				after = (height > other_height) - (height < other_height)
1448				if before != after:
1449					return False
1450			return True
1451
1452		surviving = [candidate for candidate in candidates if preserves(candidate)]
1453
1454		# One draw either way, so adding keep_contour never shifts the stream
1455		# consumed by the notes around this one.
1456		draw = rng.random()
1457
1458		if not surviving:
1459			return None
1460
1461		return surviving[int(draw * len(surviving)) % len(surviving)]
1462
1463	def answer (self, to: typing.Union[int, Degree] = 1) -> "Motif":
1464
1465		"""Call → response: re-aim the tail to a stable degree.
1466
1467		The classic consequent move — the figure repeats but its last pitched
1468		note lands home (degree 1 by default; pass ``to=5`` for a half-close,
1469		or a full ``Degree`` for register control).  Everything else —
1470		rhythm, the other pitches, velocities, controls — is untouched.
1471
1472		Degree content only: absolute MIDI has no degrees to re-aim (build
1473		the call with ``motif([...])``), and drums raise.
1474		"""
1475
1476		target = to if isinstance(to, Degree) else Degree(int(to))
1477
1478		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1479
1480		if not pitched_indices:
1481			return self
1482
1483		last = self.events[pitched_indices[-1]]
1484
1485		if not isinstance(last.pitch, Degree):
1486			raise TypeError(
1487				f"answer() re-aims scale degrees — the tail is {type(last.pitch).__name__} "
1488				"content (build the call with motif([...]) for degree content)"
1489			)
1490
1491		if isinstance(to, int):
1492			# Keep the call's register: only the step is re-aimed.
1493			target = dataclasses.replace(last.pitch, step = int(to), chroma = 0)
1494
1495		events = list(self.events)
1496		events[pitched_indices[-1]] = dataclasses.replace(last, pitch = target)
1497
1498		return Motif(events = tuple(events), length = self.length, controls = self.controls, fit = self.fit)
1499
1500	def pitched (self, spec: PitchSpec) -> "Motif":
1501
1502		"""
1503		Replace every pitch with one spec — a kick rhythm becomes a bass line.
1504
1505		``"root"`` / ``"third"`` / ``"fifth"`` / ``"seventh"`` become chord
1506		tones; any other string is a drum name; ints are MIDI; Degree /
1507		ChordTone / Approach pass through.
1508		"""
1509
1510		if isinstance(spec, str) and spec in _CHORD_TONE_NAMES:
1511			spec = ChordTone(spec)
1512
1513		events = tuple(dataclasses.replace(e, pitch=spec) for e in self.events)
1514
1515		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1516
1517	def rhythm (self) -> "Motif":
1518
1519		"""
1520		Strip pitches (and control gestures): a reusable rhythmic skeleton.
1521
1522		Timing, velocities, durations, and probabilities survive; re-pitch
1523		with :meth:`pitched` before placement (placing a skeleton raises).
1524		"""
1525
1526		events = tuple(dataclasses.replace(e, pitch=None) for e in self.events)
1527
1528		return Motif(events=events, length=self.length)
1529
1530	def onsets (self) -> typing.List[float]:
1531
1532		"""The note onset beats, in order — ready for rhythm-first generation."""
1533
1534		return [e.beat for e in self.events]
1535
1536	def transpose (self, steps: typing.Optional[int] = None, semitones: typing.Optional[int] = None) -> "Motif":
1537
1538		"""
1539		Transpose pitched content; the keyword names the unit.
1540
1541		``steps=`` moves scale degrees diatonically (the sequencing move) and
1542		raises on absolute-MIDI or drum content; ``semitones=`` is the
1543		literal chromatic form for MIDI ints and degrees.  Drum motifs raise
1544		on both — a transposed drum name is a different instrument, not a
1545		transposition.
1546		"""
1547
1548		if (steps is None) == (semitones is None):
1549			raise ValueError("transpose() takes exactly one of steps= or semitones=")
1550
1551		def move (pitch: PitchSpec) -> PitchSpec:
1552
1553			if pitch is None:
1554				return None
1555
1556			if isinstance(pitch, Approach):
1557				moved = move(pitch.target)
1558				if not isinstance(moved, (int, Degree, ChordTone)):
1559					raise TypeError(f"transpose cannot aim an Approach at {type(moved).__name__} content")
1560				return Approach(moved)
1561
1562			if steps is not None:
1563				if isinstance(pitch, Degree):
1564					return dataclasses.replace(pitch, step=pitch.step + steps)
1565				raise TypeError(
1566					f"transpose(steps=) moves scale degrees — {type(pitch).__name__} content "
1567					f"has no degrees (use semitones= for MIDI ints)"
1568				)
1569
1570			assert semitones is not None	# exactly one of steps/semitones is set (validated above)
1571
1572			if isinstance(pitch, int):
1573				return pitch + semitones
1574			if isinstance(pitch, Degree):
1575				return dataclasses.replace(pitch, chroma=pitch.chroma + semitones)
1576			raise TypeError(f"transpose(semitones=) cannot move {type(pitch).__name__} content")
1577
1578		events = tuple(dataclasses.replace(e, pitch=move(e.pitch)) for e in self.events)
1579
1580		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1581
1582	def invert (self, pivot: typing.Optional[int] = None) -> "Motif":
1583
1584		"""
1585		Mirror pitches around a pivot: MIDI content around a MIDI pivot,
1586		degree content around a degree pivot (default: the first note's pitch).
1587		Drum motifs raise.
1588		"""
1589
1590		pitched_events = [e for e in self.events if e.pitch is not None]
1591
1592		if not pitched_events:
1593			return self
1594
1595		first = pitched_events[0].pitch
1596
1597		if pivot is None:
1598			if isinstance(first, int):
1599				pivot = first
1600			elif isinstance(first, Degree):
1601				pivot = first.step
1602			else:
1603				raise TypeError(f"invert() cannot derive a pivot from {type(first).__name__} content")
1604
1605		def mirror (pitch: PitchSpec) -> PitchSpec:
1606
1607			if pitch is None:
1608				return None
1609			if isinstance(pitch, int):
1610				return 2 * pivot - pitch
1611			if isinstance(pitch, Degree):
1612				mirrored = 2 * pivot - pitch.step
1613				if mirrored < 1:
1614					raise ValueError(
1615						f"invert() around degree {pivot} sends degree {pitch.step} below the tonic — "
1616						f"raise the pivot or use Degree octaves"
1617					)
1618				# Reflection around the pivot (read at octave 0) is an isometry, so a
1619				# note's register flips too: a degree an octave above the pivot lands an
1620				# octave below it.  Negating octave needs no scale length and leaves
1621				# octave-0 content unchanged.
1622				return dataclasses.replace(pitch, step=mirrored, octave=-pitch.octave, chroma=-pitch.chroma)
1623			raise TypeError(f"invert() cannot mirror {type(pitch).__name__} content")
1624
1625		events = tuple(dataclasses.replace(e, pitch=mirror(e.pitch)) for e in self.events)
1626
1627		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1628
1629	# ── description ─────────────────────────────────────────────────────
1630
1631	def describe (self) -> str:
1632
1633		"""A readable one-line summary: length, notes (pitch@beat), and control gestures."""
1634
1635		notes = ", ".join(f"{_pitch_label(e.pitch)}@{e.beat:g}" for e in self.events)
1636		parts = [f"Motif {self.length:g} beats", f"[{notes}]" if notes else "[no notes]"]
1637
1638		if self.controls:
1639			gestures = ", ".join(_control_label(c) for c in self.controls)
1640			parts.append(f"controls [{gestures}]")
1641
1642		return " ".join(parts)
1643
1644	def __str__ (self) -> str:
1645
1646		"""Printable form (same as :meth:`describe`)."""
1647
1648		return self.describe()

An immutable musical figure: timed note events + control gestures + a length in beats.

Construct via the classmethods (degrees(), notes(), hits(), steps(), euclidean(), the control-gesture constructors, or from_events()) rather than positionally. length is explicit — a trailing rest is meaningful.

Motif( events: Tuple[MotifEvent, ...], length: float, controls: Tuple[ControlEvent, ...] = (), fit: Optional[float] = None)
events: Tuple[MotifEvent, ...]
length: float
controls: Tuple[ControlEvent, ...] = ()
fit: Optional[float] = None
@classmethod
def empty(cls) -> Motif:
403	@classmethod
404	def empty (cls) -> "Motif":
405
406		"""The empty motif (zero events, zero length) — the identity for ``then``."""
407
408		return cls(events=(), length=0.0)

The empty motif (zero events, zero length) — the identity for then.

@classmethod
def from_events( cls, events: Iterable[MotifEvent], length: Optional[float] = None, controls: Iterable[ControlEvent] = ()) -> Motif:
410	@classmethod
411	def from_events (
412		cls,
413		events: typing.Iterable[MotifEvent],
414		length: typing.Optional[float] = None,
415		controls: typing.Iterable[ControlEvent] = (),
416	) -> "Motif":
417
418		"""Build a motif from explicit events (power use; length defaults to the next whole beat)."""
419
420		events = tuple(events)
421		controls = tuple(controls)
422
423		return cls(
424			events = events,
425			length = _computed_length(events, controls) if length is None else length,
426			controls = controls,
427		)

Build a motif from explicit events (power use; length defaults to the next whole beat).

@classmethod
def degrees( cls, degrees: List[Union[int, Degree, NoneType]], beats: Optional[List[float]] = None, velocities: Any = 100, durations: Any = 1.0, probabilities: Any = 1.0, length: Optional[float] = None) -> Motif:
469	@classmethod
470	def degrees (
471		cls,
472		degrees: typing.List[typing.Union[int, Degree, None]],
473		beats: typing.Optional[typing.List[float]] = None,
474		velocities: typing.Any = _DEFAULT_VELOCITY,
475		durations: typing.Any = 1.0,
476		probabilities: typing.Any = 1.0,
477		length: typing.Optional[float] = None,
478	) -> "Motif":
479
480		"""
481		A melody written as 1-based scale degrees, one per beat by default.
482
483		Elements are ints (1 = tonic, 8 = tonic an octave up), ``None`` for a
484		rest (the beat slot still advances), or :class:`Degree` for octave/
485		chromatic detail.  Resolved against key + scale at placement.
486		Durations default to a full beat (each note holds its slot).
487		"""
488
489		converted: typing.List[PitchSpec] = []
490
491		for element in degrees:
492			if isinstance(element, int):
493				if element > _MAX_PLAUSIBLE_DEGREE:
494					raise ValueError(
495						f"Degree {element} is implausibly large — scale degrees are 1-based "
496						f"(8 = tonic an octave up). For MIDI note numbers use Motif.notes()."
497					)
498				converted.append(Degree(element))
499			elif isinstance(element, Degree) or element is None:
500				converted.append(element)
501			else:
502				raise TypeError(f"Motif.degrees takes ints, Degree, or None — got {type(element).__name__}")
503
504		return cls._from_sequence(converted, beats, velocities, durations, probabilities, length)

A melody written as 1-based scale degrees, one per beat by default.

Elements are ints (1 = tonic, 8 = tonic an octave up), None for a rest (the beat slot still advances), or Degree for octave/ chromatic detail. Resolved against key + scale at placement. Durations default to a full beat (each note holds its slot).

@classmethod
def notes( cls, notes: List[Optional[int]], beats: Optional[List[float]] = None, velocities: Any = 100, durations: Any = 1.0, probabilities: Any = 1.0, length: Optional[float] = None) -> Motif:
506	@classmethod
507	def notes (
508		cls,
509		notes: typing.List[typing.Union[int, None]],
510		beats: typing.Optional[typing.List[float]] = None,
511		velocities: typing.Any = _DEFAULT_VELOCITY,
512		durations: typing.Any = 1.0,
513		probabilities: typing.Any = 1.0,
514		length: typing.Optional[float] = None,
515	) -> "Motif":
516
517		"""A melody written as absolute MIDI note numbers (60 = middle C); ``None`` = rest."""
518
519		for element in notes:
520			# bool is a subclass of int, but True/False are never MIDI notes.
521			if isinstance(element, bool) or not (isinstance(element, int) or element is None):
522				raise TypeError(f"Motif.notes takes MIDI ints or None — got {type(element).__name__}")
523
524		return cls._from_sequence(list(notes), beats, velocities, durations, probabilities, length)

A melody written as absolute MIDI note numbers (60 = middle C); None = rest.

@classmethod
def hits( cls, pitch: Union[int, str], beats: List[float], length: Optional[float] = None, velocities: Any = 100, durations: Any = 0.1, probabilities: Any = 1.0) -> Motif:
526	@classmethod
527	def hits (
528		cls,
529		pitch: typing.Union[int, str],
530		beats: typing.List[float],
531		length: typing.Optional[float] = None,
532		velocities: typing.Any = _DEFAULT_VELOCITY,
533		durations: typing.Any = 0.1,
534		probabilities: typing.Any = 1.0,
535	) -> "Motif":
536
537		"""One pitch (usually a drum name) at a list of beat positions — the ``hit()`` convention."""
538
539		return cls._from_sequence([pitch] * len(beats), list(beats), velocities, durations, probabilities, length)

One pitch (usually a drum name) at a list of beat positions — the hit() convention.

@classmethod
def steps( cls, steps: List[int], pitches: Any, velocities: Any = 100, durations: Any = 0.1, probabilities: Any = 1.0, step_duration: float = 0.25, length: Optional[float] = None) -> Motif:
541	@classmethod
542	def steps (
543		cls,
544		steps: typing.List[int],
545		pitches: typing.Any,
546		velocities: typing.Any = _DEFAULT_VELOCITY,
547		durations: typing.Any = 0.1,
548		probabilities: typing.Any = 1.0,
549		step_duration: float = 0.25,
550		length: typing.Optional[float] = None,
551	) -> "Motif":
552
553		"""
554		Grid placement — the ``sequence()`` convention: ``steps`` are 0-based
555		grid indices (sixteenths by default), ``pitches`` a scalar or
556		parallel list of MIDI ints or drum names.
557		"""
558
559		n = len(steps)
560		pitch_list = _expand("pitches", pitches, n)
561		onsets = [s * step_duration for s in steps]
562
563		if length is None and n:
564			length = float(math.ceil((max(steps) + 1) * step_duration))
565
566		return cls._from_sequence(pitch_list, onsets, velocities, durations, probabilities, length)

Grid placement — the sequence() convention: steps are 0-based grid indices (sixteenths by default), pitches a scalar or parallel list of MIDI ints or drum names.

@classmethod
def euclidean( cls, pulses: int, steps: int, pitch: Union[int, str], length: float = 4.0, velocities: Any = 100, durations: Any = 0.1, probabilities: Any = 1.0) -> Motif:
568	@classmethod
569	def euclidean (
570		cls,
571		pulses: int,
572		steps: int,
573		pitch: typing.Union[int, str],
574		length: float = 4.0,
575		velocities: typing.Any = _DEFAULT_VELOCITY,
576		durations: typing.Any = 0.1,
577		probabilities: typing.Any = 1.0,
578	) -> "Motif":
579
580		"""A euclidean rhythm as a value: *pulses* spread evenly across *steps* over *length* beats."""
581
582		# bool is a subclass of int, but True/False are never MIDI notes.
583		if isinstance(pitch, bool):
584			raise TypeError(f"Motif.euclidean takes a MIDI int or drum name for pitch — got {pitch!r}")
585
586		# The kernel returns one 0/1 flag per grid step; onsets are the 1s.
587		# It validates pulses first, so pulses > steps still raises clearly.
588		flags = subsequence.sequence_utils.generate_euclidean_sequence(steps=steps, pulses=pulses)
589
590		if steps <= 0:
591			# A grid of zero steps holds no onsets — an empty motif of the given
592			# length, matching pulses=0 on a real grid (the empty-input policy).
593			return cls._from_sequence([], [], velocities, durations, probabilities, length)
594
595		step_duration = length / steps
596		onsets = [i * step_duration for i, flag in enumerate(flags) if flag]
597
598		return cls._from_sequence(
599			[pitch] * len(onsets),
600			onsets,
601			velocities, durations, probabilities, length,
602		)

A euclidean rhythm as a value: pulses spread evenly across steps over length beats.

@classmethod
def preset( cls, name: str, pitch: Union[int, str, NoneType] = None, length: float = 4.0, velocities: Any = 100, durations: Any = 0.1, probabilities: Any = 1.0) -> Motif:
604	@classmethod
605	def preset (
606		cls,
607		name: str,
608		pitch: typing.Optional[typing.Union[int, str]] = None,
609		length: float = 4.0,
610		velocities: typing.Any = _DEFAULT_VELOCITY,
611		durations: typing.Any = 0.1,
612		probabilities: typing.Any = 1.0,
613	) -> "Motif":
614
615		"""A named world-rhythm timeline as a value — ``Motif.preset("son_clave_3_2")``.
616
617		Looks a curated timeline up in the world-rhythm table (clave family,
618		West-African bell patterns, tresillo/cinquillo, samba) and lays its
619		onsets across *length* beats.  Onset positions are exact pulse indices
620		from Toussaint's "The Geometry of Musical Rhythm"; each preset declares
621		its own grid (16 for the clave/4-4 timelines, 12 for the bell
622		patterns) and a default drum voice.
623
624		Parameters:
625			name: A preset name (``KeyError``-style ValueError lists them all).
626			pitch: The voice — a drum name or MIDI int; defaults to the
627				preset's General-MIDI voice (``"claves"``, ``"cowbell"``,
628				``"side_stick"``, ``"low_conga"``), so it sounds against the
629				standard GM drum map without a ``pitch=``.
630			length: Total beats the cycle spans (4 = one common-time bar).
631			velocities / durations / probabilities: The parallel-list params.
632
633		Returns:
634			A drum/pitched :class:`Motif` of the timeline's onsets.
635
636		Raises:
637			ValueError: If *name* is not a known preset.
638
639		Example:
640			```python
641			clave = subsequence.Motif.preset("son_clave_3_2")              # GM "claves"
642			bell  = subsequence.Motif.preset("bembe", pitch="cowbell")     # 12-pulse
643			```
644		"""
645
646		if name not in _WORLD_RHYTHMS:
647			known = ", ".join(sorted(_WORLD_RHYTHMS))
648			raise ValueError(f"Unknown rhythm preset {name!r}. Known presets: {known}.")
649
650		steps, grid, voice = _WORLD_RHYTHMS[name]
651
652		return cls.steps(
653			steps = list(steps),
654			pitches = pitch if pitch is not None else voice,
655			velocities = velocities,
656			durations = durations,
657			probabilities = probabilities,
658			step_duration = length / grid,
659			length = length,
660		)

A named world-rhythm timeline as a value — Motif.preset("son_clave_3_2").

Looks a curated timeline up in the world-rhythm table (clave family, West-African bell patterns, tresillo/cinquillo, samba) and lays its onsets across length beats. Onset positions are exact pulse indices from Toussaint's "The Geometry of Musical Rhythm"; each preset declares its own grid (16 for the clave/4-4 timelines, 12 for the bell patterns) and a default drum voice.

Arguments:
  • name: A preset name (KeyError-style ValueError lists them all).
  • pitch: The voice — a drum name or MIDI int; defaults to the preset's General-MIDI voice ("claves", "cowbell", "side_stick", "low_conga"), so it sounds against the standard GM drum map without a pitch=.
  • length: Total beats the cycle spans (4 = one common-time bar).
  • velocities / durations / probabilities: The parallel-list params.
Returns:

A drum/pitched Motif of the timeline's onsets.

Raises:
  • ValueError: If name is not a known preset.
Example:
clave = subsequence.Motif.preset("son_clave_3_2")              # GM "claves"
bell  = subsequence.Motif.preset("bembe", pitch="cowbell")     # 12-pulse
@classmethod
def cc( cls, control: Union[int, str], values: List[int], beats: List[float], length: Optional[float] = None, probabilities: Any = 1.0) -> Motif:
733	@classmethod
734	def cc (cls, control: typing.Union[int, str], values: typing.List[int], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
735
736		"""Discrete CC writes at beat positions — mirrors ``p.cc()``; names resolve at placement."""
737
738		return cls._control_writes(CC(control), list(values), list(beats), length, probabilities)

Discrete CC writes at beat positions — mirrors p.cc(); names resolve at placement.

@classmethod
def cc_ramp( cls, control: Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, Callable[[float], float]] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif:
740	@classmethod
741	def cc_ramp (cls, control: typing.Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
742
743		"""A CC value swept ``start`` → ``end`` over a beat range — mirrors ``p.cc_ramp()``."""
744
745		return cls._control_ramp(CC(control), start, end, beat_start, beat_end, shape, length, probability)

A CC value swept startend over a beat range — mirrors p.cc_ramp().

@classmethod
def pitch_bend( cls, values: List[float], beats: List[float], length: Optional[float] = None, probabilities: Any = 1.0) -> Motif:
747	@classmethod
748	def pitch_bend (cls, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
749
750		"""Discrete pitch-bend writes (-1.0 to 1.0) at beat positions — mirrors ``p.pitch_bend()``."""
751
752		return cls._control_writes(PitchBend(), list(values), list(beats), length, probabilities)

Discrete pitch-bend writes (-1.0 to 1.0) at beat positions — mirrors p.pitch_bend().

@classmethod
def pitch_bend_ramp( cls, start: float, end: float, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, Callable[[float], float]] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif:
754	@classmethod
755	def pitch_bend_ramp (cls, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
756
757		"""Pitch bend swept ``start`` → ``end`` (-1.0 to 1.0) over a beat range — mirrors ``p.pitch_bend_ramp()``."""
758
759		return cls._control_ramp(PitchBend(), start, end, beat_start, beat_end, shape, length, probability)

Pitch bend swept startend (-1.0 to 1.0) over a beat range — mirrors p.pitch_bend_ramp().

@classmethod
def nrpn( cls, parameter: Union[int, str], values: List[int], beats: List[float], fine: bool = False, null_reset: bool = True, length: Optional[float] = None, probabilities: Any = 1.0) -> Motif:
761	@classmethod
762	def nrpn (cls, parameter: typing.Union[int, str], values: typing.List[int], beats: typing.List[float], fine: bool = False, null_reset: bool = True, length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
763
764		"""Discrete NRPN parameter writes at beat positions — mirrors ``p.nrpn()``."""
765
766		return cls._control_writes(NRPN(parameter, fine=fine, null_reset=null_reset), list(values), list(beats), length, probabilities)

Discrete NRPN parameter writes at beat positions — mirrors p.nrpn().

@classmethod
def nrpn_ramp( cls, parameter: Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, Callable[[float], float]] = 'linear', fine: bool = True, null_reset: bool = True, length: Optional[float] = None, probability: float = 1.0) -> Motif:
768	@classmethod
769	def nrpn_ramp (cls, parameter: typing.Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
770
771		"""An NRPN value swept over a beat range — mirrors ``p.nrpn_ramp()``."""
772
773		return cls._control_ramp(NRPN(parameter, fine=fine, null_reset=null_reset), start, end, beat_start, beat_end, shape, length, probability)

An NRPN value swept over a beat range — mirrors p.nrpn_ramp().

@classmethod
def rpn( cls, parameter: Union[int, str], values: List[int], beats: List[float], fine: bool = False, null_reset: bool = True, length: Optional[float] = None, probabilities: Any = 1.0) -> Motif:
775	@classmethod
776	def rpn (cls, parameter: typing.Union[int, str], values: typing.List[int], beats: typing.List[float], fine: bool = False, null_reset: bool = True, length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
777
778		"""Discrete RPN parameter writes at beat positions — mirrors ``p.rpn()``."""
779
780		return cls._control_writes(RPN(parameter, fine=fine, null_reset=null_reset), list(values), list(beats), length, probabilities)

Discrete RPN parameter writes at beat positions — mirrors p.rpn().

@classmethod
def rpn_ramp( cls, parameter: Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, Callable[[float], float]] = 'linear', fine: bool = True, null_reset: bool = True, length: Optional[float] = None, probability: float = 1.0) -> Motif:
782	@classmethod
783	def rpn_ramp (cls, parameter: typing.Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
784
785		"""An RPN value swept over a beat range — mirrors ``p.rpn_ramp()``."""
786
787		return cls._control_ramp(RPN(parameter, fine=fine, null_reset=null_reset), start, end, beat_start, beat_end, shape, length, probability)

An RPN value swept over a beat range — mirrors p.rpn_ramp().

@classmethod
def osc( cls, address: str, values: List[float], beats: List[float], length: Optional[float] = None, probabilities: Any = 1.0) -> Motif:
789	@classmethod
790	def osc (cls, address: str, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
791
792		"""Discrete OSC float sends at beat positions — mirrors ``p.osc()``."""
793
794		return cls._control_writes(OSC(address), list(values), list(beats), length, probabilities)

Discrete OSC float sends at beat positions — mirrors p.osc().

@classmethod
def osc_ramp( cls, address: str, start: float, end: float, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, Callable[[float], float]] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif:
796	@classmethod
797	def osc_ramp (cls, address: str, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
798
799		"""An OSC float swept over a beat range — mirrors ``p.osc_ramp()``."""
800
801		return cls._control_ramp(OSC(address), start, end, beat_start, beat_end, shape, length, probability)

An OSC float swept over a beat range — mirrors p.osc_ramp().

def then(self, other: Motif) -> Motif:
805	def then (self, other: "Motif") -> "Motif":
806
807		"""Closed sequential concat: glue *other* after this motif into ONE longer motif."""
808
809		if not isinstance(other, Motif):
810			raise TypeError(f"then() takes a Motif — got {type(other).__name__}")
811
812		return Motif(
813			events = self.events + tuple(dataclasses.replace(e, beat=e.beat + self.length) for e in other.events),
814			length = self.length + other.length,
815			controls = self.controls + tuple(dataclasses.replace(c, beat=c.beat + self.length) for c in other.controls),
816			# fit is a dial, not content: keep ours, inherit the other's when
817			# we have none — join()/tiling folds from empty() (fit=None), and
818			# must not silently strip a generated motif's chord-snapping.
819			fit = self.fit if self.fit is not None else other.fit,
820		)

Closed sequential concat: glue other after this motif into ONE longer motif.

@classmethod
def join( cls, motifs: Iterable[Motif]) -> Motif:
822	@classmethod
823	def join (cls, motifs: typing.Iterable["Motif"]) -> "Motif":
824
825		"""Fold a list of motifs into one with ``then`` (empty list → ``Motif.empty()``)."""
826
827		result = cls.empty()
828
829		for m in motifs:
830			result = result.then(m)
831
832		return result

Fold a list of motifs into one with then (empty list → Motif.empty()).

@classmethod
def generate( cls, rhythm: Any, length: Optional[float] = None, scale: Union[str, Sequence[int], NoneType] = None, contour: Optional[str] = None, end_on: Union[int, Degree, NoneType] = None, cadence: Optional[str] = None, pins: Optional[Dict[int, Union[int, Degree]]] = None, max_pitches: Optional[int] = None, velocities: Any = 100, durations: Any = 0.25, seed: Optional[int] = None, rng: Optional[random.Random] = None, state: Optional[Any] = None, nir_strength: float = 0.5, pitch_diversity: float = 0.6, tessitura_strength: float = 0.6) -> Motif:
 834	@classmethod
 835	def generate (
 836		cls,
 837		rhythm: typing.Any,
 838		length: typing.Optional[float] = None,
 839		scale: typing.Optional[typing.Union[str, typing.Sequence[int]]] = None,
 840		contour: typing.Optional[str] = None,
 841		end_on: typing.Optional[typing.Union[int, Degree]] = None,
 842		cadence: typing.Optional[str] = None,
 843		pins: typing.Optional[typing.Dict[int, typing.Union[int, Degree]]] = None,
 844		max_pitches: typing.Optional[int] = None,
 845		velocities: typing.Any = _DEFAULT_VELOCITY,
 846		durations: typing.Any = 0.25,
 847		seed: typing.Optional[int] = None,
 848		rng: typing.Optional[random.Random] = None,
 849		state: typing.Optional[typing.Any] = None,
 850		nir_strength: float = 0.5,
 851		pitch_diversity: float = 0.6,
 852		tessitura_strength: float = 0.6,
 853	) -> "Motif":
 854
 855		"""Generate a melodic motif — rhythm first, pitches walked, a value out.
 856
 857		The melody engine emitting a value: you give the **rhythm** (an onset
 858		list in beats, or another motif whose rhythm to borrow — cross-pattern
 859		rhythm reuse is shared values); the engine walks pitches over it
 860		through the soft scoring factors (NIR expectation, contour envelope,
 861		tessitura regression, diversity), honouring any pins.
 862
 863		The result emits **scale degrees** (resolved at placement against the
 864		composition key/scale), so a generated hook transposes, varies, and
 865		develops like a hand-written one.  ``scale=`` constrains *candidate
 866		choice only*: a name or interval list masks which pitches the walk
 867		may use, spelled relative to its best-fit reference (major or minor)
 868		— bind it in a composition whose scale matches that family and
 869		resolution is exact.  An explicit MIDI pitch pool (a list of note
 870		numbers) switches to absolute output (the sieve/atonal path).
 871
 872		Parameters:
 873			rhythm: Onset beats (``[0, 1, 1.5, 1.75, 2.5]``) or a Motif
 874				(its onsets are borrowed).
 875			length: Motif length in beats; defaults to the onsets rounded
 876				up to a whole 4-beat bar.
 877			scale: A scale name, an interval list, or an explicit MIDI
 878				pitch pool.  ``None`` = the plain seven degrees.
 879			contour: Envelope shaping the line's height over its span —
 880				``"arch"``, ``"valley"``, ``"ascending"``, ``"descending"``.
 881			end_on: Degree the line must end on — sugar for ``pins={-1: ...}``.
 882				Degree semantics: raises with an explicit MIDI pool (pin the
 883				exact note instead).
 884			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
 885				``"fakeout"``) — the line closes on that cadence's melodic
 886				degree (1 for the full closes and the fakeout, 5 for the
 887				open half).  Sugar for ``end_on=``; conflicts with it, and
 888				raises with an explicit MIDI pool like ``end_on=``.
 889			pins: ``{position: degree}`` — 1-based note positions (``-1`` =
 890				the last, the Python idiom); the engine fills between.  With
 891				an explicit MIDI pool there are no degrees to read, so each
 892				pin is the exact MIDI note to play (``Degree`` pins raise).
 893			max_pitches: Cap on distinct pitches (a tight pool is a hook);
 894				keeps the most central candidates.
 895			velocities / durations: Scalar or per-note list (the parallel-
 896				list convention).
 897			seed: Seed for the walk (required or warned — module-level
 898				nondeterminism breaks live reload).
 899			rng: Explicit stream (overrides ``seed``).
 900			state: A ``MelodicState`` whose dials, scoring factors, and
 901				melodic history seed the walk.  It is **copied** — building
 902				a value never mutates a module-level live object.  The
 903				candidate pool is not carried over: it is always rebuilt
 904				from ``scale=`` (pass an explicit pool there instead),
 905				though the state's key still sets the tonic that the NIR
 906				closure rule lands on.
 907			nir_strength / pitch_diversity / tessitura_strength: The walk's
 908				dials when no ``state`` is given.
 909
 910		Example:
 911			```python
 912			hook = subsequence.Motif.generate(
 913				rhythm=[0, 1, 1.5, 1.75, 2.5], scale="minor_pentatonic",
 914				contour="arch", end_on=1, seed=7,
 915			)
 916			```
 917		"""
 918
 919		import subsequence.melodic_state
 920
 921		onsets = list(rhythm.onsets()) if hasattr(rhythm, "onsets") else [float(b) for b in rhythm]
 922
 923		if cadence is not None:
 924			if end_on is not None:
 925				raise ValueError("cadence= already names the close degree — it conflicts with end_on=")
 926			end_on = subsequence.cadences.cadence_formula(cadence).close_degree
 927
 928		if not onsets:
 929			raise ValueError("generate() needs at least one onset — the rhythm comes first")
 930		if sorted(onsets) != onsets:
 931			raise ValueError("rhythm onsets must ascend")
 932
 933		if length is None:
 934			length = max(4.0, math.ceil((onsets[-1] + 1e-9) / 4.0) * 4.0)
 935		if onsets[-1] >= length:
 936			raise ValueError(f"the last onset ({onsets[-1]:g}) falls outside length={length:g}")
 937
 938		if rng is None:
 939			if seed is None:
 940				warnings.warn(
 941					"generate() without seed= is nondeterministic — pass seed= so the "
 942					"value survives live reload",
 943					stacklevel = 2,
 944				)
 945				rng = random.Random()
 946			else:
 947				rng = random.Random(seed)
 948
 949		# --- The candidate pool ------------------------------------------------
 950		absolute_pool: typing.Optional[typing.List[int]] = None
 951		intervals: typing.List[int]
 952
 953		if scale is None:
 954			intervals = list(subsequence.intervals.scale_pitch_classes(0, "ionian"))
 955		elif isinstance(scale, str):
 956			intervals = list(subsequence.intervals.scale_pitch_classes(0, scale))
 957		else:
 958			values = [int(v) for v in scale]
 959			if values and (min(values) != 0 or max(values) > 11):
 960				absolute_pool = sorted(values)		# an explicit MIDI pool: absolute output
 961				intervals = []
 962			else:
 963				intervals = sorted(set(values))
 964
 965		# Best-fit reference scale for degree spelling: whichever of major/
 966		# minor contains more of the pool (ties to major).  Bound under a
 967		# matching composition scale, resolution is exact.
 968		if absolute_pool is None:
 969			ionian = set(subsequence.intervals.scale_pitch_classes(0, "ionian"))
 970			aeolian = set(subsequence.intervals.scale_pitch_classes(0, "minor"))
 971			reference_name = "minor" if sum(i in aeolian for i in intervals) > sum(i in ionian for i in intervals) else "ionian"
 972			reference = list(subsequence.intervals.scale_pitch_classes(0, reference_name))
 973
 974		# --- The walking state (copied, never mutated in place) ----------------
 975		if state is not None:
 976			walker = state.clone()
 977			walker.rest_probability = 0.0		# generate is rhythm-first: every onset gets a
 978												# note, so the walker never rests (and never falls
 979												# back to a stuck repeat) — rests come from the rhythm
 980		else:
 981			walker = subsequence.melodic_state.MelodicState(
 982				nir_strength = nir_strength,
 983				pitch_diversity = pitch_diversity,
 984				tessitura_strength = tessitura_strength,
 985				chord_weight = 0.0,		# values have no chord context; fit applies at placement
 986			)
 987
 988		if absolute_pool is not None:
 989			walker.set_pool(absolute_pool)
 990		else:
 991			# Offsets over ~1.5 octaves anchored at 60 — register is decided
 992			# at placement (root=), so the anchor is arbitrary and erased.
 993			walker.set_pool([60 + octave * 12 + interval for octave in (0, 1) for interval in intervals if octave * 12 + interval <= 19])
 994
 995		if max_pitches is not None:
 996			if max_pitches < 1:
 997				raise ValueError("max_pitches must be at least 1")
 998			pool = sorted(walker._pitch_pool)
 999			centre = pool[len(pool) // 2]
1000			walker.set_pool(sorted(sorted(pool, key = lambda p: (abs(p - centre), p))[:max_pitches]))
1001
1002		# --- Pins ---------------------------------------------------------------
1003		resolved_pins: typing.Dict[int, int] = {}
1004		combined = dict(pins or {})
1005
1006		# cadence=/end_on= name scale DEGREES — meaningless against an explicit
1007		# MIDI pool, where they would silently land as raw (sub-audio) note
1008		# numbers.
1009		if absolute_pool is not None and end_on is not None:
1010			raise ValueError(
1011				"cadence=/end_on= name scale degrees, but this motif uses an "
1012				"explicit MIDI pool — pin the exact closing note instead: "
1013				"pins={-1: <midi note>}"
1014			)
1015
1016		if end_on is not None:
1017			if -1 in combined or len(onsets) in combined:
1018				raise ValueError("end_on conflicts with a pin on the last note — they name the same position")
1019			combined[-1] = end_on
1020
1021		for pin_position, pin_spec in combined.items():
1022			if not isinstance(pin_position, int) or isinstance(pin_position, bool):
1023				raise ValueError(f"pin positions are 1-based ints (or -1 for last), got {pin_position!r}")
1024			index = pin_position - 1 if pin_position >= 1 else len(onsets) + pin_position
1025			if not 0 <= index < len(onsets):
1026				raise ValueError(f"pin position {pin_position} is outside the {len(onsets)}-note rhythm")
1027			if absolute_pool is not None:
1028				# A raw int pins the exact MIDI note; a Degree has no meaning
1029				# here (the pool defines no scale to read it against).
1030				if not isinstance(pin_spec, int) or isinstance(pin_spec, bool):
1031					raise ValueError(
1032						f"pin {pin_spec!r} is a scale degree, but this motif uses an "
1033						"explicit MIDI pool — pin the exact MIDI note instead "
1034						"(e.g. pins={-1: 52})"
1035					)
1036				resolved_pins[index] = int(pin_spec)
1037			else:
1038				degree = pin_spec if isinstance(pin_spec, Degree) else Degree(int(pin_spec))
1039				step_index = (degree.step - 1) % len(reference)
1040				carry = (degree.step - 1) // len(reference)
1041				resolved_pins[index] = 60 + reference[step_index] + 12 * (carry + degree.octave) + degree.chroma
1042
1043		# --- The walk -----------------------------------------------------------
1044		envelopes: typing.Dict[str, typing.Callable[[float], float]] = {
1045			"arch": lambda pos: 0.15 + 0.8 * math.sin(math.pi * pos),
1046			"valley": lambda pos: 0.95 - 0.8 * math.sin(math.pi * pos),
1047			"ascending": lambda pos: 0.1 + 0.85 * pos,
1048			"descending": lambda pos: 0.95 - 0.85 * pos,
1049		}
1050
1051		if contour is not None and contour not in envelopes:
1052			known = ", ".join(sorted(envelopes))
1053			raise ValueError(f"unknown contour {contour!r} — expected one of: {known}")
1054
1055		chosen_pitches: typing.List[int] = []
1056
1057		for index, onset in enumerate(onsets):
1058
1059			if index in resolved_pins:
1060				pitch = resolved_pins[index]
1061				walker.record(pitch)	# pins enter the NIR context like chosen notes
1062			else:
1063				span_position = index / (len(onsets) - 1) if len(onsets) > 1 else 0.0
1064				target = envelopes[contour](span_position) if contour is not None else None
1065				picked = walker.choose_next(None, rng, beat = onset, position = span_position, contour_target = target)
1066				pitch = picked if picked is not None else walker._pitch_pool[0]
1067
1068			chosen_pitches.append(pitch)
1069
1070		# --- Emission ------------------------------------------------------------
1071		velocity_values = _expand("velocities", velocities, len(onsets))
1072		duration_values = _expand("durations", durations, len(onsets))
1073
1074		events = []
1075
1076		for index, (onset, pitch) in enumerate(zip(onsets, chosen_pitches)):
1077
1078			spec: PitchSpec
1079
1080			if absolute_pool is not None:
1081				spec = pitch
1082			else:
1083				offset = pitch - 60
1084				octave, pc = divmod(offset, 12)
1085				if pc in reference:
1086					spec = Degree(reference.index(pc) + 1, octave = octave)
1087				elif (pc + 1) % 12 in reference and pc + 1 <= 11:
1088					spec = Degree(reference.index(pc + 1) + 1, octave = octave, chroma = -1)
1089				else:
1090					spec = Degree(reference.index(pc - 1) + 1, octave = octave, chroma = 1)
1091
1092			events.append(MotifEvent(
1093				beat = onset,
1094				pitch = spec,
1095				velocity = velocity_values[index],
1096				duration = float(duration_values[index]),
1097			))
1098
1099		return cls(events = tuple(events), length = float(length), fit = 0.7)

Generate a melodic motif — rhythm first, pitches walked, a value out.

The melody engine emitting a value: you give the rhythm (an onset list in beats, or another motif whose rhythm to borrow — cross-pattern rhythm reuse is shared values); the engine walks pitches over it through the soft scoring factors (NIR expectation, contour envelope, tessitura regression, diversity), honouring any pins.

The result emits scale degrees (resolved at placement against the composition key/scale), so a generated hook transposes, varies, and develops like a hand-written one. scale= constrains candidate choice only: a name or interval list masks which pitches the walk may use, spelled relative to its best-fit reference (major or minor) — bind it in a composition whose scale matches that family and resolution is exact. An explicit MIDI pitch pool (a list of note numbers) switches to absolute output (the sieve/atonal path).

Arguments:
  • rhythm: Onset beats ([0, 1, 1.5, 1.75, 2.5]) or a Motif (its onsets are borrowed).
  • length: Motif length in beats; defaults to the onsets rounded up to a whole 4-beat bar.
  • scale: A scale name, an interval list, or an explicit MIDI pitch pool. None = the plain seven degrees.
  • contour: Envelope shaping the line's height over its span — "arch", "valley", "ascending", "descending".
  • end_on: Degree the line must end on — sugar for pins={-1: ...}. Degree semantics: raises with an explicit MIDI pool (pin the exact note instead).
  • cadence: A cadence name ("strong"/"soft"/"open"/ "fakeout") — the line closes on that cadence's melodic degree (1 for the full closes and the fakeout, 5 for the open half). Sugar for end_on=; conflicts with it, and raises with an explicit MIDI pool like end_on=.
  • pins: {position: degree} — 1-based note positions (-1 = the last, the Python idiom); the engine fills between. With an explicit MIDI pool there are no degrees to read, so each pin is the exact MIDI note to play (Degree pins raise).
  • max_pitches: Cap on distinct pitches (a tight pool is a hook); keeps the most central candidates.
  • velocities / durations: Scalar or per-note list (the parallel- list convention).
  • seed: Seed for the walk (required or warned — module-level nondeterminism breaks live reload).
  • rng: Explicit stream (overrides seed).
  • state: A MelodicState whose dials, scoring factors, and melodic history seed the walk. It is copied — building a value never mutates a module-level live object. The candidate pool is not carried over: it is always rebuilt from scale= (pass an explicit pool there instead), though the state's key still sets the tonic that the NIR closure rule lands on.
  • nir_strength / pitch_diversity / tessitura_strength: The walk's dials when no state is given.
Example:
hook = subsequence.Motif.generate(
        rhythm=[0, 1, 1.5, 1.75, 2.5], scale="minor_pentatonic",
        contour="arch", end_on=1, seed=7,
)
def stack( self, other: Union[Motif, Phrase]) -> Motif:
1101	def stack (self, other: typing.Union["Motif", "Phrase"]) -> "Motif":
1102
1103		"""
1104		Parallel merge (the spelled form of ``&``): event union, length = max.
1105
1106		No implicit tiling — a short gesture stacked under a long figure
1107		plays once.  Phrase operands flatten first.
1108		"""
1109
1110		if isinstance(other, Phrase):
1111			merged = other.flatten()
1112		elif isinstance(other, Motif):
1113			merged = other
1114		else:
1115			raise TypeError(f"stack() takes a Motif or Phrase — got {type(other).__name__}")
1116
1117		return Motif(
1118			events = self.events + merged.events,
1119			length = max(self.length, merged.length),
1120			controls = self.controls + merged.controls,
1121			fit = self.fit,
1122		)

Parallel merge (the spelled form of &): event union, length = max.

No implicit tiling — a short gesture stacked under a long figure plays once. Phrase operands flatten first.

def slice(self, start: float, end: float) -> Motif:
1124	def slice (self, start: float, end: float) -> "Motif":
1125
1126		"""
1127		A window onto the motif, on its own authority: events starting outside
1128		are dropped; durations and ramp spans truncate at the cut (a truncated
1129		ramp ends at its interpolated cut value).  Beats shift so the window
1130		starts at 0.
1131		"""
1132
1133		if end <= start:
1134			raise ValueError(f"slice end ({end}) must be after start ({start})")
1135
1136		events = tuple(
1137			dataclasses.replace(e, beat=e.beat - start, duration=min(e.duration, end - e.beat))
1138			for e in self.events
1139			if start <= e.beat < end
1140		)
1141
1142		controls = []
1143
1144		for c in self.controls:
1145			if not (start <= c.beat < end):
1146				continue
1147			if c.end is not None and c.beat + c.span > end:
1148				kept = end - c.beat
1149				controls.append(dataclasses.replace(
1150					c, beat=c.beat - start, span=kept, end=c._value_at(kept / c.span),
1151				))
1152			else:
1153				controls.append(dataclasses.replace(c, beat=c.beat - start))
1154
1155		return Motif(events=events, length=end - start, controls=tuple(controls), fit=self.fit)

A window onto the motif, on its own authority: events starting outside are dropped; durations and ramp spans truncate at the cut (a truncated ramp ends at its interpolated cut value). Beats shift so the window starts at 0.

def reverse(self) -> Motif:
1194	def reverse (self) -> "Motif":
1195
1196		"""Mirror the figure in time; ramps swap direction (a rising sweep falls)."""
1197
1198		events = tuple(
1199			dataclasses.replace(e, beat=max(0.0, self.length - e.beat - e.duration))
1200			for e in self.events
1201		)
1202		controls = tuple(
1203			dataclasses.replace(
1204				c,
1205				beat = max(0.0, self.length - c.beat - c.span),
1206				start = c.start if c.end is None else c.end,
1207				end = c.end if c.end is None else c.start,
1208			)
1209			for c in self.controls
1210		)
1211
1212		return Motif(events=events, length=self.length, controls=controls, fit=self.fit)

Mirror the figure in time; ramps swap direction (a rising sweep falls).

def rotate(self, beats: float) -> Motif:
1214	def rotate (self, beats: float) -> "Motif":
1215
1216		"""Shift every onset by *beats*, wrapping modulo the length (spans ride along)."""
1217
1218		if self.length == 0:
1219			return self
1220
1221		events = tuple(dataclasses.replace(e, beat=(e.beat + beats) % self.length) for e in self.events)
1222		controls = tuple(dataclasses.replace(c, beat=(c.beat + beats) % self.length) for c in self.controls)
1223
1224		return Motif(events=events, length=self.length, controls=controls, fit=self.fit)

Shift every onset by beats, wrapping modulo the length (spans ride along).

def stretch(self, factor: float) -> Motif:
1226	def stretch (self, factor: float) -> "Motif":
1227
1228		"""Scale time by *factor* (2.0 = half-time feel): beats, durations, spans, and length."""
1229
1230		if factor <= 0:
1231			raise ValueError(f"Stretch factor must be positive — got {factor}")
1232
1233		events = tuple(
1234			dataclasses.replace(e, beat=e.beat * factor, duration=e.duration * factor)
1235			for e in self.events
1236		)
1237		controls = tuple(
1238			dataclasses.replace(c, beat=c.beat * factor, span=c.span * factor)
1239			for c in self.controls
1240		)
1241
1242		return Motif(events=events, length=self.length * factor, controls=controls, fit=self.fit)

Scale time by factor (2.0 = half-time feel): beats, durations, spans, and length.

def quantize(self, grid: float) -> Motif:
1244	def quantize (self, grid: float) -> "Motif":
1245
1246		"""Snap note onsets to the nearest multiple of *grid* beats (control gestures untouched).
1247
1248		An onset exactly midway between grid lines snaps LATER (round half
1249		up) — every midpoint moves the same way, the predictable behaviour
1250		for a musician.  (Python's own ``round()`` is half-to-even, which
1251		made exact midpoints snap in alternating directions.)
1252		"""
1253
1254		if grid <= 0:
1255			raise ValueError(f"Quantize grid must be positive — got {grid}")
1256
1257		events = tuple(
1258			dataclasses.replace(e, beat=math.floor(e.beat / grid + 0.5) * grid)
1259			for e in self.events
1260		)
1261
1262		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)

Snap note onsets to the nearest multiple of grid beats (control gestures untouched).

An onset exactly midway between grid lines snaps LATER (round half up) — every midpoint moves the same way, the predictable behaviour for a musician. (Python's own round() is half-to-even, which made exact midpoints snap in alternating directions.)

def accent(self, beat: float, amount: int = 20) -> Motif:
1264	def accent (self, beat: float, amount: int = 20) -> "Motif":
1265
1266		"""Add *amount* velocity to every note at the given beat position (0-based beats)."""
1267
1268		def boost (velocity: typing.Union[int, typing.Tuple[int, int]]) -> typing.Union[int, typing.Tuple[int, int]]:
1269			# Clamp both ends: a negative amount (a de-accent) must not store
1270			# a velocity below 1, which MIDI cannot play.
1271			if isinstance(velocity, tuple):
1272				return (max(1, min(127, velocity[0] + amount)), max(1, min(127, velocity[1] + amount)))
1273			return max(1, min(127, velocity + amount))
1274
1275		events = tuple(
1276			dataclasses.replace(e, velocity=boost(e.velocity)) if abs(e.beat - beat) < 1e-9 else e
1277			for e in self.events
1278		)
1279
1280		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)

Add amount velocity to every note at the given beat position (0-based beats).

def with_velocity(self, velocity: Union[int, Tuple[int, int]]) -> Motif:
1282	def with_velocity (self, velocity: typing.Union[int, typing.Tuple[int, int]]) -> "Motif":
1283
1284		"""Replace every note's velocity (an int, or a ``(low, high)`` random range)."""
1285
1286		events = tuple(dataclasses.replace(e, velocity=velocity) for e in self.events)
1287
1288		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)

Replace every note's velocity (an int, or a (low, high) random range).

def vary( self, notes: int = 1, position: str = 'end', seed: Optional[int] = None, rng: Optional[random.Random] = None, keep_contour: bool = False) -> Motif:
1318	def vary (
1319		self,
1320		notes: int = 1,
1321		position: str = "end",
1322		seed: typing.Optional[int] = None,
1323		rng: typing.Optional[random.Random] = None,
1324		keep_contour: bool = False,
1325	) -> "Motif":
1326
1327		"""Replace a few pitches, preserving the rhythm — the smallest variation.
1328
1329		Rhythm, velocities, durations, rests, and control gestures are
1330		untouched; only the chosen notes' pitches move (by a small melodic
1331		nudge: scale steps for degrees, semitones for MIDI ints).
1332
1333		Parameters:
1334			notes: How many pitched notes to vary (clamped to what exists).
1335			position: Which notes — ``"end"`` (the tail, the default),
1336				``"start"``, or ``"anywhere"`` (drawn from the stream).
1337			seed: Seed for the variation.  A standalone vary without a seed
1338				warns — module-level nondeterminism breaks live reload.
1339			rng: An explicit random stream (overrides ``seed``; used by
1340				recipe machinery).
1341			keep_contour: When True, the variation preserves the line's
1342				CSEG — every varied note keeps its rank relations with
1343				every other note, so the melodic shape is identical (the
1344				motif-identity guard).  Where no nudge can preserve the
1345				contour, that note stays unchanged — shape wins over
1346				motion.
1347
1348		Example:
1349			```python
1350			answer = call.vary(notes=1, seed=4)     # same figure, new tail note
1351			```
1352		"""
1353
1354		if notes < 0:
1355			raise ValueError(f"notes must be at least 0, got {notes}")
1356		if position not in ("end", "start", "anywhere"):
1357			raise ValueError(f'position must be "end", "start", or "anywhere" — got {position!r}')
1358
1359		if rng is None:
1360			if seed is None:
1361				warnings.warn(
1362					"vary() without seed= is nondeterministic — pass seed= so the "
1363					"value survives live reload",
1364					stacklevel = 2,
1365				)
1366				rng = random.Random()
1367			else:
1368				rng = random.Random(seed)
1369
1370		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1371		count = min(notes, len(pitched_indices))
1372
1373		if count == 0:
1374			return self
1375
1376		if position == "end":
1377			chosen = pitched_indices[-count:]
1378		elif position == "start":
1379			chosen = pitched_indices[:count]
1380		else:
1381			chosen = sorted(rng.sample(pitched_indices, count))
1382
1383		events = list(self.events)
1384
1385		for index in chosen:
1386			if keep_contour:
1387				replacement = self._contour_safe_nudge(events, index, pitched_indices, rng)
1388				if replacement is not None:
1389					events[index] = dataclasses.replace(events[index], pitch = replacement)
1390			else:
1391				events[index] = dataclasses.replace(events[index], pitch = self._nudged_pitch(events[index].pitch, rng))
1392
1393		return Motif(events = tuple(events), length = self.length, controls = self.controls, fit = self.fit)

Replace a few pitches, preserving the rhythm — the smallest variation.

Rhythm, velocities, durations, rests, and control gestures are untouched; only the chosen notes' pitches move (by a small melodic nudge: scale steps for degrees, semitones for MIDI ints).

Arguments:
  • notes: How many pitched notes to vary (clamped to what exists).
  • position: Which notes — "end" (the tail, the default), "start", or "anywhere" (drawn from the stream).
  • seed: Seed for the variation. A standalone vary without a seed warns — module-level nondeterminism breaks live reload.
  • rng: An explicit random stream (overrides seed; used by recipe machinery).
  • keep_contour: When True, the variation preserves the line's CSEG — every varied note keeps its rank relations with every other note, so the melodic shape is identical (the motif-identity guard). Where no nudge can preserve the contour, that note stays unchanged — shape wins over motion.
Example:
answer = call.vary(notes=1, seed=4)     # same figure, new tail note
def answer( self, to: Union[int, Degree] = 1) -> Motif:
1463	def answer (self, to: typing.Union[int, Degree] = 1) -> "Motif":
1464
1465		"""Call → response: re-aim the tail to a stable degree.
1466
1467		The classic consequent move — the figure repeats but its last pitched
1468		note lands home (degree 1 by default; pass ``to=5`` for a half-close,
1469		or a full ``Degree`` for register control).  Everything else —
1470		rhythm, the other pitches, velocities, controls — is untouched.
1471
1472		Degree content only: absolute MIDI has no degrees to re-aim (build
1473		the call with ``motif([...])``), and drums raise.
1474		"""
1475
1476		target = to if isinstance(to, Degree) else Degree(int(to))
1477
1478		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1479
1480		if not pitched_indices:
1481			return self
1482
1483		last = self.events[pitched_indices[-1]]
1484
1485		if not isinstance(last.pitch, Degree):
1486			raise TypeError(
1487				f"answer() re-aims scale degrees — the tail is {type(last.pitch).__name__} "
1488				"content (build the call with motif([...]) for degree content)"
1489			)
1490
1491		if isinstance(to, int):
1492			# Keep the call's register: only the step is re-aimed.
1493			target = dataclasses.replace(last.pitch, step = int(to), chroma = 0)
1494
1495		events = list(self.events)
1496		events[pitched_indices[-1]] = dataclasses.replace(last, pitch = target)
1497
1498		return Motif(events = tuple(events), length = self.length, controls = self.controls, fit = self.fit)

Call → response: re-aim the tail to a stable degree.

The classic consequent move — the figure repeats but its last pitched note lands home (degree 1 by default; pass to=5 for a half-close, or a full Degree for register control). Everything else — rhythm, the other pitches, velocities, controls — is untouched.

Degree content only: absolute MIDI has no degrees to re-aim (build the call with motif([...])), and drums raise.

def pitched( self, spec: Union[int, str, Degree, ChordTone, Approach, NoneType]) -> Motif:
1500	def pitched (self, spec: PitchSpec) -> "Motif":
1501
1502		"""
1503		Replace every pitch with one spec — a kick rhythm becomes a bass line.
1504
1505		``"root"`` / ``"third"`` / ``"fifth"`` / ``"seventh"`` become chord
1506		tones; any other string is a drum name; ints are MIDI; Degree /
1507		ChordTone / Approach pass through.
1508		"""
1509
1510		if isinstance(spec, str) and spec in _CHORD_TONE_NAMES:
1511			spec = ChordTone(spec)
1512
1513		events = tuple(dataclasses.replace(e, pitch=spec) for e in self.events)
1514
1515		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)

Replace every pitch with one spec — a kick rhythm becomes a bass line.

"root" / "third" / "fifth" / "seventh" become chord tones; any other string is a drum name; ints are MIDI; Degree / ChordTone / Approach pass through.

def rhythm(self) -> Motif:
1517	def rhythm (self) -> "Motif":
1518
1519		"""
1520		Strip pitches (and control gestures): a reusable rhythmic skeleton.
1521
1522		Timing, velocities, durations, and probabilities survive; re-pitch
1523		with :meth:`pitched` before placement (placing a skeleton raises).
1524		"""
1525
1526		events = tuple(dataclasses.replace(e, pitch=None) for e in self.events)
1527
1528		return Motif(events=events, length=self.length)

Strip pitches (and control gestures): a reusable rhythmic skeleton.

Timing, velocities, durations, and probabilities survive; re-pitch with pitched() before placement (placing a skeleton raises).

def onsets(self) -> List[float]:
1530	def onsets (self) -> typing.List[float]:
1531
1532		"""The note onset beats, in order — ready for rhythm-first generation."""
1533
1534		return [e.beat for e in self.events]

The note onset beats, in order — ready for rhythm-first generation.

def transpose( self, steps: Optional[int] = None, semitones: Optional[int] = None) -> Motif:
1536	def transpose (self, steps: typing.Optional[int] = None, semitones: typing.Optional[int] = None) -> "Motif":
1537
1538		"""
1539		Transpose pitched content; the keyword names the unit.
1540
1541		``steps=`` moves scale degrees diatonically (the sequencing move) and
1542		raises on absolute-MIDI or drum content; ``semitones=`` is the
1543		literal chromatic form for MIDI ints and degrees.  Drum motifs raise
1544		on both — a transposed drum name is a different instrument, not a
1545		transposition.
1546		"""
1547
1548		if (steps is None) == (semitones is None):
1549			raise ValueError("transpose() takes exactly one of steps= or semitones=")
1550
1551		def move (pitch: PitchSpec) -> PitchSpec:
1552
1553			if pitch is None:
1554				return None
1555
1556			if isinstance(pitch, Approach):
1557				moved = move(pitch.target)
1558				if not isinstance(moved, (int, Degree, ChordTone)):
1559					raise TypeError(f"transpose cannot aim an Approach at {type(moved).__name__} content")
1560				return Approach(moved)
1561
1562			if steps is not None:
1563				if isinstance(pitch, Degree):
1564					return dataclasses.replace(pitch, step=pitch.step + steps)
1565				raise TypeError(
1566					f"transpose(steps=) moves scale degrees — {type(pitch).__name__} content "
1567					f"has no degrees (use semitones= for MIDI ints)"
1568				)
1569
1570			assert semitones is not None	# exactly one of steps/semitones is set (validated above)
1571
1572			if isinstance(pitch, int):
1573				return pitch + semitones
1574			if isinstance(pitch, Degree):
1575				return dataclasses.replace(pitch, chroma=pitch.chroma + semitones)
1576			raise TypeError(f"transpose(semitones=) cannot move {type(pitch).__name__} content")
1577
1578		events = tuple(dataclasses.replace(e, pitch=move(e.pitch)) for e in self.events)
1579
1580		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)

Transpose pitched content; the keyword names the unit.

steps= moves scale degrees diatonically (the sequencing move) and raises on absolute-MIDI or drum content; semitones= is the literal chromatic form for MIDI ints and degrees. Drum motifs raise on both — a transposed drum name is a different instrument, not a transposition.

def invert(self, pivot: Optional[int] = None) -> Motif:
1582	def invert (self, pivot: typing.Optional[int] = None) -> "Motif":
1583
1584		"""
1585		Mirror pitches around a pivot: MIDI content around a MIDI pivot,
1586		degree content around a degree pivot (default: the first note's pitch).
1587		Drum motifs raise.
1588		"""
1589
1590		pitched_events = [e for e in self.events if e.pitch is not None]
1591
1592		if not pitched_events:
1593			return self
1594
1595		first = pitched_events[0].pitch
1596
1597		if pivot is None:
1598			if isinstance(first, int):
1599				pivot = first
1600			elif isinstance(first, Degree):
1601				pivot = first.step
1602			else:
1603				raise TypeError(f"invert() cannot derive a pivot from {type(first).__name__} content")
1604
1605		def mirror (pitch: PitchSpec) -> PitchSpec:
1606
1607			if pitch is None:
1608				return None
1609			if isinstance(pitch, int):
1610				return 2 * pivot - pitch
1611			if isinstance(pitch, Degree):
1612				mirrored = 2 * pivot - pitch.step
1613				if mirrored < 1:
1614					raise ValueError(
1615						f"invert() around degree {pivot} sends degree {pitch.step} below the tonic — "
1616						f"raise the pivot or use Degree octaves"
1617					)
1618				# Reflection around the pivot (read at octave 0) is an isometry, so a
1619				# note's register flips too: a degree an octave above the pivot lands an
1620				# octave below it.  Negating octave needs no scale length and leaves
1621				# octave-0 content unchanged.
1622				return dataclasses.replace(pitch, step=mirrored, octave=-pitch.octave, chroma=-pitch.chroma)
1623			raise TypeError(f"invert() cannot mirror {type(pitch).__name__} content")
1624
1625		events = tuple(dataclasses.replace(e, pitch=mirror(e.pitch)) for e in self.events)
1626
1627		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)

Mirror pitches around a pivot: MIDI content around a MIDI pivot, degree content around a degree pivot (default: the first note's pitch). Drum motifs raise.

def describe(self) -> str:
1631	def describe (self) -> str:
1632
1633		"""A readable one-line summary: length, notes (pitch@beat), and control gestures."""
1634
1635		notes = ", ".join(f"{_pitch_label(e.pitch)}@{e.beat:g}" for e in self.events)
1636		parts = [f"Motif {self.length:g} beats", f"[{notes}]" if notes else "[no notes]"]
1637
1638		if self.controls:
1639			gestures = ", ".join(_control_label(c) for c in self.controls)
1640			parts.append(f"controls [{gestures}]")
1641
1642		return " ".join(parts)

A readable one-line summary: length, notes (pitch@beat), and control gestures.

@dataclasses.dataclass(frozen=True)
class Phrase:
1782@dataclasses.dataclass(frozen=True)
1783class Phrase:
1784
1785	"""
1786	A sequence of Motifs with segmentation preserved.
1787
1788	Segmentation is the unit of editing — it is what development and
1789	per-region regeneration operate on.  ``flatten()`` erases it into one
1790	long Motif.  Length is the sum of segment lengths.
1791
1792	A phrase made by :meth:`develop` carries its recipe, so
1793	:meth:`reroll` can regenerate a region; transforms and hand edits
1794	return recipe-less phrases (their notes no longer come from the
1795	recipe, so there is nothing honest to regenerate from).
1796	"""
1797
1798	segments: typing.Tuple[Motif, ...]
1799	recipe: typing.Optional[_PhraseRecipe]
1800
1801	def __init__ (self, segments: typing.Iterable[Motif], recipe: typing.Optional[_PhraseRecipe] = None) -> None:
1802
1803		"""Coerce any iterable of Motifs."""
1804
1805		segments = tuple(segments)
1806
1807		for segment in segments:
1808			if not isinstance(segment, Motif):
1809				raise TypeError(f"Phrase segments must be Motifs — got {type(segment).__name__}")
1810
1811		object.__setattr__(self, "segments", segments)
1812		object.__setattr__(self, "recipe", recipe)
1813
1814	@property
1815	def length (self) -> float:
1816
1817		"""Total length in beats (sum of segment lengths)."""
1818
1819		return sum(segment.length for segment in self.segments)
1820
1821	@classmethod
1822	def develop (
1823		cls,
1824		motif: Motif,
1825		bars: int = 8,
1826		plan: typing.Optional[typing.Union[typing.Sequence[str], str]] = None,
1827		seed: typing.Optional[int] = None,
1828		beats_per_bar: float = 4.0,
1829	) -> "Phrase":
1830
1831		"""Grow a motif into a phrase by a plan — the phrase generator.
1832
1833		``plan`` follows the standard form.  The literal form is a **list of
1834		unit labels** — ``plan=["a", "a", "a", "b"]``, equivalently
1835		``["a"] * 3 + ["b"]``: the first label is the given motif, each new
1836		label is a generated contrast unit (the source's rhythm, freshly
1837		re-pitched), a repeated label is a restatement, and *bars* spreads
1838		evenly across the units.  A bare string is a **recipe name** from
1839		the curated table — ``plan="call_response"`` (call, answer, call,
1840		varied answer) — reserved for plans whose semantics exceed a label
1841		skeleton.  A letter string is not a plan: a sequence of labels is a
1842		sequence, so it is a list.
1843
1844		The result carries its recipe, so :meth:`reroll` can regenerate a
1845		region later.
1846
1847		Parameters:
1848			motif: The source unit (its length must be ``bars / len(units)``
1849				bars — the plan's units tile the phrase exactly).
1850			bars: Phrase length in bars (must divide evenly by the unit
1851				count).
1852			plan: A list of unit labels, or a recipe name.
1853			seed: Seed for the generated units.  Without one, develop()
1854				warns — module-level nondeterminism breaks live reload.
1855			beats_per_bar: Bar size in beats (the value is context-free;
1856				4 is the common-time default).
1857
1858		Example:
1859			```python
1860			call = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
1861			lead = subsequence.Phrase.develop(call, bars=8, plan="call_response", seed=11)
1862			```
1863		"""
1864
1865		if plan is None:
1866			raise ValueError(
1867				'develop() needs a plan= — a list of unit labels (plan=["a", "a", "a", "b"]) '
1868				'or a recipe name (plan="call_response")'
1869			)
1870
1871		if seed is None:
1872			warnings.warn(
1873				"develop() without seed= is nondeterministic — pass seed= so the "
1874				"value survives live reload",
1875				stacklevel = 2,
1876			)
1877
1878		# How many units the plan asks for — known before any unit is built,
1879		# so a short motif can tile up to the unit size first.
1880		if isinstance(plan, str):
1881			if plan not in _PHRASE_RECIPES:
1882				known = ", ".join(sorted(_PHRASE_RECIPES))
1883				hint = ""
1884				if plan.isalpha() and plan == plan.lower() and len(set(plan)) < len(plan):
1885					spelled = ", ".join(repr(c) for c in plan)
1886					hint = f" A letter string is not a plan — a sequence of labels is a list: plan=[{spelled}]."
1887				raise ValueError(f"Unknown phrase recipe {plan!r}. Known recipes: {known}.{hint}")
1888			unit_count = _PHRASE_RECIPES[plan][0]
1889		else:
1890			labels = list(plan)
1891			if not labels or not all(isinstance(label, str) and label for label in labels):
1892				raise ValueError("plan labels must be non-empty strings, e.g. plan=['a', 'a', 'b']")
1893			unit_count = len(labels)
1894
1895		source = _tile_source(motif, bars, unit_count, beats_per_bar)
1896
1897		# An unseeded call draws a fresh salt so repeated calls genuinely
1898		# differ, as the warning above promises — interpolating None gave the
1899		# FIXED seed "None:..." and silently returned the same phrase every
1900		# time.
1901		salt = seed if seed is not None else random.randrange(2 ** 32)
1902
1903		if isinstance(plan, str):
1904			units = _PHRASE_RECIPES[plan][1](source, salt)
1905			stored_plan: typing.Union[typing.Tuple[str, ...], str] = plan
1906		else:
1907			generated: typing.Dict[str, Motif] = {labels[0]: source}
1908			for label in labels:
1909				if label not in generated:
1910					generated[label] = _contrast_unit(source, random.Random(f"{salt}:unit:{label}"))
1911			units = [generated[label] for label in labels]
1912			stored_plan = tuple(labels)
1913
1914		return cls(units, recipe = _PhraseRecipe(
1915			source = motif,
1916			plan = stored_plan,
1917			bars = bars,
1918			seed = seed,
1919			beats_per_bar = beats_per_bar,
1920		))
1921
1922	def reroll (
1923		self,
1924		bar: typing.Optional[int] = None,
1925		bars: typing.Optional[typing.Sequence[int]] = None,
1926		seed: typing.Optional[int] = None,
1927	) -> "Phrase":
1928
1929		"""Regenerate only the named bars — rhythm and boundary pitches kept.
1930
1931		Within each named bar, the first and last pitched notes stay (the
1932		boundary pins) and the interior pitches re-roll from a fresh per-bar
1933		stream salted by ``seed=`` (an unseeded call draws a fresh salt, so
1934		each call genuinely differs); onsets, durations, velocities, rests,
1935		drums, and control gestures are untouched.  Segmentation and the
1936		recipe survive, so rerolls compose.
1937
1938		Only a phrase that carries a recipe can reroll — a hand-written or
1939		transformed phrase raises loudly (its notes no longer come from a
1940		generator, so regenerating them would invent music).
1941
1942		Parameters:
1943			bar: A single 1-based bar to reroll.
1944			bars: A list of 1-based bars (the paired plural spelling).
1945			seed: Seed for the new pitches (salted per bar).  Without one,
1946				reroll() warns.
1947
1948		Example:
1949			```python
1950			lead = lead.reroll(bar=7, seed=4)    # only bar 7; rhythm + boundaries kept
1951			```
1952		"""
1953
1954		if self.recipe is None:
1955			raise ValueError(
1956				"this phrase carries no recipe (it was written by hand, or transformed "
1957				"since generation) — reroll() regenerates from a recipe; edit segments "
1958				"with replace(), or rebuild with Phrase.develop()"
1959			)
1960
1961		if (bar is None) == (bars is None):
1962			raise ValueError("reroll() takes exactly one of bar= (an int) or bars= (a list)")
1963
1964		region = [bar] if bar is not None else list(bars or [])
1965		beats_per_bar = self.recipe.beats_per_bar
1966		total_bars = int(round(self.length / beats_per_bar))
1967
1968		for number in region:
1969			if not isinstance(number, int) or isinstance(number, bool) or not 1 <= number <= total_bars:
1970				raise ValueError(f"bar {number!r} is outside this phrase (1–{total_bars})")
1971
1972		if seed is None:
1973			warnings.warn(
1974				"reroll() without seed= is nondeterministic — pass seed= so the "
1975				"value survives live reload",
1976				stacklevel = 2,
1977			)
1978
1979		# Unseeded rerolls draw a fresh salt — a fixed "None:..." seed would
1980		# "re-roll" to the identical pitches every time.
1981		salt = seed if seed is not None else random.randrange(2 ** 32)
1982
1983		windows = [
1984			((number - 1) * beats_per_bar, number * beats_per_bar, random.Random(f"{salt}:reroll:{number}"))
1985			for number in sorted(set(region))
1986		]
1987
1988		new_segments: typing.List[Motif] = []
1989		offset = 0.0
1990
1991		for segment in self.segments:
1992
1993			events = list(segment.events)
1994
1995			for window_start, window_end, rng in windows:
1996
1997				inside = [
1998					index for index, event in enumerate(events)
1999					if window_start <= offset + event.beat < window_end
2000					and event.pitch is not None and not isinstance(event.pitch, str)
2001				]
2002
2003				# Boundary pins: the first and last pitched notes of the bar
2004				# stay; only the interior re-rolls.
2005				for index in inside[1:-1]:
2006					events[index] = dataclasses.replace(
2007						events[index],
2008						pitch = segment._nudged_pitch(events[index].pitch, rng),
2009					)
2010
2011			new_segments.append(Motif(events = tuple(events), length = segment.length, controls = segment.controls))
2012			offset += segment.length
2013
2014		return Phrase(new_segments, recipe = self.recipe)
2015
2016	def flatten (self) -> Motif:
2017
2018		"""Erase segmentation: one long Motif (the monoid homomorphism onto ``then``)."""
2019
2020		return Motif.join(self.segments)
2021
2022	# ── algebra ─────────────────────────────────────────────────────────
2023
2024	def __add__ (self, other: typing.Any) -> "Phrase":
2025
2026		"""Append a Motif segment, or concatenate another Phrase's segments."""
2027
2028		if isinstance(other, Motif):
2029			return Phrase(self.segments + (other,))
2030		if isinstance(other, Phrase):
2031			return Phrase(self.segments + other.segments)
2032
2033		return NotImplemented
2034
2035	def __radd__ (self, other: typing.Any) -> "Phrase":
2036
2037		"""A Motif on the left prepends as a segment."""
2038
2039		if isinstance(other, Motif):
2040			return Phrase((other,) + self.segments)
2041
2042		return NotImplemented
2043
2044	def __mul__ (self, count: int) -> "Phrase":
2045
2046		"""Tile the segments *count* times."""
2047
2048		if not isinstance(count, int):
2049			return NotImplemented
2050		if count < 0:
2051			raise ValueError(f"Repetition count must be non-negative — got {count}")
2052
2053		return Phrase(self.segments * count)
2054
2055	__rmul__ = __mul__
2056
2057	def __and__ (self, other: typing.Any) -> Motif:
2058
2059		"""Parallel merge is vertical: Phrase operands flatten to Motif first."""
2060
2061		if isinstance(other, (Motif, Phrase)):
2062			return self.flatten().stack(other)
2063
2064		return NotImplemented
2065
2066	def stack (self, other: typing.Union[Motif, "Phrase"]) -> Motif:
2067
2068		"""The spelled form of ``&`` — flattens, then merges."""
2069
2070		return self.flatten().stack(other)
2071
2072	def slice (self, start: float, end: float) -> "Phrase":
2073
2074		"""A window; re-segments at the cut points (partial segments are sliced)."""
2075
2076		segments = []
2077		offset = 0.0
2078
2079		for segment in self.segments:
2080			seg_start, seg_end = offset, offset + segment.length
2081			lo, hi = max(start, seg_start), min(end, seg_end)
2082			if lo < hi:
2083				segments.append(segment.slice(lo - seg_start, hi - seg_start))
2084			offset = seg_end
2085
2086		return Phrase(segments)
2087
2088	def replace (self, position: int, motif: Motif) -> "Phrase":
2089
2090		"""Replace the segment at a 1-based position (musicians count from one)."""
2091
2092		if not 1 <= position <= len(self.segments):
2093			raise IndexError(f"Phrase has {len(self.segments)} segments — position {position} is out of range (1-based)")
2094
2095		segments = list(self.segments)
2096		segments[position - 1] = motif
2097
2098		return Phrase(segments)
2099
2100	# ── transforms: lifted segment-wise, except time-reordering ─────────
2101
2102	def reverse (self) -> "Phrase":
2103
2104		"""Reverse the whole timeline: segments reverse order AND each reverses internally."""
2105
2106		return Phrase(tuple(segment.reverse() for segment in reversed(self.segments)))
2107
2108	def rotate (self, beats: float) -> "Phrase":
2109
2110		"""Rotate the whole timeline modulo the total length, then re-segment at the original boundaries."""
2111
2112		flat = self.flatten().rotate(beats)
2113		segments = []
2114		offset = 0.0
2115
2116		# Re-segment by onset (events keep their full durations — a note may
2117		# ring past its new segment, exactly as it does on the flat timeline).
2118		for segment in self.segments:
2119			lo, hi = offset, offset + segment.length
2120			segments.append(Motif(
2121				events = tuple(
2122					dataclasses.replace(e, beat=e.beat - lo)
2123					for e in flat.events if lo <= e.beat < hi
2124				),
2125				length = segment.length,
2126				controls = tuple(
2127					dataclasses.replace(c, beat=c.beat - lo)
2128					for c in flat.controls if lo <= c.beat < hi
2129				),
2130			))
2131			offset = hi
2132
2133		return Phrase(segments)
2134
2135	def _lift (self, name: str, *args: typing.Any, **kwargs: typing.Any) -> "Phrase":
2136
2137		"""Apply a Motif transform to every segment."""
2138
2139		return Phrase(tuple(getattr(segment, name)(*args, **kwargs) for segment in self.segments))
2140
2141	def stretch (self, factor: float) -> "Phrase":
2142
2143		"""Scale time in every segment (lengths scale with them)."""
2144
2145		return self._lift("stretch", factor)
2146
2147	def quantize (self, grid: float) -> "Phrase":
2148
2149		"""Snap note onsets segment-wise."""
2150
2151		return self._lift("quantize", grid)
2152
2153	def with_velocity (self, velocity: typing.Union[int, typing.Tuple[int, int]]) -> "Phrase":
2154
2155		"""Replace every note's velocity, segment-wise."""
2156
2157		return self._lift("with_velocity", velocity)
2158
2159	def pitched (self, spec: PitchSpec) -> "Phrase":
2160
2161		"""Replace every pitch, segment-wise."""
2162
2163		return self._lift("pitched", spec)
2164
2165	def rhythm (self) -> "Phrase":
2166
2167		"""Strip pitches segment-wise: a phrase-shaped skeleton."""
2168
2169		return self._lift("rhythm")
2170
2171	def transpose (self, steps: typing.Optional[int] = None, semitones: typing.Optional[int] = None) -> "Phrase":
2172
2173		"""Transpose every segment (see :meth:`Motif.transpose`)."""
2174
2175		return self._lift("transpose", steps=steps, semitones=semitones)
2176
2177	def invert (self, pivot: typing.Optional[int] = None) -> "Phrase":
2178
2179		"""Mirror pitches in every segment around one pivot (see :meth:`Motif.invert`)."""
2180
2181		if pivot is None:
2182			for segment in self.segments:
2183				for event in segment.events:
2184					if event.pitch is not None:
2185						if isinstance(event.pitch, int):
2186							pivot = event.pitch
2187						elif isinstance(event.pitch, Degree):
2188							pivot = event.pitch.step
2189						break
2190				if pivot is not None:
2191					break
2192
2193		return self._lift("invert", pivot=pivot)
2194
2195	def describe (self) -> str:
2196
2197		"""A readable summary: total length and each segment on its own line."""
2198
2199		header = f"Phrase {self.length:g} beats, {len(self.segments)} segments"
2200		lines = [f"  {i + 1}. {segment.describe()}" for i, segment in enumerate(self.segments)]
2201
2202		return "\n".join([header] + lines)
2203
2204	def __str__ (self) -> str:
2205
2206		"""Printable form (same as :meth:`describe`)."""
2207
2208		return self.describe()

A sequence of Motifs with segmentation preserved.

Segmentation is the unit of editing — it is what development and per-region regeneration operate on. flatten() erases it into one long Motif. Length is the sum of segment lengths.

A phrase made by develop() carries its recipe, so reroll() can regenerate a region; transforms and hand edits return recipe-less phrases (their notes no longer come from the recipe, so there is nothing honest to regenerate from).

Phrase( segments: Iterable[Motif], recipe: Optional[subsequence.motifs._PhraseRecipe] = None)
1801	def __init__ (self, segments: typing.Iterable[Motif], recipe: typing.Optional[_PhraseRecipe] = None) -> None:
1802
1803		"""Coerce any iterable of Motifs."""
1804
1805		segments = tuple(segments)
1806
1807		for segment in segments:
1808			if not isinstance(segment, Motif):
1809				raise TypeError(f"Phrase segments must be Motifs — got {type(segment).__name__}")
1810
1811		object.__setattr__(self, "segments", segments)
1812		object.__setattr__(self, "recipe", recipe)

Coerce any iterable of Motifs.

segments: Tuple[Motif, ...]
recipe: Optional[subsequence.motifs._PhraseRecipe]
length: float
1814	@property
1815	def length (self) -> float:
1816
1817		"""Total length in beats (sum of segment lengths)."""
1818
1819		return sum(segment.length for segment in self.segments)

Total length in beats (sum of segment lengths).

@classmethod
def develop( cls, motif: Motif, bars: int = 8, plan: Union[Sequence[str], str, NoneType] = None, seed: Optional[int] = None, beats_per_bar: float = 4.0) -> Phrase:
1821	@classmethod
1822	def develop (
1823		cls,
1824		motif: Motif,
1825		bars: int = 8,
1826		plan: typing.Optional[typing.Union[typing.Sequence[str], str]] = None,
1827		seed: typing.Optional[int] = None,
1828		beats_per_bar: float = 4.0,
1829	) -> "Phrase":
1830
1831		"""Grow a motif into a phrase by a plan — the phrase generator.
1832
1833		``plan`` follows the standard form.  The literal form is a **list of
1834		unit labels** — ``plan=["a", "a", "a", "b"]``, equivalently
1835		``["a"] * 3 + ["b"]``: the first label is the given motif, each new
1836		label is a generated contrast unit (the source's rhythm, freshly
1837		re-pitched), a repeated label is a restatement, and *bars* spreads
1838		evenly across the units.  A bare string is a **recipe name** from
1839		the curated table — ``plan="call_response"`` (call, answer, call,
1840		varied answer) — reserved for plans whose semantics exceed a label
1841		skeleton.  A letter string is not a plan: a sequence of labels is a
1842		sequence, so it is a list.
1843
1844		The result carries its recipe, so :meth:`reroll` can regenerate a
1845		region later.
1846
1847		Parameters:
1848			motif: The source unit (its length must be ``bars / len(units)``
1849				bars — the plan's units tile the phrase exactly).
1850			bars: Phrase length in bars (must divide evenly by the unit
1851				count).
1852			plan: A list of unit labels, or a recipe name.
1853			seed: Seed for the generated units.  Without one, develop()
1854				warns — module-level nondeterminism breaks live reload.
1855			beats_per_bar: Bar size in beats (the value is context-free;
1856				4 is the common-time default).
1857
1858		Example:
1859			```python
1860			call = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
1861			lead = subsequence.Phrase.develop(call, bars=8, plan="call_response", seed=11)
1862			```
1863		"""
1864
1865		if plan is None:
1866			raise ValueError(
1867				'develop() needs a plan= — a list of unit labels (plan=["a", "a", "a", "b"]) '
1868				'or a recipe name (plan="call_response")'
1869			)
1870
1871		if seed is None:
1872			warnings.warn(
1873				"develop() without seed= is nondeterministic — pass seed= so the "
1874				"value survives live reload",
1875				stacklevel = 2,
1876			)
1877
1878		# How many units the plan asks for — known before any unit is built,
1879		# so a short motif can tile up to the unit size first.
1880		if isinstance(plan, str):
1881			if plan not in _PHRASE_RECIPES:
1882				known = ", ".join(sorted(_PHRASE_RECIPES))
1883				hint = ""
1884				if plan.isalpha() and plan == plan.lower() and len(set(plan)) < len(plan):
1885					spelled = ", ".join(repr(c) for c in plan)
1886					hint = f" A letter string is not a plan — a sequence of labels is a list: plan=[{spelled}]."
1887				raise ValueError(f"Unknown phrase recipe {plan!r}. Known recipes: {known}.{hint}")
1888			unit_count = _PHRASE_RECIPES[plan][0]
1889		else:
1890			labels = list(plan)
1891			if not labels or not all(isinstance(label, str) and label for label in labels):
1892				raise ValueError("plan labels must be non-empty strings, e.g. plan=['a', 'a', 'b']")
1893			unit_count = len(labels)
1894
1895		source = _tile_source(motif, bars, unit_count, beats_per_bar)
1896
1897		# An unseeded call draws a fresh salt so repeated calls genuinely
1898		# differ, as the warning above promises — interpolating None gave the
1899		# FIXED seed "None:..." and silently returned the same phrase every
1900		# time.
1901		salt = seed if seed is not None else random.randrange(2 ** 32)
1902
1903		if isinstance(plan, str):
1904			units = _PHRASE_RECIPES[plan][1](source, salt)
1905			stored_plan: typing.Union[typing.Tuple[str, ...], str] = plan
1906		else:
1907			generated: typing.Dict[str, Motif] = {labels[0]: source}
1908			for label in labels:
1909				if label not in generated:
1910					generated[label] = _contrast_unit(source, random.Random(f"{salt}:unit:{label}"))
1911			units = [generated[label] for label in labels]
1912			stored_plan = tuple(labels)
1913
1914		return cls(units, recipe = _PhraseRecipe(
1915			source = motif,
1916			plan = stored_plan,
1917			bars = bars,
1918			seed = seed,
1919			beats_per_bar = beats_per_bar,
1920		))

Grow a motif into a phrase by a plan — the phrase generator.

plan follows the standard form. The literal form is a list of unit labelsplan=["a", "a", "a", "b"], equivalently ["a"] * 3 + ["b"]: the first label is the given motif, each new label is a generated contrast unit (the source's rhythm, freshly re-pitched), a repeated label is a restatement, and bars spreads evenly across the units. A bare string is a recipe name from the curated table — plan="call_response" (call, answer, call, varied answer) — reserved for plans whose semantics exceed a label skeleton. A letter string is not a plan: a sequence of labels is a sequence, so it is a list.

The result carries its recipe, so reroll() can regenerate a region later.

Arguments:
  • motif: The source unit (its length must be bars / len(units) bars — the plan's units tile the phrase exactly).
  • bars: Phrase length in bars (must divide evenly by the unit count).
  • plan: A list of unit labels, or a recipe name.
  • seed: Seed for the generated units. Without one, develop() warns — module-level nondeterminism breaks live reload.
  • beats_per_bar: Bar size in beats (the value is context-free; 4 is the common-time default).
Example:
call = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
lead = subsequence.Phrase.develop(call, bars=8, plan="call_response", seed=11)
def reroll( self, bar: Optional[int] = None, bars: Optional[Sequence[int]] = None, seed: Optional[int] = None) -> Phrase:
1922	def reroll (
1923		self,
1924		bar: typing.Optional[int] = None,
1925		bars: typing.Optional[typing.Sequence[int]] = None,
1926		seed: typing.Optional[int] = None,
1927	) -> "Phrase":
1928
1929		"""Regenerate only the named bars — rhythm and boundary pitches kept.
1930
1931		Within each named bar, the first and last pitched notes stay (the
1932		boundary pins) and the interior pitches re-roll from a fresh per-bar
1933		stream salted by ``seed=`` (an unseeded call draws a fresh salt, so
1934		each call genuinely differs); onsets, durations, velocities, rests,
1935		drums, and control gestures are untouched.  Segmentation and the
1936		recipe survive, so rerolls compose.
1937
1938		Only a phrase that carries a recipe can reroll — a hand-written or
1939		transformed phrase raises loudly (its notes no longer come from a
1940		generator, so regenerating them would invent music).
1941
1942		Parameters:
1943			bar: A single 1-based bar to reroll.
1944			bars: A list of 1-based bars (the paired plural spelling).
1945			seed: Seed for the new pitches (salted per bar).  Without one,
1946				reroll() warns.
1947
1948		Example:
1949			```python
1950			lead = lead.reroll(bar=7, seed=4)    # only bar 7; rhythm + boundaries kept
1951			```
1952		"""
1953
1954		if self.recipe is None:
1955			raise ValueError(
1956				"this phrase carries no recipe (it was written by hand, or transformed "
1957				"since generation) — reroll() regenerates from a recipe; edit segments "
1958				"with replace(), or rebuild with Phrase.develop()"
1959			)
1960
1961		if (bar is None) == (bars is None):
1962			raise ValueError("reroll() takes exactly one of bar= (an int) or bars= (a list)")
1963
1964		region = [bar] if bar is not None else list(bars or [])
1965		beats_per_bar = self.recipe.beats_per_bar
1966		total_bars = int(round(self.length / beats_per_bar))
1967
1968		for number in region:
1969			if not isinstance(number, int) or isinstance(number, bool) or not 1 <= number <= total_bars:
1970				raise ValueError(f"bar {number!r} is outside this phrase (1–{total_bars})")
1971
1972		if seed is None:
1973			warnings.warn(
1974				"reroll() without seed= is nondeterministic — pass seed= so the "
1975				"value survives live reload",
1976				stacklevel = 2,
1977			)
1978
1979		# Unseeded rerolls draw a fresh salt — a fixed "None:..." seed would
1980		# "re-roll" to the identical pitches every time.
1981		salt = seed if seed is not None else random.randrange(2 ** 32)
1982
1983		windows = [
1984			((number - 1) * beats_per_bar, number * beats_per_bar, random.Random(f"{salt}:reroll:{number}"))
1985			for number in sorted(set(region))
1986		]
1987
1988		new_segments: typing.List[Motif] = []
1989		offset = 0.0
1990
1991		for segment in self.segments:
1992
1993			events = list(segment.events)
1994
1995			for window_start, window_end, rng in windows:
1996
1997				inside = [
1998					index for index, event in enumerate(events)
1999					if window_start <= offset + event.beat < window_end
2000					and event.pitch is not None and not isinstance(event.pitch, str)
2001				]
2002
2003				# Boundary pins: the first and last pitched notes of the bar
2004				# stay; only the interior re-rolls.
2005				for index in inside[1:-1]:
2006					events[index] = dataclasses.replace(
2007						events[index],
2008						pitch = segment._nudged_pitch(events[index].pitch, rng),
2009					)
2010
2011			new_segments.append(Motif(events = tuple(events), length = segment.length, controls = segment.controls))
2012			offset += segment.length
2013
2014		return Phrase(new_segments, recipe = self.recipe)

Regenerate only the named bars — rhythm and boundary pitches kept.

Within each named bar, the first and last pitched notes stay (the boundary pins) and the interior pitches re-roll from a fresh per-bar stream salted by seed= (an unseeded call draws a fresh salt, so each call genuinely differs); onsets, durations, velocities, rests, drums, and control gestures are untouched. Segmentation and the recipe survive, so rerolls compose.

Only a phrase that carries a recipe can reroll — a hand-written or transformed phrase raises loudly (its notes no longer come from a generator, so regenerating them would invent music).

Arguments:
  • bar: A single 1-based bar to reroll.
  • bars: A list of 1-based bars (the paired plural spelling).
  • seed: Seed for the new pitches (salted per bar). Without one, reroll() warns.
Example:
lead = lead.reroll(bar=7, seed=4)    # only bar 7; rhythm + boundaries kept
def flatten(self) -> Motif:
2016	def flatten (self) -> Motif:
2017
2018		"""Erase segmentation: one long Motif (the monoid homomorphism onto ``then``)."""
2019
2020		return Motif.join(self.segments)

Erase segmentation: one long Motif (the monoid homomorphism onto then).

def stack( self, other: Union[Motif, Phrase]) -> Motif:
2066	def stack (self, other: typing.Union[Motif, "Phrase"]) -> Motif:
2067
2068		"""The spelled form of ``&`` — flattens, then merges."""
2069
2070		return self.flatten().stack(other)

The spelled form of & — flattens, then merges.

def slice(self, start: float, end: float) -> Phrase:
2072	def slice (self, start: float, end: float) -> "Phrase":
2073
2074		"""A window; re-segments at the cut points (partial segments are sliced)."""
2075
2076		segments = []
2077		offset = 0.0
2078
2079		for segment in self.segments:
2080			seg_start, seg_end = offset, offset + segment.length
2081			lo, hi = max(start, seg_start), min(end, seg_end)
2082			if lo < hi:
2083				segments.append(segment.slice(lo - seg_start, hi - seg_start))
2084			offset = seg_end
2085
2086		return Phrase(segments)

A window; re-segments at the cut points (partial segments are sliced).

def replace( self, position: int, motif: Motif) -> Phrase:
2088	def replace (self, position: int, motif: Motif) -> "Phrase":
2089
2090		"""Replace the segment at a 1-based position (musicians count from one)."""
2091
2092		if not 1 <= position <= len(self.segments):
2093			raise IndexError(f"Phrase has {len(self.segments)} segments — position {position} is out of range (1-based)")
2094
2095		segments = list(self.segments)
2096		segments[position - 1] = motif
2097
2098		return Phrase(segments)

Replace the segment at a 1-based position (musicians count from one).

def reverse(self) -> Phrase:
2102	def reverse (self) -> "Phrase":
2103
2104		"""Reverse the whole timeline: segments reverse order AND each reverses internally."""
2105
2106		return Phrase(tuple(segment.reverse() for segment in reversed(self.segments)))

Reverse the whole timeline: segments reverse order AND each reverses internally.

def rotate(self, beats: float) -> Phrase:
2108	def rotate (self, beats: float) -> "Phrase":
2109
2110		"""Rotate the whole timeline modulo the total length, then re-segment at the original boundaries."""
2111
2112		flat = self.flatten().rotate(beats)
2113		segments = []
2114		offset = 0.0
2115
2116		# Re-segment by onset (events keep their full durations — a note may
2117		# ring past its new segment, exactly as it does on the flat timeline).
2118		for segment in self.segments:
2119			lo, hi = offset, offset + segment.length
2120			segments.append(Motif(
2121				events = tuple(
2122					dataclasses.replace(e, beat=e.beat - lo)
2123					for e in flat.events if lo <= e.beat < hi
2124				),
2125				length = segment.length,
2126				controls = tuple(
2127					dataclasses.replace(c, beat=c.beat - lo)
2128					for c in flat.controls if lo <= c.beat < hi
2129				),
2130			))
2131			offset = hi
2132
2133		return Phrase(segments)

Rotate the whole timeline modulo the total length, then re-segment at the original boundaries.

def stretch(self, factor: float) -> Phrase:
2141	def stretch (self, factor: float) -> "Phrase":
2142
2143		"""Scale time in every segment (lengths scale with them)."""
2144
2145		return self._lift("stretch", factor)

Scale time in every segment (lengths scale with them).

def quantize(self, grid: float) -> Phrase:
2147	def quantize (self, grid: float) -> "Phrase":
2148
2149		"""Snap note onsets segment-wise."""
2150
2151		return self._lift("quantize", grid)

Snap note onsets segment-wise.

def with_velocity(self, velocity: Union[int, Tuple[int, int]]) -> Phrase:
2153	def with_velocity (self, velocity: typing.Union[int, typing.Tuple[int, int]]) -> "Phrase":
2154
2155		"""Replace every note's velocity, segment-wise."""
2156
2157		return self._lift("with_velocity", velocity)

Replace every note's velocity, segment-wise.

def pitched( self, spec: Union[int, str, Degree, ChordTone, Approach, NoneType]) -> Phrase:
2159	def pitched (self, spec: PitchSpec) -> "Phrase":
2160
2161		"""Replace every pitch, segment-wise."""
2162
2163		return self._lift("pitched", spec)

Replace every pitch, segment-wise.

def rhythm(self) -> Phrase:
2165	def rhythm (self) -> "Phrase":
2166
2167		"""Strip pitches segment-wise: a phrase-shaped skeleton."""
2168
2169		return self._lift("rhythm")

Strip pitches segment-wise: a phrase-shaped skeleton.

def transpose( self, steps: Optional[int] = None, semitones: Optional[int] = None) -> Phrase:
2171	def transpose (self, steps: typing.Optional[int] = None, semitones: typing.Optional[int] = None) -> "Phrase":
2172
2173		"""Transpose every segment (see :meth:`Motif.transpose`)."""
2174
2175		return self._lift("transpose", steps=steps, semitones=semitones)

Transpose every segment (see Motif.transpose()).

def invert(self, pivot: Optional[int] = None) -> Phrase:
2177	def invert (self, pivot: typing.Optional[int] = None) -> "Phrase":
2178
2179		"""Mirror pitches in every segment around one pivot (see :meth:`Motif.invert`)."""
2180
2181		if pivot is None:
2182			for segment in self.segments:
2183				for event in segment.events:
2184					if event.pitch is not None:
2185						if isinstance(event.pitch, int):
2186							pivot = event.pitch
2187						elif isinstance(event.pitch, Degree):
2188							pivot = event.pitch.step
2189						break
2190				if pivot is not None:
2191					break
2192
2193		return self._lift("invert", pivot=pivot)

Mirror pitches in every segment around one pivot (see Motif.invert()).

def describe(self) -> str:
2195	def describe (self) -> str:
2196
2197		"""A readable summary: total length and each segment on its own line."""
2198
2199		header = f"Phrase {self.length:g} beats, {len(self.segments)} segments"
2200		lines = [f"  {i + 1}. {segment.describe()}" for i, segment in enumerate(self.segments)]
2201
2202		return "\n".join([header] + lines)

A readable summary: total length and each segment on its own line.

def motif( degrees: List[Union[int, Degree, NoneType]], beats: Optional[List[float]] = None, velocities: Any = 100, durations: Any = 1.0, probabilities: Any = 1.0, length: Optional[float] = None) -> Motif:
2211def motif (
2212	degrees: typing.List[typing.Union[int, Degree, None]],
2213	beats: typing.Optional[typing.List[float]] = None,
2214	velocities: typing.Any = _DEFAULT_VELOCITY,
2215	durations: typing.Any = 1.0,
2216	probabilities: typing.Any = 1.0,
2217	length: typing.Optional[float] = None,
2218) -> Motif:
2219
2220	"""
2221	The lowercase shortcut: a melody as 1-based scale degrees.
2222
2223	``subsequence.motif([5, 6, 5, 3])`` is ``Motif.degrees([5, 6, 5, 3])`` —
2224	relative pitch is the primary form.  For absolute MIDI note numbers use
2225	``Motif.notes([64, 65, 64, 60])``; implausibly large ints here raise so
2226	a pasted MIDI list fails loud instead of squealing octaves up.
2227	"""
2228
2229	return Motif.degrees(
2230		degrees,
2231		beats = beats,
2232		velocities = velocities,
2233		durations = durations,
2234		probabilities = probabilities,
2235		length = length,
2236	)

The lowercase shortcut: a melody as 1-based scale degrees.

subsequence.motif([5, 6, 5, 3]) is Motif.degrees([5, 6, 5, 3]) — relative pitch is the primary form. For absolute MIDI note numbers use Motif.notes([64, 65, 64, 60]); implausibly large ints here raise so a pasted MIDI list fails loud instead of squealing octaves up.

def sentence( motif: Motif, bars: int = 8, cadence: str = 'strong', seed: Optional[int] = None, beats_per_bar: float = 4.0) -> Phrase:
2239def sentence (
2240	motif: Motif,
2241	bars: int = 8,
2242	cadence: str = "strong",
2243	seed: typing.Optional[int] = None,
2244	beats_per_bar: float = 4.0,
2245) -> Phrase:
2246
2247	"""The classical sentence, as a thin combinator — idea, idea, drive, close.
2248
2249	Four units: the basic idea stated twice (the presentation), a generated
2250	contrast unit (the continuation — the source's rhythm, freshly
2251	re-pitched), and a second contrast unit whose tail lands on the
2252	cadence's close degree (the cadential close).  An 8-bar sentence from a
2253	2-bar idea is the textbook proportion; a shorter idea tiles up to the
2254	unit size first.
2255
2256	The melodic side of a cadence only — pair it with the harmonic side
2257	(``prog.cadence()``, ``Progression.generate(cadence=)``, or
2258	``request_cadence()``) and the two arrive together.
2259
2260	Parameters:
2261		motif: The basic idea (degree content — the close re-aims a degree).
2262		bars: Sentence length (must divide evenly across the 4 units).
2263		cadence: The close — ``"strong"`` lands on 1, ``"open"`` on 5,
2264			``"soft"``/``"fakeout"`` on 1 (theory aliases accepted).
2265		seed: Seed for the generated continuation units (seed-or-warn).
2266		beats_per_bar: Bar size in beats (context-free; 4 is the default).
2267
2268	Example:
2269		```python
2270		idea = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
2271		verse_lead = subsequence.sentence(idea, bars=8, cadence="open", seed=11)
2272		```
2273	"""
2274
2275	spec = subsequence.cadences.cadence_formula(cadence)
2276
2277	if seed is None:
2278		warnings.warn(
2279			"sentence() without seed= is nondeterministic — pass seed= so the "
2280			"value survives live reload",
2281			stacklevel = 2,
2282		)
2283
2284	source = _tile_source(motif, bars, 4, beats_per_bar)
2285
2286	# Unseeded calls draw a fresh salt (a fixed "None:..." seed would return
2287	# the same sentence every time, belying the warning above).
2288	salt = seed if seed is not None else random.randrange(2 ** 32)
2289
2290	continuation = _contrast_unit(source, random.Random(f"{salt}:sentence:continuation"))
2291	cadential = _contrast_unit(source, random.Random(f"{salt}:sentence:cadential")).answer(to = spec.close_degree)
2292
2293	return Phrase([source, source, continuation, cadential], recipe = _PhraseRecipe(
2294		source = motif,
2295		plan = "sentence",
2296		bars = bars,
2297		seed = seed,
2298		beats_per_bar = beats_per_bar,
2299		cadence = spec.name,
2300	))

The classical sentence, as a thin combinator — idea, idea, drive, close.

Four units: the basic idea stated twice (the presentation), a generated contrast unit (the continuation — the source's rhythm, freshly re-pitched), and a second contrast unit whose tail lands on the cadence's close degree (the cadential close). An 8-bar sentence from a 2-bar idea is the textbook proportion; a shorter idea tiles up to the unit size first.

The melodic side of a cadence only — pair it with the harmonic side (prog.cadence(), Progression.generate(cadence=), or request_cadence()) and the two arrive together.

Arguments:
  • motif: The basic idea (degree content — the close re-aims a degree).
  • bars: Sentence length (must divide evenly across the 4 units).
  • cadence: The close — "strong" lands on 1, "open" on 5, "soft"/"fakeout" on 1 (theory aliases accepted).
  • seed: Seed for the generated continuation units (seed-or-warn).
  • beats_per_bar: Bar size in beats (context-free; 4 is the default).
Example:
idea = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
verse_lead = subsequence.sentence(idea, bars=8, cadence="open", seed=11)
def period( antecedent: Union[Motif, Phrase], cadence: str = 'strong', beats_per_bar: float = 4.0) -> Phrase:
2303def period (
2304	antecedent: typing.Union[Motif, Phrase],
2305	cadence: str = "strong",
2306	beats_per_bar: float = 4.0,
2307) -> Phrase:
2308
2309	"""The classical period, as a thin combinator — question, then answer.
2310
2311	Two halves: the antecedent with its tail re-aimed to the open half-close
2312	(degree 5 — the question), then the same material restated with its tail
2313	on the cadence's close degree (the answer).  The two halves differ
2314	exactly at their closes — the open/closed contrast *is* the period.
2315
2316	Deterministic: no notes are generated, only the two tail notes re-aim
2317	(so there is no seed).  Vary the consequent yourself for a looser
2318	restatement: ``period(a).reroll(bar=7, seed=4)``.
2319
2320	Parameters:
2321		antecedent: The first half — a Motif, or a Phrase whose segmentation
2322			is kept (only its last segment's tail re-aims).
2323		cadence: The consequent's close — ``"strong"`` lands on 1 (theory
2324			aliases accepted).
2325		beats_per_bar: Bar size in beats, recorded for ``reroll()`` windows.
2326
2327	Example:
2328		```python
2329		idea = subsequence.motif([3, 4, 5, 1, None, 6, 5, 4], length=8)
2330		lead = subsequence.period(idea)        # 16 beats: half-close, then home
2331		```
2332	"""
2333
2334	spec = subsequence.cadences.cadence_formula(cadence)
2335	open_degree = subsequence.cadences.cadence_formula("open").close_degree
2336
2337	units = list(antecedent.segments) if isinstance(antecedent, Phrase) else [antecedent]
2338
2339	if not units or sum(unit.length for unit in units) <= 0:
2340		raise ValueError("cannot build a period from an empty antecedent")
2341
2342	tail = units[-1]
2343
2344	antecedent_units = units[:-1] + [tail.answer(to = open_degree)]
2345	consequent_units = units[:-1] + [tail.answer(to = spec.close_degree)]
2346
2347	source = antecedent.flatten() if isinstance(antecedent, Phrase) else antecedent
2348	total_beats = 2 * sum(unit.length for unit in units)
2349
2350	return Phrase(antecedent_units + consequent_units, recipe = _PhraseRecipe(
2351		source = source,
2352		plan = "period",
2353		bars = int(round(total_beats / beats_per_bar)),
2354		seed = None,
2355		beats_per_bar = beats_per_bar,
2356		cadence = spec.name,
2357	))

The classical period, as a thin combinator — question, then answer.

Two halves: the antecedent with its tail re-aimed to the open half-close (degree 5 — the question), then the same material restated with its tail on the cadence's close degree (the answer). The two halves differ exactly at their closes — the open/closed contrast is the period.

Deterministic: no notes are generated, only the two tail notes re-aim (so there is no seed). Vary the consequent yourself for a looser restatement: period(a).reroll(bar=7, seed=4).

Arguments:
  • antecedent: The first half — a Motif, or a Phrase whose segmentation is kept (only its last segment's tail re-aims).
  • cadence: The consequent's close — "strong" lands on 1 (theory aliases accepted).
  • beats_per_bar: Bar size in beats, recorded for reroll() windows.
Example:
idea = subsequence.motif([3, 4, 5, 1, None, 6, 5, 4], length=8)
lead = subsequence.period(idea)        # 16 beats: half-close, then home
@dataclasses.dataclass(frozen=True)
class Cadence:
30@dataclasses.dataclass(frozen=True)
31class Cadence:
32
33	"""One cadence formula — a named tail plus its melodic close.
34
35	Attributes:
36		name: The producer name (the primary key in the table).
37		theory_name: The traditional name, for the curious.
38		formula: The chord tail, in progression-element grammar, ending on
39			the arrival chord.
40		close_degree: The scale degree a melody lands on at this cadence
41			(1 for full closes; 5 for the open half — and 1 for the
42			fakeout too: the melody resolves as promised while the
43			harmony swerves, which is the trick of it).
44	"""
45
46	name: str
47	theory_name: str
48	formula: typing.Tuple[typing.Any, ...]
49	close_degree: int

One cadence formula — a named tail plus its melodic close.

Attributes:
  • name: The producer name (the primary key in the table).
  • theory_name: The traditional name, for the curious.
  • formula: The chord tail, in progression-element grammar, ending on the arrival chord.
  • close_degree: The scale degree a melody lands on at this cadence (1 for full closes; 5 for the open half — and 1 for the fakeout too: the melody resolves as promised while the harmony swerves, which is the trick of it).
Cadence( name: str, theory_name: str, formula: Tuple[Any, ...], close_degree: int)
name: str
theory_name: str
formula: Tuple[Any, ...]
close_degree: int
@dataclasses.dataclass(frozen=True)
class Section:
22@dataclasses.dataclass(frozen=True)
23class Section:
24
25	"""One section of a form — the payload home.
26
27	Attributes:
28		name: The section name (``"verse"``).
29		bars: Length in bars (≥ 1).
30		energy: The section's energy level (0.0–1.0; the arranging dial).
31			Read by ``p.energy`` and ``min_energy=`` gating; a
32			``composition.energy()`` dict overrides it (the dict is the
33			later, performance-level dial).
34		key: Optional key override — re-anchors *key-relative* content
35			(degrees, romans, generated material, and key-relative section
36			progressions bound with ``section_chords``) to this section's
37			tonic.  *Absolute* content (note names, MIDI pitches, frozen
38			chords) is never moved, and *chord-relative* content
39			(``ChordTone``, ``Approach``) tracks the sounding chord rather
40			than the key — see the three-intent model in the docs.  The
41			live graph engine (``harmony(style=...)``) stays in the
42			composition key by design (a stateful walk does not transpose
43			mid-stream).
44		scale: Optional scale/mode override (e.g. ``"minor"``) — moves the
45			mode as well as the tonic, so a section can genuinely change
46			to the relative or parallel minor.  Falls back to the form's
47			scale, then the composition's.
48	"""
49
50	name: str
51	bars: int
52	energy: float = 0.5
53	key: typing.Optional[str] = None
54	scale: typing.Optional[str] = None
55
56	def __post_init__ (self) -> None:
57
58		"""Validate the payload loudly."""
59
60		if not isinstance(self.name, str) or not self.name:
61			raise ValueError(f"a section needs a non-empty string name, got {self.name!r}")
62
63		if not isinstance(self.bars, int) or isinstance(self.bars, bool) or self.bars < 1:
64			raise ValueError(f"Section {self.name!r} must last at least 1 bar, got {self.bars!r}")
65
66		if not 0.0 <= float(self.energy) <= 1.0:
67			raise ValueError(f"Section {self.name!r} energy must be 0.0–1.0, got {self.energy!r}")

One section of a form — the payload home.

Attributes:
  • name: The section name ("verse").
  • bars: Length in bars (≥ 1).
  • energy: The section's energy level (0.0–1.0; the arranging dial). Read by p.energy and min_energy= gating; a composition.energy() dict overrides it (the dict is the later, performance-level dial).
  • key: Optional key override — re-anchors key-relative content (degrees, romans, generated material, and key-relative section progressions bound with section_chords) to this section's tonic. Absolute content (note names, MIDI pitches, frozen chords) is never moved, and chord-relative content (ChordTone, Approach) tracks the sounding chord rather than the key — see the three-intent model in the docs. The live graph engine (harmony(style=...)) stays in the composition key by design (a stateful walk does not transpose mid-stream).
  • scale: Optional scale/mode override (e.g. "minor") — moves the mode as well as the tonic, so a section can genuinely change to the relative or parallel minor. Falls back to the form's scale, then the composition's.
Section( name: str, bars: int, energy: float = 0.5, key: Optional[str] = None, scale: Optional[str] = None)
name: str
bars: int
energy: float = 0.5
key: Optional[str] = None
scale: Optional[str] = None
@dataclasses.dataclass(frozen=True)
class Form:
 86@dataclasses.dataclass(frozen=True)
 87class Form:
 88
 89	"""A frozen sequence of Sections — the editable, bindable form value.
 90
 91	List-friendly: the constructor coerces ``("name", bars)`` tuples, so
 92	``Form([("verse", 8), ("chorus", 8)])`` and
 93	``Form([Section("verse", 8), Section("chorus", 8)])`` are the same value.
 94	Repetition is Python list arithmetic before construction.
 95
 96	A form may carry its own ``key``/``scale`` — the **form tier** of the
 97	key-source chain (``Section.key`` overrides it; it overrides the
 98	composition key).  A whole AABA in one key with one section borrowing
 99	another is ``Form([...], key="A")`` plus a ``Section(..., key="F")``.
100	"""
101
102	sections: typing.Tuple[Section, ...]
103	key: typing.Optional[str] = None
104	scale: typing.Optional[str] = None
105
106	def __init__ (
107		self,
108		sections: typing.Iterable[typing.Any],
109		key: typing.Optional[str] = None,
110		scale: typing.Optional[str] = None,
111	) -> None:
112
113		"""Coerce any iterable of Sections / (name, bars) tuples."""
114
115		coerced = tuple(_coerce_section(element) for element in sections)
116
117		if not coerced:
118			raise ValueError("a Form needs at least one section")
119
120		object.__setattr__(self, "sections", coerced)
121		object.__setattr__(self, "key", key)
122		object.__setattr__(self, "scale", scale)
123
124	@property
125	def bars (self) -> int:
126
127		"""Total length in bars."""
128
129		return sum(section.bars for section in self.sections)
130
131	def __len__ (self) -> int:
132
133		"""Number of sections."""
134
135		return len(self.sections)
136
137	def __iter__ (self) -> typing.Iterator[Section]:
138
139		"""Iterate the sections in order."""
140
141		return iter(self.sections)
142
143	def __add__ (self, other: "Form") -> "Form":
144
145		"""Sequential concatenation: ``intro_form + body_form``.
146
147		The **left** operand's form-tier ``key``/``scale`` survives (a single
148		value cannot hold two form keys); the right form's form-tier key is
149		dropped.  Per-section ``Section.key``/``scale`` on either side is
150		preserved — the sections concatenate intact.
151		"""
152
153		if not isinstance(other, Form):
154			return NotImplemented
155
156		return Form(self.sections + other.sections, key = self.key, scale = self.scale)
157
158	def replace (
159		self,
160		slot: int,
161		section: typing.Optional[Section] = None,
162		**changes: typing.Any,
163	) -> "Form":
164
165		"""Replace the section at a 1-based slot — whole, or by field.
166
167		``form.replace(3, bars=16)`` stretches slot 3;
168		``form.replace(3, Section("drop", 16, energy=1.0))`` swaps it out.
169		"""
170
171		index = _check_slot(slot, len(self.sections))
172
173		if section is not None and changes:
174			raise ValueError("pass either a Section or field changes, not both")
175
176		if section is None:
177			if not changes:
178				raise ValueError("replace() needs a Section or field changes (bars=, energy=, key=, name=)")
179			section = dataclasses.replace(self.sections[index], **changes)
180
181		return Form(self.sections[:index] + (_coerce_section(section),) + self.sections[index + 1:], key = self.key, scale = self.scale)
182
183	def insert (self, slot: int, section: typing.Any) -> "Form":
184
185		"""Insert a section *at* a 1-based slot (existing sections shift right).
186
187		``slot`` may be ``len(form) + 1`` to append.
188		"""
189
190		if not isinstance(slot, int) or isinstance(slot, bool) or not 1 <= slot <= len(self.sections) + 1:
191			raise ValueError(f"slot {slot!r} is out of range (1–{len(self.sections) + 1})")
192
193		index = slot - 1
194
195		return Form(self.sections[:index] + (_coerce_section(section),) + self.sections[index:], key = self.key, scale = self.scale)
196
197	def with_energy (self, energies: typing.Dict[str, float]) -> "Form":
198
199		"""Set the energy payload on named sections — ``{"chorus": 0.9}``.
200
201		Every section whose name appears in the mapping takes the new value;
202		naming a section the form does not contain raises.  Energy *ramps*
203		(``(start, end)`` tuples) live in ``composition.energy()``, not in
204		the payload — a Section carries one number.
205		"""
206
207		names = {section.name for section in self.sections}
208
209		for name in energies:
210			if name not in names:
211				known = ", ".join(sorted(names))
212				raise ValueError(f"with_energy: no section named {name!r} in this form (sections: {known})")
213
214		return Form(tuple(
215			dataclasses.replace(section, energy = energies[section.name])
216			if section.name in energies else section
217			for section in self.sections
218		), key = self.key, scale = self.scale)
219
220	def describe (self) -> str:
221
222		"""A readable one-section-per-line summary."""
223
224		lines = [f"Form — {len(self.sections)} sections over {self.bars} bars"]
225
226		if self.key is not None or self.scale is not None:
227			lines.append(f"  (form key={self.key or '–'} scale={self.scale or '–'})")
228
229		bar = 1
230
231		for slot, section in enumerate(self.sections, start = 1):
232			extras = f"  energy={section.energy:g}"
233			if section.key is not None:
234				extras += f"  key={section.key}"
235			if section.scale is not None:
236				extras += f"  scale={section.scale}"
237			lines.append(f"  {slot}. bars {bar}{bar + section.bars - 1}  {section.name:<10} ({section.bars} bars){extras}")
238			bar += section.bars
239
240		return "\n".join(lines)
241
242	def __str__ (self) -> str:
243
244		"""Same as :meth:`describe`."""
245
246		return self.describe()

A frozen sequence of Sections — the editable, bindable form value.

List-friendly: the constructor coerces ("name", bars) tuples, so Form([("verse", 8), ("chorus", 8)]) and Form([Section("verse", 8), Section("chorus", 8)]) are the same value. Repetition is Python list arithmetic before construction.

A form may carry its own key/scale — the form tier of the key-source chain (Section.key overrides it; it overrides the composition key). A whole AABA in one key with one section borrowing another is Form([...], key="A") plus a Section(..., key="F").

Form( sections: Iterable[Any], key: Optional[str] = None, scale: Optional[str] = None)
106	def __init__ (
107		self,
108		sections: typing.Iterable[typing.Any],
109		key: typing.Optional[str] = None,
110		scale: typing.Optional[str] = None,
111	) -> None:
112
113		"""Coerce any iterable of Sections / (name, bars) tuples."""
114
115		coerced = tuple(_coerce_section(element) for element in sections)
116
117		if not coerced:
118			raise ValueError("a Form needs at least one section")
119
120		object.__setattr__(self, "sections", coerced)
121		object.__setattr__(self, "key", key)
122		object.__setattr__(self, "scale", scale)

Coerce any iterable of Sections / (name, bars) tuples.

sections: Tuple[Section, ...]
key: Optional[str] = None
scale: Optional[str] = None
bars: int
124	@property
125	def bars (self) -> int:
126
127		"""Total length in bars."""
128
129		return sum(section.bars for section in self.sections)

Total length in bars.

def replace( self, slot: int, section: Optional[Section] = None, **changes: Any) -> Form:
158	def replace (
159		self,
160		slot: int,
161		section: typing.Optional[Section] = None,
162		**changes: typing.Any,
163	) -> "Form":
164
165		"""Replace the section at a 1-based slot — whole, or by field.
166
167		``form.replace(3, bars=16)`` stretches slot 3;
168		``form.replace(3, Section("drop", 16, energy=1.0))`` swaps it out.
169		"""
170
171		index = _check_slot(slot, len(self.sections))
172
173		if section is not None and changes:
174			raise ValueError("pass either a Section or field changes, not both")
175
176		if section is None:
177			if not changes:
178				raise ValueError("replace() needs a Section or field changes (bars=, energy=, key=, name=)")
179			section = dataclasses.replace(self.sections[index], **changes)
180
181		return Form(self.sections[:index] + (_coerce_section(section),) + self.sections[index + 1:], key = self.key, scale = self.scale)

Replace the section at a 1-based slot — whole, or by field.

form.replace(3, bars=16) stretches slot 3; form.replace(3, Section("drop", 16, energy=1.0)) swaps it out.

def insert(self, slot: int, section: Any) -> Form:
183	def insert (self, slot: int, section: typing.Any) -> "Form":
184
185		"""Insert a section *at* a 1-based slot (existing sections shift right).
186
187		``slot`` may be ``len(form) + 1`` to append.
188		"""
189
190		if not isinstance(slot, int) or isinstance(slot, bool) or not 1 <= slot <= len(self.sections) + 1:
191			raise ValueError(f"slot {slot!r} is out of range (1–{len(self.sections) + 1})")
192
193		index = slot - 1
194
195		return Form(self.sections[:index] + (_coerce_section(section),) + self.sections[index:], key = self.key, scale = self.scale)

Insert a section at a 1-based slot (existing sections shift right).

slot may be len(form) + 1 to append.

def with_energy(self, energies: Dict[str, float]) -> Form:
197	def with_energy (self, energies: typing.Dict[str, float]) -> "Form":
198
199		"""Set the energy payload on named sections — ``{"chorus": 0.9}``.
200
201		Every section whose name appears in the mapping takes the new value;
202		naming a section the form does not contain raises.  Energy *ramps*
203		(``(start, end)`` tuples) live in ``composition.energy()``, not in
204		the payload — a Section carries one number.
205		"""
206
207		names = {section.name for section in self.sections}
208
209		for name in energies:
210			if name not in names:
211				known = ", ".join(sorted(names))
212				raise ValueError(f"with_energy: no section named {name!r} in this form (sections: {known})")
213
214		return Form(tuple(
215			dataclasses.replace(section, energy = energies[section.name])
216			if section.name in energies else section
217			for section in self.sections
218		), key = self.key, scale = self.scale)

Set the energy payload on named sections — {"chorus": 0.9}.

Every section whose name appears in the mapping takes the new value; naming a section the form does not contain raises. Energy ramps ((start, end) tuples) live in composition.energy(), not in the payload — a Section carries one number.

def describe(self) -> str:
220	def describe (self) -> str:
221
222		"""A readable one-section-per-line summary."""
223
224		lines = [f"Form — {len(self.sections)} sections over {self.bars} bars"]
225
226		if self.key is not None or self.scale is not None:
227			lines.append(f"  (form key={self.key or '–'} scale={self.scale or '–'})")
228
229		bar = 1
230
231		for slot, section in enumerate(self.sections, start = 1):
232			extras = f"  energy={section.energy:g}"
233			if section.key is not None:
234				extras += f"  key={section.key}"
235			if section.scale is not None:
236				extras += f"  scale={section.scale}"
237			lines.append(f"  {slot}. bars {bar}{bar + section.bars - 1}  {section.name:<10} ({section.bars} bars){extras}")
238			bar += section.bars
239
240		return "\n".join(lines)

A readable one-section-per-line summary.

@dataclasses.dataclass(frozen=True)
class Degree:
 96@dataclasses.dataclass(frozen=True)
 97class Degree:
 98
 99	"""
100	A scale degree — 1-based, resolved against key + scale at placement.
101
102	Degree 1 is the tonic; 8 is the tonic an octave up (steps may exceed the
103	scale length and resolve into higher octaves).  ``octave`` shifts whole
104	octaves; ``chroma`` is a chromatic offset in semitones (+1 = sharpened).
105	"""
106
107	step: int
108	octave: int = 0
109	chroma: int = 0
110
111	def __post_init__ (self) -> None:
112
113		"""Validate that the degree is 1-based and plausibly a degree."""
114
115		if self.step < 1:
116			raise ValueError(f"Degree steps are 1-based (1 = tonic) — got {self.step}")

A scale degree — 1-based, resolved against key + scale at placement.

Degree 1 is the tonic; 8 is the tonic an octave up (steps may exceed the scale length and resolve into higher octaves). octave shifts whole octaves; chroma is a chromatic offset in semitones (+1 = sharpened).

Degree(step: int, octave: int = 0, chroma: int = 0)
step: int
octave: int = 0
chroma: int = 0
@dataclasses.dataclass(frozen=True)
class ChordTone:
119@dataclasses.dataclass(frozen=True)
120class ChordTone:
121
122	"""
123	An index into the current chord's tones — 1-based, resolved at placement.
124
125	Accepts an int (1 = root, 2 = third, ...) or one of the names
126	``"root"`` / ``"third"`` / ``"fifth"`` / ``"seventh"``.  ``octave``
127	shifts whole octaves.
128	"""
129
130	index: int
131	octave: int = 0
132
133	def __init__ (self, index_or_name: typing.Union[int, str], octave: int = 0) -> None:
134
135		"""Normalize a tone name to its 1-based index."""
136
137		if isinstance(index_or_name, str):
138			if index_or_name not in _CHORD_TONE_NAMES:
139				raise ValueError(
140					f"Unknown chord tone name '{index_or_name}' — "
141					f"use one of {sorted(_CHORD_TONE_NAMES)} or a 1-based index"
142				)
143			index = _CHORD_TONE_NAMES[index_or_name]
144		else:
145			index = index_or_name
146
147		if index < 1:
148			raise ValueError(f"Chord tone indices are 1-based (1 = root) — got {index}")
149
150		object.__setattr__(self, "index", index)
151		object.__setattr__(self, "octave", octave)

An index into the current chord's tones — 1-based, resolved at placement.

Accepts an int (1 = root, 2 = third, ...) or one of the names "root" / "third" / "fifth" / "seventh". octave shifts whole octaves.

ChordTone(index_or_name: Union[int, str], octave: int = 0)
133	def __init__ (self, index_or_name: typing.Union[int, str], octave: int = 0) -> None:
134
135		"""Normalize a tone name to its 1-based index."""
136
137		if isinstance(index_or_name, str):
138			if index_or_name not in _CHORD_TONE_NAMES:
139				raise ValueError(
140					f"Unknown chord tone name '{index_or_name}' — "
141					f"use one of {sorted(_CHORD_TONE_NAMES)} or a 1-based index"
142				)
143			index = _CHORD_TONE_NAMES[index_or_name]
144		else:
145			index = index_or_name
146
147		if index < 1:
148			raise ValueError(f"Chord tone indices are 1-based (1 = root) — got {index}")
149
150		object.__setattr__(self, "index", index)
151		object.__setattr__(self, "octave", octave)

Normalize a tone name to its 1-based index.

index: int
octave: int = 0
@dataclasses.dataclass(frozen=True)
class Approach:
154@dataclasses.dataclass(frozen=True)
155class Approach:
156
157	"""
158	A half-step approach into a target pitch at the next chord boundary.
159
160	Resolves at placement, one semitone below its target (the leading-tone
161	approach); a ``ChordTone`` target reads the NEXT chord through the
162	harmony window, so the approach lands as the harmony arrives.
163	"""
164
165	target: typing.Union[int, Degree, ChordTone]

A half-step approach into a target pitch at the next chord boundary.

Resolves at placement, one semitone below its target (the leading-tone approach); a ChordTone target reads the NEXT chord through the harmony window, so the approach lands as the harmony arrives.

Approach( target: Union[int, Degree, ChordTone])
target: Union[int, Degree, ChordTone]
@dataclasses.dataclass(frozen=True)
class MotifEvent:
265@dataclasses.dataclass(frozen=True)
266class MotifEvent:
267
268	"""
269	One timed note event inside a Motif.
270
271	``pitch`` is a specification: an absolute MIDI int, a drum name string,
272	a :class:`Degree`, :class:`ChordTone`, or :class:`Approach` — or None
273	for a pitch-stripped skeleton event (see :meth:`Motif.rhythm`), which
274	must be re-pitched via :meth:`Motif.pitched` before placement.
275	``velocity`` is an int or a ``(low, high)`` random-range tuple.
276	"""
277
278	beat: float
279	pitch: PitchSpec
280	velocity: typing.Union[int, typing.Tuple[int, int]] = _DEFAULT_VELOCITY
281	duration: float = 0.25
282	probability: float = 1.0
283
284	def __post_init__ (self) -> None:
285
286		"""Validate ranges that are wrong at any placement."""
287
288		if self.duration <= 0:
289			raise ValueError(f"Event duration must be positive — got {self.duration}")
290		if not 0.0 <= self.probability <= 1.0:
291			raise ValueError(f"Event probability must be 0.0–1.0 — got {self.probability}")
292
293	def _sort_key (self) -> tuple:
294
295		"""Canonical ordering key — makes parallel merge order-independent."""
296
297		return (self.beat, _pitch_sort_key(self.pitch), _velocity_key(self.velocity), self.duration, self.probability)

One timed note event inside a Motif.

pitch is a specification: an absolute MIDI int, a drum name string, a Degree, ChordTone, or Approach — or None for a pitch-stripped skeleton event (see Motif.rhythm()), which must be re-pitched via Motif.pitched() before placement. velocity is an int or a (low, high) random-range tuple.

MotifEvent( beat: float, pitch: Union[int, str, Degree, ChordTone, Approach, NoneType], velocity: Union[int, Tuple[int, int]] = 100, duration: float = 0.25, probability: float = 1.0)
beat: float
pitch: Union[int, str, Degree, ChordTone, Approach, NoneType]
velocity: Union[int, Tuple[int, int]] = 100
duration: float = 0.25
probability: float = 1.0
@dataclasses.dataclass(frozen=True)
class ControlEvent:
300@dataclasses.dataclass(frozen=True)
301class ControlEvent:
302
303	"""
304	One timed control gesture inside a Motif: a discrete write or a shaped ramp.
305
306	A discrete write has ``end=None`` and ``span=0.0``; a ramp interpolates
307	``start`` → ``end`` over ``span`` beats through the easing ``shape``.
308	Pulse density (``resolution=``) is deliberately not stored here — beats
309	and shapes are music; MIDI traffic density is set at the placement call.
310	"""
311
312	beat: float
313	signal: ControlSignal
314	start: float
315	end: typing.Optional[float] = None
316	span: float = 0.0
317	shape: typing.Union[str, "subsequence.easing.EasingFn"] = "linear"
318	probability: float = 1.0
319
320	def __post_init__ (self) -> None:
321
322		"""Validate the discrete/ramp invariants."""
323
324		if (self.end is None) != (self.span == 0.0):
325			raise ValueError("A ramp needs both end= and span= (a discrete write has neither)")
326		if self.span < 0:
327			raise ValueError(f"Ramp span must be non-negative — got {self.span}")
328		if not 0.0 <= self.probability <= 1.0:
329			raise ValueError(f"Event probability must be 0.0–1.0 — got {self.probability}")
330
331	def _sort_key (self) -> tuple:
332
333		"""Canonical ordering key — makes parallel merge order-independent."""
334
335		end = self.start if self.end is None else self.end
336		return (self.beat, _signal_sort_key(self.signal), self.start, end, self.span, self.probability)
337
338	def _value_at (self, fraction: float) -> float:
339
340		"""The interpolated value at a 0–1 fraction through the ramp."""
341
342		if self.end is None:
343			return self.start
344
345		easing_fn = self.shape if callable(self.shape) else subsequence.easing.get_easing(self.shape)
346		return self.start + (self.end - self.start) * easing_fn(max(0.0, min(1.0, fraction)))

One timed control gesture inside a Motif: a discrete write or a shaped ramp.

A discrete write has end=None and span=0.0; a ramp interpolates startend over span beats through the easing shape. Pulse density (resolution=) is deliberately not stored here — beats and shapes are music; MIDI traffic density is set at the placement call.

ControlEvent( beat: float, signal: Union[subsequence.motifs.CC, subsequence.motifs.PitchBend, subsequence.motifs.NRPN, subsequence.motifs.RPN, subsequence.motifs.OSC], start: float, end: Optional[float] = None, span: float = 0.0, shape: Union[str, Callable[[float], float]] = 'linear', probability: float = 1.0)
beat: float
start: float
end: Optional[float] = None
span: float = 0.0
shape: Union[str, Callable[[float], float]] = 'linear'
probability: float = 1.0
@dataclasses.dataclass(frozen=True)
class Progression:
1007@dataclasses.dataclass(frozen=True)
1008class Progression:
1009
1010	"""A frozen sequence of :class:`ChordSpan` — the governing harmony value.
1011
1012	Always a realised value: binding it to the clock freezes one realisation;
1013	``p.progression()`` keeps its breathing behaviour by re-realising a fresh
1014	one each rebuild.  Iterating yields ``(chord, start, length)``
1015	:class:`ChordEvent` tuples (the old ``ChordTimeline`` contract), so
1016	placement loops keep working unchanged.
1017
1018	The governing family supports ``+`` (concatenate) and ``*`` (tile) but
1019	never ``&`` — there is one current chord (P1, the type law).
1020
1021	Attributes:
1022		spans: The chord spans, in order.
1023		trailing_history: Engine continuity metadata set by
1024			:meth:`Composition.freeze` — the NIR history at capture time,
1025			restored on each frozen replay.  Empty for hand-built values.
1026	"""
1027
1028	spans: typing.Tuple[ChordSpan, ...]
1029	trailing_history: typing.Tuple[subsequence.chords.Chord, ...] = ()
1030
1031	def __post_init__ (self) -> None:
1032
1033		"""Normalise span containers to tuples."""
1034
1035		object.__setattr__(self, "spans", tuple(self.spans))
1036		object.__setattr__(self, "trailing_history", tuple(self.trailing_history))
1037
1038		if not self.spans:
1039			raise ValueError("a Progression needs at least one chord span")
1040
1041	# -- queries ------------------------------------------------------------
1042
1043	@property
1044	def length (self) -> float:
1045
1046		"""Total length in beats (the sum of span lengths)."""
1047
1048		return float(sum(span.beats for span in self.spans))
1049
1050	@property
1051	def is_concrete (self) -> bool:
1052
1053		"""True when every span is key-independent (no romans/degrees)."""
1054
1055		return all(span.is_concrete for span in self.spans)
1056
1057	@property
1058	def chords (self) -> typing.Tuple[typing.Any, ...]:
1059
1060		"""The bare chords, one per span (concrete progressions only)."""
1061
1062		self._require_concrete("read .chords")
1063
1064		return tuple(span.chord for span in self.spans)
1065
1066	@property
1067	def loops_on_exhaustion (self) -> bool:
1068
1069		"""True when the clock must loop rather than fall through to live stepping."""
1070
1071		return any(isinstance(span.chord, PitchSet) for span in self.spans)
1072
1073	def _require_concrete (self, action: str) -> None:
1074
1075		"""Raise with a resolution hint when key-relative spans remain."""
1076
1077		if not self.is_concrete:
1078			relative = ", ".join(span.label() for span in self.spans if not span.is_concrete)
1079			raise ValueError(
1080				f"cannot {action} on a key-relative progression (contains {relative}) — "
1081				"call .resolve(key=...) first, or bind it where a key is known"
1082			)
1083
1084	def __iter__ (self) -> typing.Iterator[ChordEvent]:
1085
1086		"""Yield ``(chord, start, length)`` events — decorated chords where spiced."""
1087
1088		self._require_concrete("iterate")
1089
1090		cursor = 0.0
1091
1092		for span in self.spans:
1093			chord = DecoratedChord(span) if span.is_decorated else span.chord
1094			yield ChordEvent(chord=chord, start=cursor, length=span.beats)
1095			cursor += span.beats
1096
1097	def __len__ (self) -> int:
1098
1099		"""The number of chord spans."""
1100
1101		return len(self.spans)
1102
1103	def events (self) -> typing.Tuple[ChordEvent, ...]:
1104
1105		"""The realised timeline as a tuple (iteration, materialised)."""
1106
1107		return tuple(self)
1108
1109	def span_at (self, beat: float) -> typing.Tuple[ChordSpan, float, float]:
1110
1111		"""Return ``(span, start, end)`` for the span sounding at *beat*.
1112
1113		*beat* wraps modulo the progression length, so the lookup also
1114		serves looped playback.
1115		"""
1116
1117		position = beat % self.length
1118		cursor = 0.0
1119
1120		for span in self.spans:
1121			if cursor <= position < cursor + span.beats:
1122				return span, cursor, cursor + span.beats
1123			cursor += span.beats
1124
1125		final = self.spans[-1]
1126		return final, self.length - final.beats, self.length
1127
1128	def resolve (self, key: typing.Union[str, int], scale: str = "ionian") -> "Progression":
1129
1130		"""Resolve every key-relative span against a key (name or pitch class)."""
1131
1132		key_pc = key if isinstance(key, int) else subsequence.chords.key_name_to_pc(key)
1133
1134		return dataclasses.replace(
1135			self,
1136			spans = tuple(span.resolve(key_pc, scale) for span in self.spans),
1137		)
1138
1139	@classmethod
1140	def generate (
1141		cls,
1142		style: typing.Union[str, typing.Any] = "functional_major",
1143		bars: int = 8,
1144		beats: typing.Union[float, typing.List[float]] = DEFAULT_SPAN_BEATS,
1145		*,
1146		key: typing.Optional[str] = None,
1147		scale: typing.Optional[str] = None,
1148		seed: typing.Optional[int] = None,
1149		rng: typing.Optional[random.Random] = None,
1150		pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
1151		end: typing.Optional[typing.Any] = None,
1152		avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
1153		cadence: typing.Optional[str] = None,
1154		dominant_7th: bool = True,
1155		gravity: float = 1.0,
1156		nir_strength: float = 0.5,
1157		minor_turnaround_weight: float = 0.0,
1158		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1159	) -> "Progression":
1160
1161		"""Generate a progression from a chord-graph walk — the hybrid generator.
1162
1163		Full parameter pass-through to the engine (no more throwaway default
1164		engines), plus the hybrid constraints: ``pins`` fix chords at 1-based
1165		bars, ``end`` fixes the last bar, ``avoid`` excludes chords
1166		everywhere.  Constraints compile into the walk — a backward
1167		feasibility pass guarantees satisfiability before any chord is
1168		drawn (unsatisfiable constraints raise immediately), then a forward
1169		walk samples through the engine's real history-dependent weights
1170		(NIR, gravity, diversity keep their character).
1171
1172		**Without** ``key=`` the result is key-relative — the walk runs
1173		against a reference tonic and the spans store scale-proof
1174		major-relative romans, so the value prints meaningfully unbound and
1175		resolves wherever it is bound (the walk itself is key-invariant).
1176		**With** ``key=`` the result is concrete.
1177
1178		Parameters:
1179			style: A chord-graph style name (or ``ChordGraph`` instance).
1180			bars: How many chords to generate.
1181			beats: Span length per chord — a scalar, or a list cycled.
1182			key: Key for a concrete result; omit for a key-relative value.
1183			scale: Scale for int constraints' quality inference (e.g.
1184				``end=1``).  Defaults from the style (aeolian_minor →
1185				minor); explicit strings (``"V"``, ``"bVII7"``) never
1186				need it.
1187			seed: Seed for the walk.  A standalone generated value without
1188				a seed warns — module-level nondeterminism breaks live
1189				reload.
1190			rng: An explicit random stream (overrides ``seed``).
1191			pins: ``{bar: chord}`` — 1-based; values parse like progression
1192				elements (ints, romans, names, ``Chord``).
1193			end: The chord at the final bar — ``end="V"`` is the cadential
1194				major dominant in minor (a string because it is chromatic;
1195				no int can ask for it).
1196			avoid: Chords excluded from the walk.  Naming a chord outside
1197				the style's vocabulary is allowed (trivially satisfied).
1198			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
1199				``"fakeout"``, theory aliases accepted) — its formula
1200				becomes pins on the final bars, so the walk *approaches*
1201				the close.  Conflicts with ``end=`` or pins on those bars.
1202			dominant_7th / gravity / nir_strength / minor_turnaround_weight /
1203				root_diversity: The engine parameters, exactly as
1204				:meth:`Composition.harmony` takes them.
1205
1206		Example:
1207			```python
1208			chorus = subsequence.Progression.generate(
1209				style="aeolian_minor", bars=4, end="V", seed=7,
1210			)
1211			print(chorus)        # romans until bound
1212			```
1213		"""
1214
1215		if bars < 1:
1216			raise ValueError("bars must be at least 1")
1217
1218		if cadence is not None:
1219			pins = cadence_pins(cadence, bars, pins, end)
1220			end = None
1221
1222		if rng is None:
1223			if seed is None:
1224				warnings.warn(
1225					"Progression.generate without seed= is nondeterministic — "
1226					"pass seed= so the value survives live reload",
1227					stacklevel = 2,
1228				)
1229				rng = random.Random()
1230			else:
1231				rng = random.Random(seed)
1232
1233		resolved_scale = scale if scale is not None else _STYLE_SCALES.get(style if isinstance(style, str) else "", "ionian")
1234		relative = key is None
1235		reference = key if key is not None else "C"
1236
1237		state = subsequence.harmonic_state.HarmonicState(
1238			key_name = reference,
1239			graph_style = style,
1240			include_dominant_7th = dominant_7th,
1241			key_gravity_blend = gravity,
1242			nir_strength = nir_strength,
1243			minor_turnaround_weight = minor_turnaround_weight,
1244			root_diversity = root_diversity,
1245			rng = rng,
1246		)
1247
1248		resolved_pins = {
1249			position: resolve_constraint(spec, state.key_root_pc, resolved_scale, f"pins[{position}]")
1250			for position, spec in (pins or {}).items()
1251		}
1252		resolved_end = resolve_constraint(end, state.key_root_pc, resolved_scale, "end") if end is not None else None
1253		resolved_avoid = [resolve_constraint(spec, state.key_root_pc, resolved_scale, "avoid") for spec in (avoid or [])]
1254
1255		if 1 in resolved_pins:
1256			if resolved_pins[1] not in state.graph.nodes():
1257				raise ValueError(
1258					f"pins[1]={resolved_pins[1].name()} is not in style {style!r}'s vocabulary"
1259				)
1260			state.current_chord = resolved_pins[1]
1261
1262		def commit (chosen: subsequence.chords.Chord) -> None:
1263			state.current_chord = chosen
1264
1265		walked = subsequence.sequence_utils.constrained_walk(
1266			state.graph,
1267			state.current_chord,
1268			bars,
1269			rng = state.rng,
1270			pins = resolved_pins,
1271			end = resolved_end,
1272			avoid = resolved_avoid,
1273			weight_modifier = state._transition_weight,
1274			before_choice = state._record_transition_source,
1275			after_choice = commit,
1276		)
1277
1278		lengths = _span_lengths(beats, bars)
1279
1280		if relative:
1281			return cls(spans = tuple(
1282				ChordSpan(chord = _roman_from_chord(chord, state.key_root_pc), beats = lengths[index])
1283				for index, chord in enumerate(walked)
1284			))
1285
1286		return cls(spans = tuple(
1287			ChordSpan(chord = chord, beats = lengths[index])
1288			for index, chord in enumerate(walked)
1289		))
1290
1291	# -- algebra ------------------------------------------------------------
1292
1293	def __add__ (self, other: "Progression") -> "Progression":
1294
1295		"""Concatenate two progressions (the governing ``+``)."""
1296
1297		if not isinstance(other, Progression):
1298			return NotImplemented
1299
1300		return Progression(spans = self.spans + other.spans)
1301
1302	def __mul__ (self, count: int) -> "Progression":
1303
1304		"""Tile the spans *count* times."""
1305
1306		if not isinstance(count, int) or isinstance(count, bool):
1307			return NotImplemented
1308		if count < 1:
1309			raise ValueError("a progression must repeat at least once (n >= 1)")
1310
1311		return Progression(spans = self.spans * count)
1312
1313	def __and__ (self, other: typing.Any) -> "Progression":
1314
1315		"""Parallel merge is a type error for governing values — by design."""
1316
1317		raise TypeError(
1318			"Progressions cannot be merged with & — there is one current chord. "
1319			"Sequence them with +, or give a pattern its own part-level progression."
1320		)
1321
1322	# -- spice (the five operators) and editing ------------------------------
1323
1324	def extend (self, *extensions: typing.Any, only: typing.Optional[typing.List[int]] = None) -> "Progression":
1325
1326		"""Add chord extensions (``7``/``9``/``11``/``13``/``"sus4"``/...) to every span.
1327
1328		``only=`` restricts the spice to the given 1-based chord slots.
1329		"""
1330
1331		slots = set(range(len(self.spans))) if only is None else {_check_slot(s, len(self.spans)) for s in only}
1332
1333		spans = tuple(
1334			dataclasses.replace(span, extensions = tuple(dict.fromkeys(span.extensions + extensions)))
1335			if index in slots else span
1336			for index, span in enumerate(self.spans)
1337		)
1338
1339		return dataclasses.replace(self, spans=spans)
1340
1341	def inversions (self, spec: typing.Union[int, typing.List[int]]) -> "Progression":
1342
1343		"""Set chord inversions — a single int for all spans, or a list cycled per span."""
1344
1345		values = [spec] if isinstance(spec, int) else list(spec)
1346
1347		if not values:
1348			raise ValueError("inversions list is empty — pass at least one inversion")
1349
1350		spans = tuple(
1351			dataclasses.replace(span, inversion = int(values[index % len(values)]))
1352			for index, span in enumerate(self.spans)
1353		)
1354
1355		return dataclasses.replace(self, spans=spans)
1356
1357	def spread (self, style: str) -> "Progression":
1358
1359		"""Set the voicing spread: ``"close"``, ``"open"`` (drop-2), or ``"wide"``."""
1360
1361		spans = tuple(dataclasses.replace(span, spread = None if style == "close" else style) for span in self.spans)
1362
1363		return dataclasses.replace(self, spans=spans)
1364
1365	def over (self, bass: typing.Union[int, str], only: typing.Optional[typing.List[int]] = None) -> "Progression":
1366
1367		"""Put the progression over a slash/pedal bass — *the* trance/techno move.
1368
1369		*bass* is a pitch class int, a note name (``"G"``), or ``"tonic"``.  A
1370		note name is key-independent, so it resolves to its pitch class right
1371		here; ``"tonic"`` follows the key and stays relative until the
1372		progression is resolved.  ``only=`` restricts it to the given 1-based
1373		slots (slash chords rather than a full pedal).
1374		"""
1375
1376		if isinstance(bass, str) and bass != "tonic":
1377			bass = subsequence.chords.key_name_to_pc(bass)	# note names are key-independent — resolve now
1378		elif isinstance(bass, int) and not 0 <= bass <= 11:
1379			raise ValueError(f"a bass pitch class must be 0–11, got {bass}")
1380
1381		slots = set(range(len(self.spans))) if only is None else {_check_slot(s, len(self.spans)) for s in only}
1382
1383		spans = tuple(
1384			dataclasses.replace(span, bass=bass) if index in slots else span
1385			for index, span in enumerate(self.spans)
1386		)
1387
1388		return dataclasses.replace(self, spans=spans)
1389
1390	def borrow (self, slot: typing.Union[int, typing.List[int]]) -> "Progression":
1391
1392		"""Borrow the chord(s) at the given 1-based slot(s) from the parallel scale.
1393
1394		Modal interchange for key-relative content: the degree re-resolves
1395		against the parallel mode (minor under a major scale and vice
1396		versa).  Concrete chords raise — there is nothing relative to borrow.
1397		"""
1398
1399		slots = {_check_slot(s, len(self.spans)) for s in ([slot] if isinstance(slot, int) else slot)}
1400
1401		spans = list(self.spans)
1402
1403		for index in slots:
1404			chord = spans[index].chord
1405			if not isinstance(chord, RomanChord):
1406				raise ValueError(
1407					f"slot {index + 1} holds a concrete chord ({spans[index].label()}) — "
1408					"borrow() needs key-relative content (an int degree or roman)"
1409				)
1410			spans[index] = dataclasses.replace(spans[index], chord = dataclasses.replace(chord, borrowed = not chord.borrowed))
1411
1412		return dataclasses.replace(self, spans=tuple(spans))
1413
1414	def replace (self, slot: int, chord: typing.Any) -> "Progression":
1415
1416		"""Replace the chord at a 1-based slot (the span keeps its beats)."""
1417
1418		index = _check_slot(slot, len(self.spans))
1419		parsed = parse_element(chord, beats = self.spans[index].beats)
1420
1421		spans = self.spans[:index] + (parsed,) + self.spans[index + 1:]
1422
1423		return dataclasses.replace(self, spans=spans)
1424
1425	def cadence (self, name: str = "strong") -> "Progression":
1426
1427		"""Substitute a cadence formula into the tail — the close, named.
1428
1429		The final spans take the formula's chords (``"strong"`` is V→I,
1430		``"soft"`` IV→I, ``"open"`` IV→V, ``"fakeout"`` V→vi; theory names —
1431		authentic, plagal, half, deceptive — work as aliases).  Each replaced
1432		span keeps its beats; its old chord and decorations go.  Formula
1433		chords are key-relative (ints follow the bound scale's qualities,
1434		``"V"`` is the major dominant by convention), so the tail resolves
1435		wherever the progression is bound — a concrete progression becomes
1436		mixed and resolves its tail at bind time, like any roman content.
1437
1438		Example::
1439
1440			verse = subsequence.progression(["Am", "F", "C", "G"]).cadence("open")
1441			# Bound in A minor: Am F Dm E — the half close, hanging on the dominant
1442
1443		Raises:
1444			ValueError: If the cadence name is unknown, or the progression
1445				has fewer spans than the formula.
1446		"""
1447
1448		spec = subsequence.cadences.cadence_formula(name)
1449		count = len(spec.formula)
1450
1451		if len(self.spans) < count:
1452			raise ValueError(
1453				f"cadence({name!r}) substitutes the last {count} chords, but this "
1454				f"progression has only {len(self.spans)}"
1455			)
1456
1457		tail = tuple(
1458			parse_element(element, beats = span.beats)
1459			for element, span in zip(spec.formula, self.spans[-count:])
1460		)
1461
1462		return dataclasses.replace(self, spans = self.spans[:-count] + tail)
1463
1464	def with_rhythm (self, beats: typing.Union[float, typing.List[float]]) -> "Progression":
1465
1466		"""Reshape the harmonic rhythm — a scalar for all spans, or a list cycled per span."""
1467
1468		if isinstance(beats, bool):
1469			raise TypeError(f"with_rhythm takes beats or a list of beats, got bool: {beats!r}")
1470
1471		values = [float(beats)] if isinstance(beats, (int, float)) else [float(b) for b in beats]
1472
1473		if not values:
1474			raise ValueError("with_rhythm list is empty — pass at least one length")
1475
1476		spans = tuple(
1477			dataclasses.replace(span, beats = float(values[index % len(values)]))
1478			for index, span in enumerate(self.spans)
1479		)
1480
1481		return dataclasses.replace(self, spans=spans)
1482
1483	def elaborate (self, depth: int = 1, seed: typing.Optional[int] = None) -> "Progression":
1484
1485		"""Steedman-inspired chord elaboration — approach each chord by fifths.
1486
1487		Implements the heart of Mark Steedman's generative grammar for
1488		jazz/blues chord sequences: every chord is **approached** by a chain
1489		of secondary dominants propagated backward around the cycle of fifths
1490		(Rule 3, "the perfect cadence propagated backward"), carved out of that
1491		chord's own span (Rule 1, metric subdivision).  ``depth`` is literally
1492		how many fifth-steps back the chain extends:
1493
1494		- ``depth=0`` — identity (the bare progression).
1495		- ``depth=1`` — a secondary dominant before each chord: ``[X]`` →
1496		  ``[V7/X, X]`` (e.g. a bar of C becomes G7 C).
1497		- ``depth=2`` — a secondary ii–V: ``[ii/X, V7/X, X]`` (Dm7 G7 C).
1498		- ``depth≥3`` — the chain extends (…V7/V7/X), the furthest-back chord
1499		  is made minor — the ``ii`` of *its own local dominant* (the next
1500		  link in the chain), forming a ii–V into that link, not the
1501		  target's own ii — and dominants are recoloured by **tritone
1502		  substitution** with even odds (Rule 4) for chromatic descents.
1503		  This tritone choice is the only nondeterministic part, so ``seed``
1504		  is taken (or warned) at depth ≥ 3.
1505
1506		Its flagship is the 12-bar blues with depth-per-chorus — elaborate a
1507		``"twelve_bar_blues"`` more each chorus and the ii–V turnarounds and
1508		tritone subs accumulate.
1509
1510		The progression must be **concrete** (resolved to rooted chords);
1511		the inserted dominants are computed by pitch-class arithmetic.  Each
1512		chord keeps its decorations on the final (resolved) sub-span; the
1513		inserted approach chords are bare dominant/minor sevenths.  Note that
1514		each span is divided into ``depth + 1`` equal sub-spans, so deep
1515		elaboration of a short harmonic rhythm can drop sub-spans below the
1516		harmony clock's lookahead floor — which raises at ``play()``/
1517		``render()`` if the result is bound to the global clock (it is free
1518		of that floor at the part level, ``p.progression()``).
1519
1520		Parameters:
1521			depth: Elaboration depth (≥ 0).
1522			seed: Seed for the depth-≥3 tritone-substitution choices.
1523
1524		Returns:
1525			A new :class:`Progression` with the approach chords inserted.
1526
1527		Raises:
1528			ValueError: If *depth* is negative, the progression is
1529				key-relative, or any span is a rootless
1530				:class:`PitchSet`.
1531
1532		Example:
1533			```python
1534			blues = subsequence.progression("twelve_bar_blues").resolve("C")
1535			chorus2 = blues.elaborate(2, seed=4)      # ii–V turnarounds throughout
1536			```
1537		"""
1538
1539		if depth < 0:
1540			raise ValueError("elaborate depth must be at least 0")
1541
1542		self._require_concrete("elaborate")
1543
1544		if depth == 0:
1545			return self
1546
1547		for span in self.spans:
1548			if isinstance(span.chord, PitchSet):
1549				raise ValueError("elaborate needs rooted chords — a PitchSet has no root to approach by fifths")
1550
1551		if depth >= 3 and seed is None:
1552			warnings.warn(
1553				"elaborate(depth>=3) makes tritone-substitution choices — pass seed= so the "
1554				"result survives live reload",
1555				stacklevel = 2,
1556			)
1557
1558		rng = random.Random(seed)
1559		new_spans: typing.List[ChordSpan] = []
1560
1561		for span in self.spans:
1562
1563			target_root = span.chord.root_pc
1564			sub_beats = span.beats / (depth + 1)
1565
1566			# The backward cycle-of-fifths chain, furthest-back first: chord j
1567			# sits a fifth above chord j-1's target, i.e. root = X + 7·j.  The
1568			# furthest-back (j == depth) is made minor — the ii of its OWN
1569			# local dominant (the next link), forming a ii–V into that link —
1570			# once the chain is long enough (depth >= 2) to spell one.
1571			for j in range(depth, 0, -1):
1572				root = (target_root + 7 * j) % 12
1573				quality = "minor_7th" if (j == depth and depth >= 2) else "dominant_7th"
1574
1575				# Tritone substitution recolours a dominant to the dom7 a
1576				# tritone away (same guide tones, chromatic resolution).
1577				if quality == "dominant_7th" and depth >= 3 and rng.random() < 0.5:
1578					root = (root + 6) % 12
1579
1580				new_spans.append(ChordSpan(chord = subsequence.chords.Chord(root_pc = root, quality = quality), beats = sub_beats))
1581
1582			# The target keeps its own chord and decorations, on its sub-span.
1583			new_spans.append(dataclasses.replace(span, beats = sub_beats))
1584
1585		return dataclasses.replace(self, spans = tuple(new_spans))
1586
1587	# -- description ----------------------------------------------------------
1588
1589	def describe (self, key: typing.Optional[typing.Union[str, int]] = None, scale: str = "ionian") -> str:
1590
1591		"""A readable, one-chord-per-line summary.
1592
1593		Key-relative spans print as written (romans/degrees) when unbound,
1594		and as concrete chord names under a *key*.
1595		"""
1596
1597		key_pc = None if key is None else (key if isinstance(key, int) else subsequence.chords.key_name_to_pc(key))
1598
1599		lines = [f"Progression — {len(self.spans)} chords over {self.length:g} beats"]
1600		cursor = 0.0
1601
1602		for span in self.spans:
1603			lines.append(
1604				f"  {cursor:6.2f}{cursor + span.beats:6.2f}   "
1605				f"{span.label(key_pc, scale):<8} ({span.beats:g} beats)"
1606			)
1607			cursor += span.beats
1608
1609		return "\n".join(lines)
1610
1611	def __str__ (self) -> str:
1612
1613		"""Same as :meth:`describe` with no key bound."""
1614
1615		return self.describe()

A frozen sequence of ChordSpan — the governing harmony value.

Always a realised value: binding it to the clock freezes one realisation; p.progression() keeps its breathing behaviour by re-realising a fresh one each rebuild. Iterating yields (chord, start, length) ChordEvent tuples (the old ChordTimeline contract), so placement loops keep working unchanged.

The governing family supports + (concatenate) and * (tile) but never & — there is one current chord (P1, the type law).

Attributes:
  • spans: The chord spans, in order.
  • trailing_history: Engine continuity metadata set by Composition.freeze() — the NIR history at capture time, restored on each frozen replay. Empty for hand-built values.
Progression( spans: Tuple[ChordSpan, ...], trailing_history: Tuple[Chord, ...] = ())
spans: Tuple[ChordSpan, ...]
trailing_history: Tuple[Chord, ...] = ()
length: float
1043	@property
1044	def length (self) -> float:
1045
1046		"""Total length in beats (the sum of span lengths)."""
1047
1048		return float(sum(span.beats for span in self.spans))

Total length in beats (the sum of span lengths).

is_concrete: bool
1050	@property
1051	def is_concrete (self) -> bool:
1052
1053		"""True when every span is key-independent (no romans/degrees)."""
1054
1055		return all(span.is_concrete for span in self.spans)

True when every span is key-independent (no romans/degrees).

chords: Tuple[Any, ...]
1057	@property
1058	def chords (self) -> typing.Tuple[typing.Any, ...]:
1059
1060		"""The bare chords, one per span (concrete progressions only)."""
1061
1062		self._require_concrete("read .chords")
1063
1064		return tuple(span.chord for span in self.spans)

The bare chords, one per span (concrete progressions only).

loops_on_exhaustion: bool
1066	@property
1067	def loops_on_exhaustion (self) -> bool:
1068
1069		"""True when the clock must loop rather than fall through to live stepping."""
1070
1071		return any(isinstance(span.chord, PitchSet) for span in self.spans)

True when the clock must loop rather than fall through to live stepping.

def events(self) -> Tuple[subsequence.progressions.ChordEvent, ...]:
1103	def events (self) -> typing.Tuple[ChordEvent, ...]:
1104
1105		"""The realised timeline as a tuple (iteration, materialised)."""
1106
1107		return tuple(self)

The realised timeline as a tuple (iteration, materialised).

def span_at( self, beat: float) -> Tuple[ChordSpan, float, float]:
1109	def span_at (self, beat: float) -> typing.Tuple[ChordSpan, float, float]:
1110
1111		"""Return ``(span, start, end)`` for the span sounding at *beat*.
1112
1113		*beat* wraps modulo the progression length, so the lookup also
1114		serves looped playback.
1115		"""
1116
1117		position = beat % self.length
1118		cursor = 0.0
1119
1120		for span in self.spans:
1121			if cursor <= position < cursor + span.beats:
1122				return span, cursor, cursor + span.beats
1123			cursor += span.beats
1124
1125		final = self.spans[-1]
1126		return final, self.length - final.beats, self.length

Return (span, start, end) for the span sounding at beat.

beat wraps modulo the progression length, so the lookup also serves looped playback.

def resolve( self, key: Union[str, int], scale: str = 'ionian') -> Progression:
1128	def resolve (self, key: typing.Union[str, int], scale: str = "ionian") -> "Progression":
1129
1130		"""Resolve every key-relative span against a key (name or pitch class)."""
1131
1132		key_pc = key if isinstance(key, int) else subsequence.chords.key_name_to_pc(key)
1133
1134		return dataclasses.replace(
1135			self,
1136			spans = tuple(span.resolve(key_pc, scale) for span in self.spans),
1137		)

Resolve every key-relative span against a key (name or pitch class).

@classmethod
def generate( cls, style: Union[str, Any] = 'functional_major', bars: int = 8, beats: Union[float, List[float]] = 4.0, *, key: Optional[str] = None, scale: Optional[str] = None, seed: Optional[int] = None, rng: Optional[random.Random] = None, pins: Optional[Dict[int, Any]] = None, end: Optional[Any] = None, avoid: Optional[Sequence[Any]] = None, cadence: Optional[str] = None, dominant_7th: bool = True, gravity: float = 1.0, nir_strength: float = 0.5, minor_turnaround_weight: float = 0.0, root_diversity: float = 0.4) -> Progression:
1139	@classmethod
1140	def generate (
1141		cls,
1142		style: typing.Union[str, typing.Any] = "functional_major",
1143		bars: int = 8,
1144		beats: typing.Union[float, typing.List[float]] = DEFAULT_SPAN_BEATS,
1145		*,
1146		key: typing.Optional[str] = None,
1147		scale: typing.Optional[str] = None,
1148		seed: typing.Optional[int] = None,
1149		rng: typing.Optional[random.Random] = None,
1150		pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
1151		end: typing.Optional[typing.Any] = None,
1152		avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
1153		cadence: typing.Optional[str] = None,
1154		dominant_7th: bool = True,
1155		gravity: float = 1.0,
1156		nir_strength: float = 0.5,
1157		minor_turnaround_weight: float = 0.0,
1158		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1159	) -> "Progression":
1160
1161		"""Generate a progression from a chord-graph walk — the hybrid generator.
1162
1163		Full parameter pass-through to the engine (no more throwaway default
1164		engines), plus the hybrid constraints: ``pins`` fix chords at 1-based
1165		bars, ``end`` fixes the last bar, ``avoid`` excludes chords
1166		everywhere.  Constraints compile into the walk — a backward
1167		feasibility pass guarantees satisfiability before any chord is
1168		drawn (unsatisfiable constraints raise immediately), then a forward
1169		walk samples through the engine's real history-dependent weights
1170		(NIR, gravity, diversity keep their character).
1171
1172		**Without** ``key=`` the result is key-relative — the walk runs
1173		against a reference tonic and the spans store scale-proof
1174		major-relative romans, so the value prints meaningfully unbound and
1175		resolves wherever it is bound (the walk itself is key-invariant).
1176		**With** ``key=`` the result is concrete.
1177
1178		Parameters:
1179			style: A chord-graph style name (or ``ChordGraph`` instance).
1180			bars: How many chords to generate.
1181			beats: Span length per chord — a scalar, or a list cycled.
1182			key: Key for a concrete result; omit for a key-relative value.
1183			scale: Scale for int constraints' quality inference (e.g.
1184				``end=1``).  Defaults from the style (aeolian_minor →
1185				minor); explicit strings (``"V"``, ``"bVII7"``) never
1186				need it.
1187			seed: Seed for the walk.  A standalone generated value without
1188				a seed warns — module-level nondeterminism breaks live
1189				reload.
1190			rng: An explicit random stream (overrides ``seed``).
1191			pins: ``{bar: chord}`` — 1-based; values parse like progression
1192				elements (ints, romans, names, ``Chord``).
1193			end: The chord at the final bar — ``end="V"`` is the cadential
1194				major dominant in minor (a string because it is chromatic;
1195				no int can ask for it).
1196			avoid: Chords excluded from the walk.  Naming a chord outside
1197				the style's vocabulary is allowed (trivially satisfied).
1198			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
1199				``"fakeout"``, theory aliases accepted) — its formula
1200				becomes pins on the final bars, so the walk *approaches*
1201				the close.  Conflicts with ``end=`` or pins on those bars.
1202			dominant_7th / gravity / nir_strength / minor_turnaround_weight /
1203				root_diversity: The engine parameters, exactly as
1204				:meth:`Composition.harmony` takes them.
1205
1206		Example:
1207			```python
1208			chorus = subsequence.Progression.generate(
1209				style="aeolian_minor", bars=4, end="V", seed=7,
1210			)
1211			print(chorus)        # romans until bound
1212			```
1213		"""
1214
1215		if bars < 1:
1216			raise ValueError("bars must be at least 1")
1217
1218		if cadence is not None:
1219			pins = cadence_pins(cadence, bars, pins, end)
1220			end = None
1221
1222		if rng is None:
1223			if seed is None:
1224				warnings.warn(
1225					"Progression.generate without seed= is nondeterministic — "
1226					"pass seed= so the value survives live reload",
1227					stacklevel = 2,
1228				)
1229				rng = random.Random()
1230			else:
1231				rng = random.Random(seed)
1232
1233		resolved_scale = scale if scale is not None else _STYLE_SCALES.get(style if isinstance(style, str) else "", "ionian")
1234		relative = key is None
1235		reference = key if key is not None else "C"
1236
1237		state = subsequence.harmonic_state.HarmonicState(
1238			key_name = reference,
1239			graph_style = style,
1240			include_dominant_7th = dominant_7th,
1241			key_gravity_blend = gravity,
1242			nir_strength = nir_strength,
1243			minor_turnaround_weight = minor_turnaround_weight,
1244			root_diversity = root_diversity,
1245			rng = rng,
1246		)
1247
1248		resolved_pins = {
1249			position: resolve_constraint(spec, state.key_root_pc, resolved_scale, f"pins[{position}]")
1250			for position, spec in (pins or {}).items()
1251		}
1252		resolved_end = resolve_constraint(end, state.key_root_pc, resolved_scale, "end") if end is not None else None
1253		resolved_avoid = [resolve_constraint(spec, state.key_root_pc, resolved_scale, "avoid") for spec in (avoid or [])]
1254
1255		if 1 in resolved_pins:
1256			if resolved_pins[1] not in state.graph.nodes():
1257				raise ValueError(
1258					f"pins[1]={resolved_pins[1].name()} is not in style {style!r}'s vocabulary"
1259				)
1260			state.current_chord = resolved_pins[1]
1261
1262		def commit (chosen: subsequence.chords.Chord) -> None:
1263			state.current_chord = chosen
1264
1265		walked = subsequence.sequence_utils.constrained_walk(
1266			state.graph,
1267			state.current_chord,
1268			bars,
1269			rng = state.rng,
1270			pins = resolved_pins,
1271			end = resolved_end,
1272			avoid = resolved_avoid,
1273			weight_modifier = state._transition_weight,
1274			before_choice = state._record_transition_source,
1275			after_choice = commit,
1276		)
1277
1278		lengths = _span_lengths(beats, bars)
1279
1280		if relative:
1281			return cls(spans = tuple(
1282				ChordSpan(chord = _roman_from_chord(chord, state.key_root_pc), beats = lengths[index])
1283				for index, chord in enumerate(walked)
1284			))
1285
1286		return cls(spans = tuple(
1287			ChordSpan(chord = chord, beats = lengths[index])
1288			for index, chord in enumerate(walked)
1289		))

Generate a progression from a chord-graph walk — the hybrid generator.

Full parameter pass-through to the engine (no more throwaway default engines), plus the hybrid constraints: pins fix chords at 1-based bars, end fixes the last bar, avoid excludes chords everywhere. Constraints compile into the walk — a backward feasibility pass guarantees satisfiability before any chord is drawn (unsatisfiable constraints raise immediately), then a forward walk samples through the engine's real history-dependent weights (NIR, gravity, diversity keep their character).

Without key= the result is key-relative — the walk runs against a reference tonic and the spans store scale-proof major-relative romans, so the value prints meaningfully unbound and resolves wherever it is bound (the walk itself is key-invariant). With key= the result is concrete.

Arguments:
  • style: A chord-graph style name (or ChordGraph instance).
  • bars: How many chords to generate.
  • beats: Span length per chord — a scalar, or a list cycled.
  • key: Key for a concrete result; omit for a key-relative value.
  • scale: Scale for int constraints' quality inference (e.g. end=1). Defaults from the style (aeolian_minor → minor); explicit strings ("V", "bVII7") never need it.
  • seed: Seed for the walk. A standalone generated value without a seed warns — module-level nondeterminism breaks live reload.
  • rng: An explicit random stream (overrides seed).
  • pins: {bar: chord} — 1-based; values parse like progression elements (ints, romans, names, Chord).
  • end: The chord at the final bar — end="V" is the cadential major dominant in minor (a string because it is chromatic; no int can ask for it).
  • avoid: Chords excluded from the walk. Naming a chord outside the style's vocabulary is allowed (trivially satisfied).
  • cadence: A cadence name ("strong"/"soft"/"open"/ "fakeout", theory aliases accepted) — its formula becomes pins on the final bars, so the walk approaches the close. Conflicts with end= or pins on those bars.
  • dominant_7th / gravity / nir_strength / minor_turnaround_weight / root_diversity: The engine parameters, exactly as Composition.harmony() takes them.
Example:
chorus = subsequence.Progression.generate(
        style="aeolian_minor", bars=4, end="V", seed=7,
)
print(chorus)        # romans until bound
def extend( self, *extensions: Any, only: Optional[List[int]] = None) -> Progression:
1324	def extend (self, *extensions: typing.Any, only: typing.Optional[typing.List[int]] = None) -> "Progression":
1325
1326		"""Add chord extensions (``7``/``9``/``11``/``13``/``"sus4"``/...) to every span.
1327
1328		``only=`` restricts the spice to the given 1-based chord slots.
1329		"""
1330
1331		slots = set(range(len(self.spans))) if only is None else {_check_slot(s, len(self.spans)) for s in only}
1332
1333		spans = tuple(
1334			dataclasses.replace(span, extensions = tuple(dict.fromkeys(span.extensions + extensions)))
1335			if index in slots else span
1336			for index, span in enumerate(self.spans)
1337		)
1338
1339		return dataclasses.replace(self, spans=spans)

Add chord extensions (7/9/11/13/"sus4"/...) to every span.

only= restricts the spice to the given 1-based chord slots.

def inversions( self, spec: Union[int, List[int]]) -> Progression:
1341	def inversions (self, spec: typing.Union[int, typing.List[int]]) -> "Progression":
1342
1343		"""Set chord inversions — a single int for all spans, or a list cycled per span."""
1344
1345		values = [spec] if isinstance(spec, int) else list(spec)
1346
1347		if not values:
1348			raise ValueError("inversions list is empty — pass at least one inversion")
1349
1350		spans = tuple(
1351			dataclasses.replace(span, inversion = int(values[index % len(values)]))
1352			for index, span in enumerate(self.spans)
1353		)
1354
1355		return dataclasses.replace(self, spans=spans)

Set chord inversions — a single int for all spans, or a list cycled per span.

def spread(self, style: str) -> Progression:
1357	def spread (self, style: str) -> "Progression":
1358
1359		"""Set the voicing spread: ``"close"``, ``"open"`` (drop-2), or ``"wide"``."""
1360
1361		spans = tuple(dataclasses.replace(span, spread = None if style == "close" else style) for span in self.spans)
1362
1363		return dataclasses.replace(self, spans=spans)

Set the voicing spread: "close", "open" (drop-2), or "wide".

def over( self, bass: Union[int, str], only: Optional[List[int]] = None) -> Progression:
1365	def over (self, bass: typing.Union[int, str], only: typing.Optional[typing.List[int]] = None) -> "Progression":
1366
1367		"""Put the progression over a slash/pedal bass — *the* trance/techno move.
1368
1369		*bass* is a pitch class int, a note name (``"G"``), or ``"tonic"``.  A
1370		note name is key-independent, so it resolves to its pitch class right
1371		here; ``"tonic"`` follows the key and stays relative until the
1372		progression is resolved.  ``only=`` restricts it to the given 1-based
1373		slots (slash chords rather than a full pedal).
1374		"""
1375
1376		if isinstance(bass, str) and bass != "tonic":
1377			bass = subsequence.chords.key_name_to_pc(bass)	# note names are key-independent — resolve now
1378		elif isinstance(bass, int) and not 0 <= bass <= 11:
1379			raise ValueError(f"a bass pitch class must be 0–11, got {bass}")
1380
1381		slots = set(range(len(self.spans))) if only is None else {_check_slot(s, len(self.spans)) for s in only}
1382
1383		spans = tuple(
1384			dataclasses.replace(span, bass=bass) if index in slots else span
1385			for index, span in enumerate(self.spans)
1386		)
1387
1388		return dataclasses.replace(self, spans=spans)

Put the progression over a slash/pedal bass — the trance/techno move.

bass is a pitch class int, a note name ("G"), or "tonic". A note name is key-independent, so it resolves to its pitch class right here; "tonic" follows the key and stays relative until the progression is resolved. only= restricts it to the given 1-based slots (slash chords rather than a full pedal).

def borrow( self, slot: Union[int, List[int]]) -> Progression:
1390	def borrow (self, slot: typing.Union[int, typing.List[int]]) -> "Progression":
1391
1392		"""Borrow the chord(s) at the given 1-based slot(s) from the parallel scale.
1393
1394		Modal interchange for key-relative content: the degree re-resolves
1395		against the parallel mode (minor under a major scale and vice
1396		versa).  Concrete chords raise — there is nothing relative to borrow.
1397		"""
1398
1399		slots = {_check_slot(s, len(self.spans)) for s in ([slot] if isinstance(slot, int) else slot)}
1400
1401		spans = list(self.spans)
1402
1403		for index in slots:
1404			chord = spans[index].chord
1405			if not isinstance(chord, RomanChord):
1406				raise ValueError(
1407					f"slot {index + 1} holds a concrete chord ({spans[index].label()}) — "
1408					"borrow() needs key-relative content (an int degree or roman)"
1409				)
1410			spans[index] = dataclasses.replace(spans[index], chord = dataclasses.replace(chord, borrowed = not chord.borrowed))
1411
1412		return dataclasses.replace(self, spans=tuple(spans))

Borrow the chord(s) at the given 1-based slot(s) from the parallel scale.

Modal interchange for key-relative content: the degree re-resolves against the parallel mode (minor under a major scale and vice versa). Concrete chords raise — there is nothing relative to borrow.

def replace(self, slot: int, chord: Any) -> Progression:
1414	def replace (self, slot: int, chord: typing.Any) -> "Progression":
1415
1416		"""Replace the chord at a 1-based slot (the span keeps its beats)."""
1417
1418		index = _check_slot(slot, len(self.spans))
1419		parsed = parse_element(chord, beats = self.spans[index].beats)
1420
1421		spans = self.spans[:index] + (parsed,) + self.spans[index + 1:]
1422
1423		return dataclasses.replace(self, spans=spans)

Replace the chord at a 1-based slot (the span keeps its beats).

def cadence(self, name: str = 'strong') -> Progression:
1425	def cadence (self, name: str = "strong") -> "Progression":
1426
1427		"""Substitute a cadence formula into the tail — the close, named.
1428
1429		The final spans take the formula's chords (``"strong"`` is V→I,
1430		``"soft"`` IV→I, ``"open"`` IV→V, ``"fakeout"`` V→vi; theory names —
1431		authentic, plagal, half, deceptive — work as aliases).  Each replaced
1432		span keeps its beats; its old chord and decorations go.  Formula
1433		chords are key-relative (ints follow the bound scale's qualities,
1434		``"V"`` is the major dominant by convention), so the tail resolves
1435		wherever the progression is bound — a concrete progression becomes
1436		mixed and resolves its tail at bind time, like any roman content.
1437
1438		Example::
1439
1440			verse = subsequence.progression(["Am", "F", "C", "G"]).cadence("open")
1441			# Bound in A minor: Am F Dm E — the half close, hanging on the dominant
1442
1443		Raises:
1444			ValueError: If the cadence name is unknown, or the progression
1445				has fewer spans than the formula.
1446		"""
1447
1448		spec = subsequence.cadences.cadence_formula(name)
1449		count = len(spec.formula)
1450
1451		if len(self.spans) < count:
1452			raise ValueError(
1453				f"cadence({name!r}) substitutes the last {count} chords, but this "
1454				f"progression has only {len(self.spans)}"
1455			)
1456
1457		tail = tuple(
1458			parse_element(element, beats = span.beats)
1459			for element, span in zip(spec.formula, self.spans[-count:])
1460		)
1461
1462		return dataclasses.replace(self, spans = self.spans[:-count] + tail)

Substitute a cadence formula into the tail — the close, named.

The final spans take the formula's chords ("strong" is V→I, "soft" IV→I, "open" IV→V, "fakeout" V→vi; theory names — authentic, plagal, half, deceptive — work as aliases). Each replaced span keeps its beats; its old chord and decorations go. Formula chords are key-relative (ints follow the bound scale's qualities, "V" is the major dominant by convention), so the tail resolves wherever the progression is bound — a concrete progression becomes mixed and resolves its tail at bind time, like any roman content.

Example::

    verse = subsequence.progression(["Am", "F", "C", "G"]).cadence("open")
    # Bound in A minor: Am F Dm E — the half close, hanging on the dominant
Raises:
  • ValueError: If the cadence name is unknown, or the progression has fewer spans than the formula.
def with_rhythm( self, beats: Union[float, List[float]]) -> Progression:
1464	def with_rhythm (self, beats: typing.Union[float, typing.List[float]]) -> "Progression":
1465
1466		"""Reshape the harmonic rhythm — a scalar for all spans, or a list cycled per span."""
1467
1468		if isinstance(beats, bool):
1469			raise TypeError(f"with_rhythm takes beats or a list of beats, got bool: {beats!r}")
1470
1471		values = [float(beats)] if isinstance(beats, (int, float)) else [float(b) for b in beats]
1472
1473		if not values:
1474			raise ValueError("with_rhythm list is empty — pass at least one length")
1475
1476		spans = tuple(
1477			dataclasses.replace(span, beats = float(values[index % len(values)]))
1478			for index, span in enumerate(self.spans)
1479		)
1480
1481		return dataclasses.replace(self, spans=spans)

Reshape the harmonic rhythm — a scalar for all spans, or a list cycled per span.

def elaborate( self, depth: int = 1, seed: Optional[int] = None) -> Progression:
1483	def elaborate (self, depth: int = 1, seed: typing.Optional[int] = None) -> "Progression":
1484
1485		"""Steedman-inspired chord elaboration — approach each chord by fifths.
1486
1487		Implements the heart of Mark Steedman's generative grammar for
1488		jazz/blues chord sequences: every chord is **approached** by a chain
1489		of secondary dominants propagated backward around the cycle of fifths
1490		(Rule 3, "the perfect cadence propagated backward"), carved out of that
1491		chord's own span (Rule 1, metric subdivision).  ``depth`` is literally
1492		how many fifth-steps back the chain extends:
1493
1494		- ``depth=0`` — identity (the bare progression).
1495		- ``depth=1`` — a secondary dominant before each chord: ``[X]`` →
1496		  ``[V7/X, X]`` (e.g. a bar of C becomes G7 C).
1497		- ``depth=2`` — a secondary ii–V: ``[ii/X, V7/X, X]`` (Dm7 G7 C).
1498		- ``depth≥3`` — the chain extends (…V7/V7/X), the furthest-back chord
1499		  is made minor — the ``ii`` of *its own local dominant* (the next
1500		  link in the chain), forming a ii–V into that link, not the
1501		  target's own ii — and dominants are recoloured by **tritone
1502		  substitution** with even odds (Rule 4) for chromatic descents.
1503		  This tritone choice is the only nondeterministic part, so ``seed``
1504		  is taken (or warned) at depth ≥ 3.
1505
1506		Its flagship is the 12-bar blues with depth-per-chorus — elaborate a
1507		``"twelve_bar_blues"`` more each chorus and the ii–V turnarounds and
1508		tritone subs accumulate.
1509
1510		The progression must be **concrete** (resolved to rooted chords);
1511		the inserted dominants are computed by pitch-class arithmetic.  Each
1512		chord keeps its decorations on the final (resolved) sub-span; the
1513		inserted approach chords are bare dominant/minor sevenths.  Note that
1514		each span is divided into ``depth + 1`` equal sub-spans, so deep
1515		elaboration of a short harmonic rhythm can drop sub-spans below the
1516		harmony clock's lookahead floor — which raises at ``play()``/
1517		``render()`` if the result is bound to the global clock (it is free
1518		of that floor at the part level, ``p.progression()``).
1519
1520		Parameters:
1521			depth: Elaboration depth (≥ 0).
1522			seed: Seed for the depth-≥3 tritone-substitution choices.
1523
1524		Returns:
1525			A new :class:`Progression` with the approach chords inserted.
1526
1527		Raises:
1528			ValueError: If *depth* is negative, the progression is
1529				key-relative, or any span is a rootless
1530				:class:`PitchSet`.
1531
1532		Example:
1533			```python
1534			blues = subsequence.progression("twelve_bar_blues").resolve("C")
1535			chorus2 = blues.elaborate(2, seed=4)      # ii–V turnarounds throughout
1536			```
1537		"""
1538
1539		if depth < 0:
1540			raise ValueError("elaborate depth must be at least 0")
1541
1542		self._require_concrete("elaborate")
1543
1544		if depth == 0:
1545			return self
1546
1547		for span in self.spans:
1548			if isinstance(span.chord, PitchSet):
1549				raise ValueError("elaborate needs rooted chords — a PitchSet has no root to approach by fifths")
1550
1551		if depth >= 3 and seed is None:
1552			warnings.warn(
1553				"elaborate(depth>=3) makes tritone-substitution choices — pass seed= so the "
1554				"result survives live reload",
1555				stacklevel = 2,
1556			)
1557
1558		rng = random.Random(seed)
1559		new_spans: typing.List[ChordSpan] = []
1560
1561		for span in self.spans:
1562
1563			target_root = span.chord.root_pc
1564			sub_beats = span.beats / (depth + 1)
1565
1566			# The backward cycle-of-fifths chain, furthest-back first: chord j
1567			# sits a fifth above chord j-1's target, i.e. root = X + 7·j.  The
1568			# furthest-back (j == depth) is made minor — the ii of its OWN
1569			# local dominant (the next link), forming a ii–V into that link —
1570			# once the chain is long enough (depth >= 2) to spell one.
1571			for j in range(depth, 0, -1):
1572				root = (target_root + 7 * j) % 12
1573				quality = "minor_7th" if (j == depth and depth >= 2) else "dominant_7th"
1574
1575				# Tritone substitution recolours a dominant to the dom7 a
1576				# tritone away (same guide tones, chromatic resolution).
1577				if quality == "dominant_7th" and depth >= 3 and rng.random() < 0.5:
1578					root = (root + 6) % 12
1579
1580				new_spans.append(ChordSpan(chord = subsequence.chords.Chord(root_pc = root, quality = quality), beats = sub_beats))
1581
1582			# The target keeps its own chord and decorations, on its sub-span.
1583			new_spans.append(dataclasses.replace(span, beats = sub_beats))
1584
1585		return dataclasses.replace(self, spans = tuple(new_spans))

Steedman-inspired chord elaboration — approach each chord by fifths.

Implements the heart of Mark Steedman's generative grammar for jazz/blues chord sequences: every chord is approached by a chain of secondary dominants propagated backward around the cycle of fifths (Rule 3, "the perfect cadence propagated backward"), carved out of that chord's own span (Rule 1, metric subdivision). depth is literally how many fifth-steps back the chain extends:

  • depth=0 — identity (the bare progression).
  • depth=1 — a secondary dominant before each chord: [X][V7/X, X] (e.g. a bar of C becomes G7 C).
  • depth=2 — a secondary ii–V: [ii/X, V7/X, X] (Dm7 G7 C).
  • depth≥3 — the chain extends (…V7/V7/X), the furthest-back chord is made minor — the ii of its own local dominant (the next link in the chain), forming a ii–V into that link, not the target's own ii — and dominants are recoloured by tritone substitution with even odds (Rule 4) for chromatic descents. This tritone choice is the only nondeterministic part, so seed is taken (or warned) at depth ≥ 3.

Its flagship is the 12-bar blues with depth-per-chorus — elaborate a "twelve_bar_blues" more each chorus and the ii–V turnarounds and tritone subs accumulate.

The progression must be concrete (resolved to rooted chords); the inserted dominants are computed by pitch-class arithmetic. Each chord keeps its decorations on the final (resolved) sub-span; the inserted approach chords are bare dominant/minor sevenths. Note that each span is divided into depth + 1 equal sub-spans, so deep elaboration of a short harmonic rhythm can drop sub-spans below the harmony clock's lookahead floor — which raises at play()/ render() if the result is bound to the global clock (it is free of that floor at the part level, p.progression()).

Arguments:
  • depth: Elaboration depth (≥ 0).
  • seed: Seed for the depth-≥3 tritone-substitution choices.
Returns:

A new Progression with the approach chords inserted.

Raises:
  • ValueError: If depth is negative, the progression is key-relative, or any span is a rootless PitchSet.
Example:
blues = subsequence.progression("twelve_bar_blues").resolve("C")
chorus2 = blues.elaborate(2, seed=4)      # ii–V turnarounds throughout
def describe( self, key: Union[int, str, NoneType] = None, scale: str = 'ionian') -> str:
1589	def describe (self, key: typing.Optional[typing.Union[str, int]] = None, scale: str = "ionian") -> str:
1590
1591		"""A readable, one-chord-per-line summary.
1592
1593		Key-relative spans print as written (romans/degrees) when unbound,
1594		and as concrete chord names under a *key*.
1595		"""
1596
1597		key_pc = None if key is None else (key if isinstance(key, int) else subsequence.chords.key_name_to_pc(key))
1598
1599		lines = [f"Progression — {len(self.spans)} chords over {self.length:g} beats"]
1600		cursor = 0.0
1601
1602		for span in self.spans:
1603			lines.append(
1604				f"  {cursor:6.2f}{cursor + span.beats:6.2f}   "
1605				f"{span.label(key_pc, scale):<8} ({span.beats:g} beats)"
1606			)
1607			cursor += span.beats
1608
1609		return "\n".join(lines)

A readable, one-chord-per-line summary.

Key-relative spans print as written (romans/degrees) when unbound, and as concrete chord names under a key.

@dataclasses.dataclass(frozen=True)
class ChordSpan:
483@dataclasses.dataclass(frozen=True)
484class ChordSpan:
485
486	"""One chord with a duration and its decoration — the unit of harmonic time.
487
488	Decoration (extensions, slash bass, inversion, spread) lives HERE, never
489	on :class:`~subsequence.chords.Chord`: the engine's graph identity stays
490	the bare triad, and the decorated voicing is what patterns hear.
491
492	Attributes:
493		chord: A concrete ``Chord``, a key-relative :class:`RomanChord`, or a
494			:class:`PitchSet`.
495		beats: Span length in beats.
496		extensions: Extension markers — ints (``7``, ``9``, ``11``, ``13``)
497			or names (``"sus2"``, ``"sus4"``, ``"add9"``, ``"6"``).
498		bass: Slash/pedal bass — a pitch class int, a note name, or
499			``"tonic"`` (resolved against the key at query time).
500		inversion: Chord inversion for the voicing (0 = root position).
501		spread: Voicing spread — ``"close"`` (default), ``"open"`` (drop-2),
502			or ``"wide"`` (drop-2-and-4).
503		extension_intervals: Pre-computed semitone offsets for the
504			extensions, set by :meth:`Progression.resolve` for diatonic
505			degrees.  ``None`` means "derive from the chord's own colour".
506	"""
507
508	chord: typing.Any
509	beats: float
510	extensions: typing.Tuple[typing.Any, ...] = ()
511	bass: typing.Optional[typing.Union[int, str]] = None
512	inversion: int = 0
513	spread: typing.Optional[str] = None
514	extension_intervals: typing.Optional[typing.Tuple[int, ...]] = None
515
516	def __post_init__ (self) -> None:
517
518		"""Validate beats, extensions, and spread."""
519
520		if self.beats <= 0:
521			raise ValueError(f"a chord span must last at least one beat-fraction, got {self.beats:g}")
522
523		for extension in self.extensions:
524			if isinstance(extension, bool) or not (
525				(isinstance(extension, int) and extension in _NUMERIC_EXTENSIONS)
526				or (isinstance(extension, str) and extension in _EXTENSION_NAMES)
527			):
528				known = ", ".join(["7", "9", "11", "13"] + sorted(_EXTENSION_NAMES))
529				raise ValueError(f"unknown extension {extension!r} — expected one of: {known}")
530
531		if self.spread is not None and self.spread not in _SPREAD_STYLES:
532			raise ValueError(f"unknown spread {self.spread!r} — expected one of: " + ", ".join(sorted(_SPREAD_STYLES)))
533
534	@property
535	def is_concrete (self) -> bool:
536
537		"""True when the chord (and any pedal bass) needs no key context to sound.
538
539		A ``"tonic"`` pedal bass is key-relative, so a span carrying one is not
540		concrete until :meth:`resolve` pins it to a key.  Note-name basses are
541		resolved to a pitch class eagerly in :meth:`Progression.over`, so they
542		never linger here as strings.
543		"""
544
545		return not isinstance(self.chord, RomanChord) and not isinstance(self.bass, str)
546
547	@property
548	def is_decorated (self) -> bool:
549
550		"""True when the span carries any decoration beyond the bare chord."""
551
552		return bool(self.extensions) or self.bass is not None or self.inversion != 0 or self.spread is not None
553
554	def resolve (self, key_pc: int, scale: str = "ionian") -> "ChordSpan":
555
556		"""Return a concrete span: romans resolved, bass resolved to a pitch class."""
557
558		chord = self.chord
559		extension_intervals = self.extension_intervals
560
561		if isinstance(chord, RomanChord):
562			if chord.quality is None and any(isinstance(e, int) for e in self.extensions):
563				extension_intervals = chord.diatonic_extension_intervals(key_pc, scale, self.extensions)
564			chord = chord.resolve(key_pc, scale)
565
566		bass: typing.Optional[typing.Union[int, str]] = self.bass
567
568		if isinstance(bass, str):
569			if bass == "tonic":
570				bass = key_pc
571			else:
572				bass = subsequence.chords.key_name_to_pc(bass)
573
574		return dataclasses.replace(
575			self,
576			chord = chord,
577			bass = bass,
578			extension_intervals = extension_intervals,
579		)
580
581	def label (self, key_pc: typing.Optional[int] = None, scale: str = "ionian") -> str:
582
583		"""A printable chord label: roman text when relative, decorated name when concrete."""
584
585		if isinstance(self.chord, RomanChord):
586			if key_pc is None:
587				text = self.chord.label()
588				return text + self._decoration_suffix(resolved=False)
589			return self.resolve(key_pc, scale).label()
590
591		base = str(self.chord.name())
592		return base + self._decoration_suffix(resolved=True)
593
594	def _decoration_suffix (self, resolved: bool) -> str:
595
596		"""The printable decoration tail (extensions and slash bass)."""
597
598		parts = ""
599		numeric = sorted(e for e in self.extensions if isinstance(e, int))
600
601		# 9 implies 7 (and so on up): print only the highest stacked extension.
602		stacked = [e for e in numeric if e in (7, 9, 11, 13)]
603		if stacked:
604			parts += str(stacked[-1])
605
606		for name in (e for e in self.extensions if isinstance(e, str)):
607			parts += name
608
609		if self.bass is not None:
610			if isinstance(self.bass, int):
611				parts += "/" + subsequence.chords.PC_TO_NOTE_NAME[self.bass % 12]
612			else:
613				parts += "/" + str(self.bass)
614
615		return parts
616
617	def decorated_intervals (self) -> typing.List[int]:
618
619		"""Semitone offsets of the decorated voicing (before inversion/spread/bass).
620
621		Numeric extensions deepen the chord in its own colour — a minor third
622		gets a minor seventh, a major third a major seventh, a diminished
623		triad a diminished seventh.  Diatonic degrees extended with
624		``extend(...)`` carry pre-computed scale-true intervals instead (so V
625		gets its dominant seventh).  Write ``"G7"``/``"V7"`` when you want the
626		dominant colour on a concrete major chord.
627		"""
628
629		if isinstance(self.chord, RomanChord):
630			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
631
632		intervals = list(self.chord.intervals())
633
634		sus = [e for e in self.extensions if e in ("sus2", "sus4")]
635		if sus and len(intervals) >= 2:
636			intervals[1] = 2 if sus[0] == "sus2" else 5
637
638		numeric = sorted(e for e in self.extensions if isinstance(e, int))
639
640		if self.extension_intervals is not None:
641			added: typing.List[int] = list(self.extension_intervals)
642		else:
643			added = []
644			third = intervals[1] if len(intervals) >= 2 else None
645			has_seventh = any(i in (9, 10, 11) for i in intervals)
646			stacked = [e for e in numeric if e in _NUMERIC_EXTENSIONS]
647
648			if stacked and not has_seventh:
649				if third == 3 and len(intervals) >= 3 and intervals[2] == 6:
650					added.append(9)		# diminished colour
651				elif third == 3:
652					added.append(10)	# minor colour
653				elif third == 4:
654					added.append(11)	# major colour
655				else:
656					added.append(10)	# sus / no third: the dominant-leaning seventh
657
658			for extension in stacked:
659				if extension == 9:
660					added.append(14)
661				elif extension == 11:
662					added.append(17)
663				elif extension == 13:
664					added.append(21)
665
666		if "add9" in self.extensions:
667			added.append(14)
668		if "6" in self.extensions:
669			added.append(9)
670
671		return sorted(set(intervals) | set(added))
672
673	def tones (self, root: int = 60, count: typing.Optional[int] = None) -> typing.List[int]:
674
675		"""MIDI notes of the decorated voicing nearest *root* (concrete spans only).
676
677		Applies, in order: extensions, inversion, spread, then the slash/pedal
678		bass below the voicing.  ``PitchSet`` spans return their absolute
679		pitches (decoration other than ``count`` does not apply).
680		"""
681
682		if isinstance(self.chord, RomanChord):
683			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
684
685		if isinstance(self.chord, PitchSet):
686			return self.chord.tones(root, inversion=self.inversion, count=count)
687
688		intervals = self.decorated_intervals()
689
690		if self.inversion != 0:
691			intervals = subsequence.voicings.invert_chord(intervals, self.inversion)
692
693		if self.spread == "open" and len(intervals) >= 3:
694			intervals = sorted(intervals[:-2] + [intervals[-2] - 12] + intervals[-1:])
695		elif self.spread == "wide" and len(intervals) >= 3:
696			dropped = [i - 12 if position in (len(intervals) - 2, len(intervals) - 4) else i for position, i in enumerate(intervals)]
697			intervals = sorted(dropped)
698
699		offset = (self.chord.root_pc - root) % 12
700		if offset > 6:
701			offset -= 12
702		effective_root = root + offset
703
704		if count is not None:
705			n = len(intervals)
706			span_octave = max(12, ((max(intervals) // 12) + 1) * 12)
707			pitches = [effective_root + intervals[i % n] + span_octave * (i // n) for i in range(count)]
708		else:
709			pitches = [effective_root + interval for interval in intervals]
710
711		if self.bass is not None and isinstance(self.bass, int):
712			lowest = min(pitches)
713			bass_note = lowest - ((lowest - self.bass) % 12)
714			if bass_note == lowest:
715				bass_note -= 12
716			pitches = [bass_note] + pitches
717
718		return pitches

One chord with a duration and its decoration — the unit of harmonic time.

Decoration (extensions, slash bass, inversion, spread) lives HERE, never on ~subsequence.chords.Chord: the engine's graph identity stays the bare triad, and the decorated voicing is what patterns hear.

Attributes:
  • chord: A concrete Chord, a key-relative RomanChord, or a PitchSet.
  • beats: Span length in beats.
  • extensions: Extension markers — ints (7, 9, 11, 13) or names ("sus2", "sus4", "add9", "6").
  • bass: Slash/pedal bass — a pitch class int, a note name, or "tonic" (resolved against the key at query time).
  • inversion: Chord inversion for the voicing (0 = root position).
  • spread: Voicing spread — "close" (default), "open" (drop-2), or "wide" (drop-2-and-4).
  • extension_intervals: Pre-computed semitone offsets for the extensions, set by Progression.resolve() for diatonic degrees. None means "derive from the chord's own colour".
ChordSpan( chord: Any, beats: float, extensions: Tuple[Any, ...] = (), bass: Union[int, str, NoneType] = None, inversion: int = 0, spread: Optional[str] = None, extension_intervals: Optional[Tuple[int, ...]] = None)
chord: Any
beats: float
extensions: Tuple[Any, ...] = ()
bass: Union[int, str, NoneType] = None
inversion: int = 0
spread: Optional[str] = None
extension_intervals: Optional[Tuple[int, ...]] = None
is_concrete: bool
534	@property
535	def is_concrete (self) -> bool:
536
537		"""True when the chord (and any pedal bass) needs no key context to sound.
538
539		A ``"tonic"`` pedal bass is key-relative, so a span carrying one is not
540		concrete until :meth:`resolve` pins it to a key.  Note-name basses are
541		resolved to a pitch class eagerly in :meth:`Progression.over`, so they
542		never linger here as strings.
543		"""
544
545		return not isinstance(self.chord, RomanChord) and not isinstance(self.bass, str)

True when the chord (and any pedal bass) needs no key context to sound.

A "tonic" pedal bass is key-relative, so a span carrying one is not concrete until resolve() pins it to a key. Note-name basses are resolved to a pitch class eagerly in Progression.over(), so they never linger here as strings.

is_decorated: bool
547	@property
548	def is_decorated (self) -> bool:
549
550		"""True when the span carries any decoration beyond the bare chord."""
551
552		return bool(self.extensions) or self.bass is not None or self.inversion != 0 or self.spread is not None

True when the span carries any decoration beyond the bare chord.

def resolve( self, key_pc: int, scale: str = 'ionian') -> ChordSpan:
554	def resolve (self, key_pc: int, scale: str = "ionian") -> "ChordSpan":
555
556		"""Return a concrete span: romans resolved, bass resolved to a pitch class."""
557
558		chord = self.chord
559		extension_intervals = self.extension_intervals
560
561		if isinstance(chord, RomanChord):
562			if chord.quality is None and any(isinstance(e, int) for e in self.extensions):
563				extension_intervals = chord.diatonic_extension_intervals(key_pc, scale, self.extensions)
564			chord = chord.resolve(key_pc, scale)
565
566		bass: typing.Optional[typing.Union[int, str]] = self.bass
567
568		if isinstance(bass, str):
569			if bass == "tonic":
570				bass = key_pc
571			else:
572				bass = subsequence.chords.key_name_to_pc(bass)
573
574		return dataclasses.replace(
575			self,
576			chord = chord,
577			bass = bass,
578			extension_intervals = extension_intervals,
579		)

Return a concrete span: romans resolved, bass resolved to a pitch class.

def label(self, key_pc: Optional[int] = None, scale: str = 'ionian') -> str:
581	def label (self, key_pc: typing.Optional[int] = None, scale: str = "ionian") -> str:
582
583		"""A printable chord label: roman text when relative, decorated name when concrete."""
584
585		if isinstance(self.chord, RomanChord):
586			if key_pc is None:
587				text = self.chord.label()
588				return text + self._decoration_suffix(resolved=False)
589			return self.resolve(key_pc, scale).label()
590
591		base = str(self.chord.name())
592		return base + self._decoration_suffix(resolved=True)

A printable chord label: roman text when relative, decorated name when concrete.

def decorated_intervals(self) -> List[int]:
617	def decorated_intervals (self) -> typing.List[int]:
618
619		"""Semitone offsets of the decorated voicing (before inversion/spread/bass).
620
621		Numeric extensions deepen the chord in its own colour — a minor third
622		gets a minor seventh, a major third a major seventh, a diminished
623		triad a diminished seventh.  Diatonic degrees extended with
624		``extend(...)`` carry pre-computed scale-true intervals instead (so V
625		gets its dominant seventh).  Write ``"G7"``/``"V7"`` when you want the
626		dominant colour on a concrete major chord.
627		"""
628
629		if isinstance(self.chord, RomanChord):
630			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
631
632		intervals = list(self.chord.intervals())
633
634		sus = [e for e in self.extensions if e in ("sus2", "sus4")]
635		if sus and len(intervals) >= 2:
636			intervals[1] = 2 if sus[0] == "sus2" else 5
637
638		numeric = sorted(e for e in self.extensions if isinstance(e, int))
639
640		if self.extension_intervals is not None:
641			added: typing.List[int] = list(self.extension_intervals)
642		else:
643			added = []
644			third = intervals[1] if len(intervals) >= 2 else None
645			has_seventh = any(i in (9, 10, 11) for i in intervals)
646			stacked = [e for e in numeric if e in _NUMERIC_EXTENSIONS]
647
648			if stacked and not has_seventh:
649				if third == 3 and len(intervals) >= 3 and intervals[2] == 6:
650					added.append(9)		# diminished colour
651				elif third == 3:
652					added.append(10)	# minor colour
653				elif third == 4:
654					added.append(11)	# major colour
655				else:
656					added.append(10)	# sus / no third: the dominant-leaning seventh
657
658			for extension in stacked:
659				if extension == 9:
660					added.append(14)
661				elif extension == 11:
662					added.append(17)
663				elif extension == 13:
664					added.append(21)
665
666		if "add9" in self.extensions:
667			added.append(14)
668		if "6" in self.extensions:
669			added.append(9)
670
671		return sorted(set(intervals) | set(added))

Semitone offsets of the decorated voicing (before inversion/spread/bass).

Numeric extensions deepen the chord in its own colour — a minor third gets a minor seventh, a major third a major seventh, a diminished triad a diminished seventh. Diatonic degrees extended with extend(...) carry pre-computed scale-true intervals instead (so V gets its dominant seventh). Write "G7"/"V7" when you want the dominant colour on a concrete major chord.

def tones(self, root: int = 60, count: Optional[int] = None) -> List[int]:
673	def tones (self, root: int = 60, count: typing.Optional[int] = None) -> typing.List[int]:
674
675		"""MIDI notes of the decorated voicing nearest *root* (concrete spans only).
676
677		Applies, in order: extensions, inversion, spread, then the slash/pedal
678		bass below the voicing.  ``PitchSet`` spans return their absolute
679		pitches (decoration other than ``count`` does not apply).
680		"""
681
682		if isinstance(self.chord, RomanChord):
683			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
684
685		if isinstance(self.chord, PitchSet):
686			return self.chord.tones(root, inversion=self.inversion, count=count)
687
688		intervals = self.decorated_intervals()
689
690		if self.inversion != 0:
691			intervals = subsequence.voicings.invert_chord(intervals, self.inversion)
692
693		if self.spread == "open" and len(intervals) >= 3:
694			intervals = sorted(intervals[:-2] + [intervals[-2] - 12] + intervals[-1:])
695		elif self.spread == "wide" and len(intervals) >= 3:
696			dropped = [i - 12 if position in (len(intervals) - 2, len(intervals) - 4) else i for position, i in enumerate(intervals)]
697			intervals = sorted(dropped)
698
699		offset = (self.chord.root_pc - root) % 12
700		if offset > 6:
701			offset -= 12
702		effective_root = root + offset
703
704		if count is not None:
705			n = len(intervals)
706			span_octave = max(12, ((max(intervals) // 12) + 1) * 12)
707			pitches = [effective_root + intervals[i % n] + span_octave * (i // n) for i in range(count)]
708		else:
709			pitches = [effective_root + interval for interval in intervals]
710
711		if self.bass is not None and isinstance(self.bass, int):
712			lowest = min(pitches)
713			bass_note = lowest - ((lowest - self.bass) % 12)
714			if bass_note == lowest:
715				bass_note -= 12
716			pitches = [bass_note] + pitches
717
718		return pitches

MIDI notes of the decorated voicing nearest root (concrete spans only).

Applies, in order: extensions, inversion, spread, then the slash/pedal bass below the voicing. PitchSet spans return their absolute pitches (decoration other than count does not apply).

@dataclasses.dataclass(frozen=True)
class PitchSet:
113@dataclasses.dataclass(frozen=True)
114class PitchSet:
115
116	"""A nameless sonority — a frozen set of absolute MIDI pitches.
117
118	The escape hatch for chords with no root or quality: clusters, spectral
119	stacks, found objects.  It duck-types ``.tones()`` so every placement verb
120	and the injected ``chord`` accept it unchanged.  By design it is excluded
121	from generation and diatonic spice (there is nothing to transpose
122	diatonically), and a progression containing one loops on exhaustion
123	rather than falling through to live graph stepping.
124
125	Pitches are absolute: ``tones()`` ignores its ``root`` argument — you
126	chose the register when you chose the pitches.
127	"""
128
129	pitches: typing.Tuple[int, ...]
130
131	def __init__ (self, pitches: typing.Iterable[int]) -> None:
132
133		"""Normalise any iterable of MIDI pitches into a sorted frozen tuple."""
134
135		values = tuple(sorted(int(p) for p in pitches))
136
137		if not values:
138			raise ValueError("PitchSet needs at least one pitch")
139
140		object.__setattr__(self, "pitches", values)
141
142	def tones (self, root: int = 60, inversion: int = 0, count: typing.Optional[int] = None) -> typing.List[int]:
143
144		"""Return the pitches (absolute — *root* is ignored by design).
145
146		``inversion`` rotates pitches up an octave; ``count`` cycles the set
147		into higher octaves, matching the ``Chord.tones`` contract.
148		"""
149
150		pitches = list(self.pitches)
151
152		if inversion != 0:
153			for _ in range(inversion % len(pitches)):
154				pitches.append(pitches.pop(0) + 12)
155
156		if count is not None:
157			n = len(pitches)
158			return [pitches[i % n] + 12 * (i // n) for i in range(count)]
159
160		return pitches
161
162	def intervals (self) -> typing.List[int]:
163
164		"""Semitone offsets from the lowest pitch (the ``Chord`` protocol)."""
165
166		return [p - self.pitches[0] for p in self.pitches]
167
168	def name (self) -> str:
169
170		"""A readable label for describe() output."""
171
172		return "PitchSet(" + ", ".join(str(p) for p in self.pitches) + ")"

A nameless sonority — a frozen set of absolute MIDI pitches.

The escape hatch for chords with no root or quality: clusters, spectral stacks, found objects. It duck-types .tones() so every placement verb and the injected chord accept it unchanged. By design it is excluded from generation and diatonic spice (there is nothing to transpose diatonically), and a progression containing one loops on exhaustion rather than falling through to live graph stepping.

Pitches are absolute: tones() ignores its root argument — you chose the register when you chose the pitches.

PitchSet(pitches: Iterable[int])
131	def __init__ (self, pitches: typing.Iterable[int]) -> None:
132
133		"""Normalise any iterable of MIDI pitches into a sorted frozen tuple."""
134
135		values = tuple(sorted(int(p) for p in pitches))
136
137		if not values:
138			raise ValueError("PitchSet needs at least one pitch")
139
140		object.__setattr__(self, "pitches", values)

Normalise any iterable of MIDI pitches into a sorted frozen tuple.

pitches: Tuple[int, ...]
def tones( self, root: int = 60, inversion: int = 0, count: Optional[int] = None) -> List[int]:
142	def tones (self, root: int = 60, inversion: int = 0, count: typing.Optional[int] = None) -> typing.List[int]:
143
144		"""Return the pitches (absolute — *root* is ignored by design).
145
146		``inversion`` rotates pitches up an octave; ``count`` cycles the set
147		into higher octaves, matching the ``Chord.tones`` contract.
148		"""
149
150		pitches = list(self.pitches)
151
152		if inversion != 0:
153			for _ in range(inversion % len(pitches)):
154				pitches.append(pitches.pop(0) + 12)
155
156		if count is not None:
157			n = len(pitches)
158			return [pitches[i % n] + 12 * (i // n) for i in range(count)]
159
160		return pitches

Return the pitches (absolute — root is ignored by design).

inversion rotates pitches up an octave; count cycles the set into higher octaves, matching the Chord.tones contract.

def intervals(self) -> List[int]:
162	def intervals (self) -> typing.List[int]:
163
164		"""Semitone offsets from the lowest pitch (the ``Chord`` protocol)."""
165
166		return [p - self.pitches[0] for p in self.pitches]

Semitone offsets from the lowest pitch (the Chord protocol).

def name(self) -> str:
168	def name (self) -> str:
169
170		"""A readable label for describe() output."""
171
172		return "PitchSet(" + ", ".join(str(p) for p in self.pitches) + ")"

A readable label for describe() output.

def progression( source: Optional[Any] = None, beats: Union[float, List[float]] = 4.0, *, style: Optional[str] = None, bars: int = 8, key: Optional[str] = None, scale: Optional[str] = None, seed: Optional[int] = None, rng: Optional[random.Random] = None, pins: Optional[Dict[int, Any]] = None, end: Optional[Any] = None, avoid: Optional[Sequence[Any]] = None, cadence: Optional[str] = None, dominant_7th: bool = True, gravity: float = 1.0, nir_strength: float = 0.5, minor_turnaround_weight: float = 0.0, root_diversity: float = 0.4) -> Progression:
1641def progression (
1642	source: typing.Optional[typing.Any] = None,
1643	beats: typing.Union[float, typing.List[float]] = DEFAULT_SPAN_BEATS,
1644	*,
1645	style: typing.Optional[str] = None,
1646	bars: int = 8,
1647	key: typing.Optional[str] = None,
1648	scale: typing.Optional[str] = None,
1649	seed: typing.Optional[int] = None,
1650	rng: typing.Optional[random.Random] = None,
1651	pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
1652	end: typing.Optional[typing.Any] = None,
1653	avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
1654	cadence: typing.Optional[str] = None,
1655	dominant_7th: bool = True,
1656	gravity: float = 1.0,
1657	nir_strength: float = 0.5,
1658	minor_turnaround_weight: float = 0.0,
1659	root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1660) -> Progression:
1661
1662	"""Build a :class:`Progression` — the lowercase factory.
1663
1664	Dispatch by argument type: a **list** parses per element (ints where
1665	diatonic, name/roman strings where nominal/chromatic,
1666	``(element, beats)`` tuples for per-chord durations); a bare **string** names a
1667	preset from the curated table; ``style=`` generates *bars* chords from a
1668	chord-graph walk (requires ``key=``).
1669
1670	Parameters:
1671		source: The element list, preset name, or an existing Progression
1672			(returned unchanged).
1673		beats: Span length per chord — a scalar, or a list cycled per chord
1674			(``beats=[4, 4, 2, 6]`` shapes the harmonic rhythm).
1675		style: A chord-graph style name to generate from (e.g.
1676			``"aeolian_minor"``).
1677		bars: How many chords to generate (style mode only).
1678		key: Key for style generation.
1679		seed: Seed for style generation.  A standalone generated value
1680			without a seed warns — module-level nondeterminism breaks live
1681			reload.
1682		rng: An explicit random stream (overrides ``seed``; used by
1683			engine-mediated calls).
1684		dominant_7th / gravity / nir_strength: Graph-walk parameters,
1685			matching :meth:`Composition.harmony` (style mode only; full
1686			pass-through arrives with ``Progression.generate``).
1687
1688	Example:
1689		```python
1690		verse = subsequence.progression([1, 6, 3, 7])           # i–VI–III–VII in A minor
1691		blues = subsequence.progression(["I7"] * 4 + ["IV7", "IV7", "I7", "I7", "V7", "IV7", "I7", "I7"])
1692		walk  = subsequence.progression(style="aeolian_minor", key="A", bars=8, seed=3)
1693		```
1694	"""
1695
1696	if style is not None:
1697		if source is not None:
1698			raise ValueError("pass either source or style=, not both")
1699		return Progression.generate(
1700			style = style,
1701			bars = bars,
1702			beats = beats,
1703			key = key,
1704			scale = scale,
1705			seed = seed,
1706			rng = rng,
1707			pins = pins,
1708			end = end,
1709			avoid = avoid,
1710			cadence = cadence,
1711			dominant_7th = dominant_7th,
1712			gravity = gravity,
1713			nir_strength = nir_strength,
1714			minor_turnaround_weight = minor_turnaround_weight,
1715			root_diversity = root_diversity,
1716		)
1717
1718	# Generation-only knobs are meaningless for a concrete source — reject
1719	# them so a musician asking for cadence= or key= on a list gets a usable
1720	# error instead of a silent no-op.
1721	generation_only = {
1722		"bars": bars != 8,
1723		"key": key is not None,
1724		"scale": scale is not None,
1725		"seed": seed is not None,
1726		"rng": rng is not None,
1727		"pins": pins is not None,
1728		"end": end is not None,
1729		"avoid": avoid is not None,
1730		"cadence": cadence is not None,
1731		"dominant_7th": dominant_7th is not True,
1732		"gravity": gravity != 1.0,
1733		"nir_strength": nir_strength != 0.5,
1734		"minor_turnaround_weight": minor_turnaround_weight != 0.0,
1735		"root_diversity": root_diversity != subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1736	}
1737	passed = [name for name, was_set in generation_only.items() if was_set]
1738
1739	if passed:
1740		raise ValueError(
1741			f"{', '.join(sorted(passed))} only apply when generating with style=. "
1742			"A concrete progression takes these as methods instead — e.g. "
1743			".cadence('strong') for the close, and the key binds at "
1744			"composition.harmony() / resolve() time."
1745		)
1746
1747	if isinstance(source, Progression):
1748		return source
1749
1750	if isinstance(source, str):
1751		if source in _PRESETS:
1752			return progression(_PRESETS[source], beats=beats)
1753		known = ", ".join(sorted(_PRESETS))
1754		raise ValueError(
1755			f"Unknown progression preset {source!r}. Known presets: {known}. "
1756			"Or pass a list — progression([1, 6, 3, 7]) / progression(['Am', 'F', 'C', 'G'])."
1757		)
1758
1759	if source is None:
1760		raise ValueError("progression() needs a source list (or style=...)")
1761
1762	elements = list(source)
1763
1764	if not elements:
1765		raise ValueError("progression list is empty — pass at least one chord")
1766
1767	lengths = _span_lengths(beats, len(elements))
1768
1769	return Progression(spans = tuple(
1770		parse_element(element, beats=lengths[index])
1771		for index, element in enumerate(elements)
1772	))

Build a Progression — the lowercase factory.

Dispatch by argument type: a list parses per element (ints where diatonic, name/roman strings where nominal/chromatic, (element, beats) tuples for per-chord durations); a bare string names a preset from the curated table; style= generates bars chords from a chord-graph walk (requires key=).

Arguments:
  • source: The element list, preset name, or an existing Progression (returned unchanged).
  • beats: Span length per chord — a scalar, or a list cycled per chord (beats=[4, 4, 2, 6] shapes the harmonic rhythm).
  • style: A chord-graph style name to generate from (e.g. "aeolian_minor").
  • bars: How many chords to generate (style mode only).
  • key: Key for style generation.
  • seed: Seed for style generation. A standalone generated value without a seed warns — module-level nondeterminism breaks live reload.
  • rng: An explicit random stream (overrides seed; used by engine-mediated calls).
  • dominant_7th / gravity / nir_strength: Graph-walk parameters, matching Composition.harmony() (style mode only; full pass-through arrives with Progression.generate).
Example:
verse = subsequence.progression([1, 6, 3, 7])           # i–VI–III–VII in A minor
blues = subsequence.progression(["I7"] * 4 + ["IV7", "IV7", "I7", "I7", "V7", "IV7", "I7", "I7"])
walk  = subsequence.progression(style="aeolian_minor", key="A", bars=8, seed=3)
@dataclasses.dataclass(frozen=True)
class Chord:
124@dataclasses.dataclass(frozen=True)
125class Chord:
126
127	"""
128	Represents a chord as a root pitch class and quality.
129	"""
130
131	root_pc: int
132	quality: str
133
134
135	def intervals (self) -> typing.List[int]:
136
137		"""
138		Return the chord intervals for this chord quality.
139		"""
140
141		if self.quality not in CHORD_INTERVALS:
142			raise ValueError(f"Unknown chord quality: {self.quality}")
143
144		return CHORD_INTERVALS[self.quality]
145
146
147
148	def tones (self, root: int, inversion: int = 0, count: typing.Optional[int] = None) -> typing.List[int]:
149
150		"""Return MIDI note numbers for chord tones starting from a root.
151
152		Finds the MIDI note corresponding to the chord's root pitch class that is
153		closest to the provided ``root`` argument.
154
155		Parameters:
156			root: MIDI note number (e.g., 60 = middle C) to center the chord around.
157			inversion: Chord inversion (0 = root position, 1 = first, 2 = second, ...).
158				Wraps around for values >= number of notes.
159			count: Number of notes to return. When set, the chord intervals cycle
160				into higher octaves until ``count`` notes are produced. When ``None``
161				(default), returns the natural chord tones.
162
163		Returns:
164			List of MIDI note numbers for chord tones
165
166		Example:
167			```python
168			chord = Chord(root_pc=0, quality="major")  # C major
169			chord.tones(root=60)               # [60, 64, 67] - root position around C4
170			chord.tones(root=62)               # [60, 64, 67] - still finds C4 as closest root
171			chord.tones(root=70)               # [72, 76, 79] - finds C5 as closest root
172			```
173		"""
174
175		# Find the MIDI note for self.root_pc that is closest to the requested root.
176		# This handles octaves automatically.
177		offset = (self.root_pc - root) % 12
178		if offset > 6:
179			offset -= 12
180
181		effective_root = root + offset
182
183		intervals = self.intervals()
184
185		if inversion != 0:
186			intervals = subsequence.voicings.invert_chord(intervals, inversion)
187
188		if count is not None:
189			n = len(intervals)
190			return [effective_root + intervals[i % n] + 12 * (i // n) for i in range(count)]
191
192		return [effective_root + interval for interval in intervals]
193
194
195	def root_note (self, root_midi: int) -> int:
196
197		"""
198		Return the MIDI note number for the chord root nearest to *root_midi*.
199
200		This is equivalent to ``self.tones(root_midi)[0]`` but makes intent
201		explicit when you only need the single root pitch.
202
203		Parameters:
204			root_midi: Reference MIDI note number used to find the closest octave
205			           of this chord's root pitch class.
206
207		Returns:
208			MIDI note number of the chord root.
209
210		Example:
211			```python
212			chord = Chord(root_pc=4, quality="major")  # E major
213			chord.root_note(60)   # → 64  (E4, nearest to C4)
214			chord.root_note(69)   # → 64  (E4, nearest to A4)
215			```
216		"""
217
218		return self.tones(root_midi)[0]
219
220
221	def bass_note (self, root_midi: int, octave_offset: int = -1) -> int:
222
223		"""
224		Return the chord root shifted by a number of octaves.
225
226		Commonly used to produce a bass register note one or two octaves
227		below the chord voicing.
228
229		Parameters:
230			root_midi: Reference MIDI note number (passed to :meth:`root_note`).
231			octave_offset: Octaves to shift; negative moves down (default ``-1``).
232
233		Returns:
234			MIDI note number of the chord root in the target register.
235
236		Example:
237			```python
238			chord = Chord(root_pc=4, quality="major")  # E major
239			chord.bass_note(64)        # → 52  (E3, one octave down from E4)
240			chord.bass_note(64, -2)    # → 40  (E2, two octaves down)
241			```
242		"""
243
244		return self.root_note(root_midi) + (12 * octave_offset)
245
246
247	def name (self) -> str:
248
249		"""
250		Return a human-friendly chord name.
251
252		A registered quality without a suffix prints as ``root(quality)``
253		(e.g. ``"C(quartal)"``) rather than masquerading as a plain major.
254		"""
255
256		root_name = PC_TO_NOTE_NAME[self.root_pc % 12]
257
258		if self.quality not in CHORD_SUFFIX:
259			return f"{root_name}({self.quality})"
260
261		return f"{root_name}{CHORD_SUFFIX[self.quality]}"

Represents a chord as a root pitch class and quality.

Chord(root_pc: int, quality: str)
root_pc: int
quality: str
def intervals(self) -> List[int]:
135	def intervals (self) -> typing.List[int]:
136
137		"""
138		Return the chord intervals for this chord quality.
139		"""
140
141		if self.quality not in CHORD_INTERVALS:
142			raise ValueError(f"Unknown chord quality: {self.quality}")
143
144		return CHORD_INTERVALS[self.quality]

Return the chord intervals for this chord quality.

def tones( self, root: int, inversion: int = 0, count: Optional[int] = None) -> List[int]:
148	def tones (self, root: int, inversion: int = 0, count: typing.Optional[int] = None) -> typing.List[int]:
149
150		"""Return MIDI note numbers for chord tones starting from a root.
151
152		Finds the MIDI note corresponding to the chord's root pitch class that is
153		closest to the provided ``root`` argument.
154
155		Parameters:
156			root: MIDI note number (e.g., 60 = middle C) to center the chord around.
157			inversion: Chord inversion (0 = root position, 1 = first, 2 = second, ...).
158				Wraps around for values >= number of notes.
159			count: Number of notes to return. When set, the chord intervals cycle
160				into higher octaves until ``count`` notes are produced. When ``None``
161				(default), returns the natural chord tones.
162
163		Returns:
164			List of MIDI note numbers for chord tones
165
166		Example:
167			```python
168			chord = Chord(root_pc=0, quality="major")  # C major
169			chord.tones(root=60)               # [60, 64, 67] - root position around C4
170			chord.tones(root=62)               # [60, 64, 67] - still finds C4 as closest root
171			chord.tones(root=70)               # [72, 76, 79] - finds C5 as closest root
172			```
173		"""
174
175		# Find the MIDI note for self.root_pc that is closest to the requested root.
176		# This handles octaves automatically.
177		offset = (self.root_pc - root) % 12
178		if offset > 6:
179			offset -= 12
180
181		effective_root = root + offset
182
183		intervals = self.intervals()
184
185		if inversion != 0:
186			intervals = subsequence.voicings.invert_chord(intervals, inversion)
187
188		if count is not None:
189			n = len(intervals)
190			return [effective_root + intervals[i % n] + 12 * (i // n) for i in range(count)]
191
192		return [effective_root + interval for interval in intervals]

Return MIDI note numbers for chord tones starting from a root.

Finds the MIDI note corresponding to the chord's root pitch class that is closest to the provided root argument.

Arguments:
  • root: MIDI note number (e.g., 60 = middle C) to center the chord around.
  • inversion: Chord inversion (0 = root position, 1 = first, 2 = second, ...). Wraps around for values >= number of notes.
  • count: Number of notes to return. When set, the chord intervals cycle into higher octaves until count notes are produced. When None (default), returns the natural chord tones.
Returns:

List of MIDI note numbers for chord tones

Example:
chord = Chord(root_pc=0, quality="major")  # C major
chord.tones(root=60)               # [60, 64, 67] - root position around C4
chord.tones(root=62)               # [60, 64, 67] - still finds C4 as closest root
chord.tones(root=70)               # [72, 76, 79] - finds C5 as closest root
def root_note(self, root_midi: int) -> int:
195	def root_note (self, root_midi: int) -> int:
196
197		"""
198		Return the MIDI note number for the chord root nearest to *root_midi*.
199
200		This is equivalent to ``self.tones(root_midi)[0]`` but makes intent
201		explicit when you only need the single root pitch.
202
203		Parameters:
204			root_midi: Reference MIDI note number used to find the closest octave
205			           of this chord's root pitch class.
206
207		Returns:
208			MIDI note number of the chord root.
209
210		Example:
211			```python
212			chord = Chord(root_pc=4, quality="major")  # E major
213			chord.root_note(60)   # → 64  (E4, nearest to C4)
214			chord.root_note(69)   # → 64  (E4, nearest to A4)
215			```
216		"""
217
218		return self.tones(root_midi)[0]

Return the MIDI note number for the chord root nearest to root_midi.

This is equivalent to self.tones(root_midi)[0] but makes intent explicit when you only need the single root pitch.

Arguments:
  • root_midi: Reference MIDI note number used to find the closest octave of this chord's root pitch class.
Returns:

MIDI note number of the chord root.

Example:
chord = Chord(root_pc=4, quality="major")  # E major
chord.root_note(60)   # → 64  (E4, nearest to C4)
chord.root_note(69)   # → 64  (E4, nearest to A4)
def bass_note(self, root_midi: int, octave_offset: int = -1) -> int:
221	def bass_note (self, root_midi: int, octave_offset: int = -1) -> int:
222
223		"""
224		Return the chord root shifted by a number of octaves.
225
226		Commonly used to produce a bass register note one or two octaves
227		below the chord voicing.
228
229		Parameters:
230			root_midi: Reference MIDI note number (passed to :meth:`root_note`).
231			octave_offset: Octaves to shift; negative moves down (default ``-1``).
232
233		Returns:
234			MIDI note number of the chord root in the target register.
235
236		Example:
237			```python
238			chord = Chord(root_pc=4, quality="major")  # E major
239			chord.bass_note(64)        # → 52  (E3, one octave down from E4)
240			chord.bass_note(64, -2)    # → 40  (E2, two octaves down)
241			```
242		"""
243
244		return self.root_note(root_midi) + (12 * octave_offset)

Return the chord root shifted by a number of octaves.

Commonly used to produce a bass register note one or two octaves below the chord voicing.

Arguments:
  • root_midi: Reference MIDI note number (passed to root_note()).
  • octave_offset: Octaves to shift; negative moves down (default -1).
Returns:

MIDI note number of the chord root in the target register.

Example:
chord = Chord(root_pc=4, quality="major")  # E major
chord.bass_note(64)        # → 52  (E3, one octave down from E4)
chord.bass_note(64, -2)    # → 40  (E2, two octaves down)
def name(self) -> str:
247	def name (self) -> str:
248
249		"""
250		Return a human-friendly chord name.
251
252		A registered quality without a suffix prints as ``root(quality)``
253		(e.g. ``"C(quartal)"``) rather than masquerading as a plain major.
254		"""
255
256		root_name = PC_TO_NOTE_NAME[self.root_pc % 12]
257
258		if self.quality not in CHORD_SUFFIX:
259			return f"{root_name}({self.quality})"
260
261		return f"{root_name}{CHORD_SUFFIX[self.quality]}"

Return a human-friendly chord name.

A registered quality without a suffix prints as root(quality) (e.g. "C(quartal)") rather than masquerading as a plain major.

@dataclasses.dataclass
class Groove:
 21@dataclasses.dataclass
 22class Groove:
 23
 24	"""
 25	A timing/velocity template applied to quantized grid positions.
 26
 27	A groove is a repeating pattern of per-step timing offsets and optional
 28	velocity adjustments aligned to a rhythmic grid. Apply it as a post-build
 29	transform with ``p.groove(template)`` to give a pattern its characteristic
 30	feel — swing, shuffle, MPC-style pocket, or anything extracted from an
 31	Ableton ``.agr`` file.
 32
 33	Parameters:
 34		offsets: Timing offset per grid slot, in beats. Repeats cyclically.
 35			Positive values delay the note; negative values push it earlier.
 36		grid: Grid size in beats (0.25 = 16th notes, 0.5 = 8th notes).
 37		velocities: Optional velocity scale per grid slot (1.0 = unchanged).
 38			Repeats cyclically alongside offsets.
 39
 40	Example::
 41
 42		# Ableton-style 57% swing on 16th notes
 43		groove = Groove.swing(percent=57)
 44
 45		# Custom groove with timing and velocity
 46		groove = Groove(
 47			grid=0.25,
 48			offsets=[0.0, +0.02, 0.0, -0.01],
 49			velocities=[1.0, 0.7, 0.9, 0.6],
 50		)
 51	"""
 52
 53	offsets: typing.List[float]
 54	grid: float = 0.25
 55	velocities: typing.Optional[typing.List[float]] = None
 56
 57	def __post_init__ (self) -> None:
 58		if not self.offsets:
 59			raise ValueError("offsets must not be empty")
 60		if self.grid <= 0:
 61			raise ValueError("grid must be positive")
 62		if self.velocities is not None and not self.velocities:
 63			raise ValueError("velocities must not be empty (use None for no velocity adjustment)")
 64
 65	@staticmethod
 66	def swing (percent: float = 57.0, grid: float = 0.25) -> "Groove":
 67
 68		"""
 69		Create a swing groove from a percentage.
 70
 71		50% is straight (no swing). 67% is approximately triplet swing.
 72		57% is a moderate shuffle — the Ableton default.
 73
 74		Parameters:
 75			percent: Swing amount (50–75 is the useful range).
 76			grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths).
 77		"""
 78
 79		if percent < 50.0 or percent > 99.0:
 80			raise ValueError("swing percent must be between 50 and 99")
 81		pair_duration = grid * 2
 82		offset = (percent / 100.0 - 0.5) * pair_duration
 83		return Groove(offsets=[0.0, offset], grid=grid)
 84
 85	@staticmethod
 86	def from_agr (path: str, grid: typing.Optional[float] = None) -> "Groove":
 87
 88		"""
 89		Import timing and velocity data from an Ableton .agr groove file.
 90
 91		An ``.agr`` file is an XML document containing a MIDI clip whose
 92		note positions encode the groove's rhythmic feel. This method reads
 93		those note start times and velocities and converts them into the
 94		``Groove`` dataclass format (per-step offsets and velocity scales).
 95
 96		Without ``grid=``, the grid is inferred as ``clip length / note
 97		count`` — which assumes the clip plays **exactly one note per grid
 98		cell** (the standard shape for a groove clip). A clip with rests or
 99		chords breaks that assumption: pass ``grid=`` explicitly (e.g.
100		``grid=0.25`` for a 16th-note groove) and empty cells keep a neutral
101		offset. A clip whose notes cannot be assigned one-per-cell raises
102		rather than importing a wrong feel.
103
104		**What is extracted:**
105
106		- ``Time`` attribute of each ``MidiNoteEvent`` → timing offsets
107		  relative to ideal grid positions.
108		- ``Velocity`` attribute of each ``MidiNoteEvent`` → velocity
109		  scaling (normalised to the highest velocity in the file).
110		- ``TimingAmount`` from the Groove element → pre-scales the timing
111		  offsets (100 = full, 70 = 70% of the groove's timing).
112		- ``VelocityAmount`` from the Groove element → pre-scales velocity
113		  deviation (100 = full groove velocity, 0 = no velocity changes).
114
115		The resulting ``Groove`` reflects the file author's intended
116		strength. Use ``strength=`` when applying to further adjust.
117
118		**What is NOT imported:**
119
120		``RandomAmount`` (use ``p.randomize()`` separately for random
121		jitter) and ``QuantizationAmount`` (not applicable - Subsequence
122		notes are already grid-quantized by construction).
123
124		Other ``MidiNoteEvent`` fields (``Duration``, ``VelocityDeviation``,
125		``OffVelocity``, ``Probability``) are also ignored.
126
127		Parameters:
128			path: Path to the .agr file.
129			grid: Grid size in beats (0.25 = 16th notes). ``None`` (default)
130				infers it from the clip, assuming one note per cell.
131		"""
132
133		tree = xml.etree.ElementTree.parse(path)
134		root = tree.getroot()
135
136		# Find the MIDI clip
137		clip = root.find(".//MidiClip")
138		if clip is None:
139			raise ValueError(f"No MidiClip found in {path}")
140
141		# Get clip length
142		current_end = clip.find("CurrentEnd")
143		if current_end is None:
144			raise ValueError(f"No CurrentEnd found in {path}")
145		clip_length = float(current_end.get("Value", "4"))
146
147		# Read Groove Pool blend parameters
148		groove_elem = root.find(".//Groove")
149		timing_amount = 100.0
150		velocity_amount = 100.0
151		if groove_elem is not None:
152			timing_el = groove_elem.find("TimingAmount")
153			if timing_el is not None:
154				timing_amount = float(timing_el.get("Value", "100"))
155			velocity_el = groove_elem.find("VelocityAmount")
156			if velocity_el is not None:
157				velocity_amount = float(velocity_el.get("Value", "100"))
158
159		timing_scale = timing_amount / 100.0
160		velocity_scale = velocity_amount / 100.0
161
162		# Extract note events sorted by time
163		events = clip.findall(".//MidiNoteEvent")
164		if not events:
165			raise ValueError(f"No MidiNoteEvent elements found in {path}")
166
167		times: typing.List[float] = []
168		velocities_raw: typing.List[float] = []
169		for event in events:
170			times.append(float(event.get("Time", "0")))
171			velocities_raw.append(float(event.get("Velocity", "127")))
172
173		# Sort as PAIRS - sorting times alone desynced each offset from its
174		# note's velocity whenever the XML listed events out of time order.
175		paired = sorted(zip(times, velocities_raw))
176		times = [t for t, _ in paired]
177		velocities_raw = [v for _, v in paired]
178
179		note_count = len(times)
180
181		# Infer grid from clip length and note count — valid only for the
182		# one-note-per-cell clip shape (see docstring); grid= overrides.
183		if grid is None:
184			grid = clip_length / note_count
185
186		if grid <= 0:
187			raise ValueError(f"grid must be positive — got {grid}")
188
189		slot_count = max(1, int(round(clip_length / grid)))
190
191		# Bind each note to its NEAREST grid line (robust to rests under an
192		# explicit grid — empty cells keep a neutral offset), refusing
193		# ambiguous clips instead of importing a garbage feel.
194		slot_offsets = [0.0] * slot_count
195		slot_velocities: typing.List[typing.Optional[float]] = [None] * slot_count
196
197		for time, velocity in zip(times, velocities_raw):
198
199			slot = int(round(time / grid))
200
201			if not 0 <= slot < slot_count:
202				raise ValueError(
203					f"{path}: note at beat {time:g} falls outside the {slot_count}-cell "
204					f"grid (grid={grid:g}, clip length {clip_length:g}) — pass grid= "
205					"matching the clip's note spacing"
206				)
207
208			if slot_velocities[slot] is not None:
209				raise ValueError(
210					f"{path}: two notes share grid cell {slot} (a chord, or a grid "
211					"coarser than the clip's note spacing) — pass grid= matching the "
212					"clip (e.g. grid=0.25 for 16ths)"
213				)
214
215			slot_offsets[slot] = (time - slot * grid) * timing_scale
216			slot_velocities[slot] = velocity
217
218		# Calculate velocity scales (relative to max velocity in the file),
219		# blended toward 1.0 by VelocityAmount; empty cells stay neutral (1.0).
220		filled = [v for v in slot_velocities if v is not None]
221		max_vel = max(filled)
222		has_velocity_variation = any(v != max_vel for v in filled)
223		groove_velocities: typing.Optional[typing.List[float]] = None
224		if has_velocity_variation and max_vel > 0:
225			raw_scales = [(v / max_vel) if v is not None else 1.0 for v in slot_velocities]
226			# velocity_scale=1.0 → full groove velocity; 0.0 → all 1.0 (no change)
227			groove_velocities = [1.0 + (s - 1.0) * velocity_scale for s in raw_scales]
228			# If blending has removed all variation, set to None
229			if all(abs(v - 1.0) < 1e-9 for v in groove_velocities):
230				groove_velocities = None
231
232		return Groove(offsets=slot_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:
65	@staticmethod
66	def swing (percent: float = 57.0, grid: float = 0.25) -> "Groove":
67
68		"""
69		Create a swing groove from a percentage.
70
71		50% is straight (no swing). 67% is approximately triplet swing.
72		57% is a moderate shuffle — the Ableton default.
73
74		Parameters:
75			percent: Swing amount (50–75 is the useful range).
76			grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths).
77		"""
78
79		if percent < 50.0 or percent > 99.0:
80			raise ValueError("swing percent must be between 50 and 99")
81		pair_duration = grid * 2
82		offset = (percent / 100.0 - 0.5) * pair_duration
83		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, grid: Optional[float] = None) -> Groove:
 85	@staticmethod
 86	def from_agr (path: str, grid: typing.Optional[float] = None) -> "Groove":
 87
 88		"""
 89		Import timing and velocity data from an Ableton .agr groove file.
 90
 91		An ``.agr`` file is an XML document containing a MIDI clip whose
 92		note positions encode the groove's rhythmic feel. This method reads
 93		those note start times and velocities and converts them into the
 94		``Groove`` dataclass format (per-step offsets and velocity scales).
 95
 96		Without ``grid=``, the grid is inferred as ``clip length / note
 97		count`` — which assumes the clip plays **exactly one note per grid
 98		cell** (the standard shape for a groove clip). A clip with rests or
 99		chords breaks that assumption: pass ``grid=`` explicitly (e.g.
100		``grid=0.25`` for a 16th-note groove) and empty cells keep a neutral
101		offset. A clip whose notes cannot be assigned one-per-cell raises
102		rather than importing a wrong feel.
103
104		**What is extracted:**
105
106		- ``Time`` attribute of each ``MidiNoteEvent`` → timing offsets
107		  relative to ideal grid positions.
108		- ``Velocity`` attribute of each ``MidiNoteEvent`` → velocity
109		  scaling (normalised to the highest velocity in the file).
110		- ``TimingAmount`` from the Groove element → pre-scales the timing
111		  offsets (100 = full, 70 = 70% of the groove's timing).
112		- ``VelocityAmount`` from the Groove element → pre-scales velocity
113		  deviation (100 = full groove velocity, 0 = no velocity changes).
114
115		The resulting ``Groove`` reflects the file author's intended
116		strength. Use ``strength=`` when applying to further adjust.
117
118		**What is NOT imported:**
119
120		``RandomAmount`` (use ``p.randomize()`` separately for random
121		jitter) and ``QuantizationAmount`` (not applicable - Subsequence
122		notes are already grid-quantized by construction).
123
124		Other ``MidiNoteEvent`` fields (``Duration``, ``VelocityDeviation``,
125		``OffVelocity``, ``Probability``) are also ignored.
126
127		Parameters:
128			path: Path to the .agr file.
129			grid: Grid size in beats (0.25 = 16th notes). ``None`` (default)
130				infers it from the clip, assuming one note per cell.
131		"""
132
133		tree = xml.etree.ElementTree.parse(path)
134		root = tree.getroot()
135
136		# Find the MIDI clip
137		clip = root.find(".//MidiClip")
138		if clip is None:
139			raise ValueError(f"No MidiClip found in {path}")
140
141		# Get clip length
142		current_end = clip.find("CurrentEnd")
143		if current_end is None:
144			raise ValueError(f"No CurrentEnd found in {path}")
145		clip_length = float(current_end.get("Value", "4"))
146
147		# Read Groove Pool blend parameters
148		groove_elem = root.find(".//Groove")
149		timing_amount = 100.0
150		velocity_amount = 100.0
151		if groove_elem is not None:
152			timing_el = groove_elem.find("TimingAmount")
153			if timing_el is not None:
154				timing_amount = float(timing_el.get("Value", "100"))
155			velocity_el = groove_elem.find("VelocityAmount")
156			if velocity_el is not None:
157				velocity_amount = float(velocity_el.get("Value", "100"))
158
159		timing_scale = timing_amount / 100.0
160		velocity_scale = velocity_amount / 100.0
161
162		# Extract note events sorted by time
163		events = clip.findall(".//MidiNoteEvent")
164		if not events:
165			raise ValueError(f"No MidiNoteEvent elements found in {path}")
166
167		times: typing.List[float] = []
168		velocities_raw: typing.List[float] = []
169		for event in events:
170			times.append(float(event.get("Time", "0")))
171			velocities_raw.append(float(event.get("Velocity", "127")))
172
173		# Sort as PAIRS - sorting times alone desynced each offset from its
174		# note's velocity whenever the XML listed events out of time order.
175		paired = sorted(zip(times, velocities_raw))
176		times = [t for t, _ in paired]
177		velocities_raw = [v for _, v in paired]
178
179		note_count = len(times)
180
181		# Infer grid from clip length and note count — valid only for the
182		# one-note-per-cell clip shape (see docstring); grid= overrides.
183		if grid is None:
184			grid = clip_length / note_count
185
186		if grid <= 0:
187			raise ValueError(f"grid must be positive — got {grid}")
188
189		slot_count = max(1, int(round(clip_length / grid)))
190
191		# Bind each note to its NEAREST grid line (robust to rests under an
192		# explicit grid — empty cells keep a neutral offset), refusing
193		# ambiguous clips instead of importing a garbage feel.
194		slot_offsets = [0.0] * slot_count
195		slot_velocities: typing.List[typing.Optional[float]] = [None] * slot_count
196
197		for time, velocity in zip(times, velocities_raw):
198
199			slot = int(round(time / grid))
200
201			if not 0 <= slot < slot_count:
202				raise ValueError(
203					f"{path}: note at beat {time:g} falls outside the {slot_count}-cell "
204					f"grid (grid={grid:g}, clip length {clip_length:g}) — pass grid= "
205					"matching the clip's note spacing"
206				)
207
208			if slot_velocities[slot] is not None:
209				raise ValueError(
210					f"{path}: two notes share grid cell {slot} (a chord, or a grid "
211					"coarser than the clip's note spacing) — pass grid= matching the "
212					"clip (e.g. grid=0.25 for 16ths)"
213				)
214
215			slot_offsets[slot] = (time - slot * grid) * timing_scale
216			slot_velocities[slot] = velocity
217
218		# Calculate velocity scales (relative to max velocity in the file),
219		# blended toward 1.0 by VelocityAmount; empty cells stay neutral (1.0).
220		filled = [v for v in slot_velocities if v is not None]
221		max_vel = max(filled)
222		has_velocity_variation = any(v != max_vel for v in filled)
223		groove_velocities: typing.Optional[typing.List[float]] = None
224		if has_velocity_variation and max_vel > 0:
225			raw_scales = [(v / max_vel) if v is not None else 1.0 for v in slot_velocities]
226			# velocity_scale=1.0 → full groove velocity; 0.0 → all 1.0 (no change)
227			groove_velocities = [1.0 + (s - 1.0) * velocity_scale for s in raw_scales]
228			# If blending has removed all variation, set to None
229			if all(abs(v - 1.0) < 1e-9 for v in groove_velocities):
230				groove_velocities = None
231
232		return Groove(offsets=slot_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).

Without grid=, the grid is inferred as clip length / note count — which assumes the clip plays exactly one note per grid cell (the standard shape for a groove clip). A clip with rests or chords breaks that assumption: pass grid= explicitly (e.g. grid=0.25 for a 16th-note groove) and empty cells keep a neutral offset. A clip whose notes cannot be assigned one-per-cell raises rather than importing a wrong feel.

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.
  • grid: Grid size in beats (0.25 = 16th notes). None (default) infers it from the clip, assuming one note per cell.
class MelodicState:
208class MelodicState:
209
210	"""Persistent melodic context that applies NIR scoring to single-note lines."""
211
212
213	def __init__ (
214		self,
215		key: typing.Optional[str] = None,
216		mode: typing.Optional[str] = None,
217		low: int = 48,
218		high: int = 72,
219		nir_strength: float = 0.5,
220		chord_weight: float = 0.4,
221		rest_probability: float = 0.0,
222		pitch_diversity: float = 0.6,
223		tessitura_strength: float = 0.0,
224	) -> None:
225
226		"""Initialise a melodic state for a given key, mode, and MIDI register.
227
228		Parameters:
229			key: Root note of the key (e.g. ``"C"``, ``"F#"``, ``"Bb"``).
230			    When omitted, the state adopts the **composition's** key the
231			    first time ``p.melody()`` uses it (falling back to ``"C"``).
232			mode: Scale mode name.  Accepts any mode registered with
233			      :func:`~subsequence.intervals.scale_pitch_classes` (e.g.
234			      ``"ionian"``, ``"aeolian"``, ``"dorian"``).  When omitted,
235			      adopts the composition's scale (falling back to ``"ionian"``).
236			low: Lowest MIDI note (inclusive) in the pitch pool.
237			high: Highest MIDI note (inclusive) in the pitch pool.
238			nir_strength: 0.0–1.0.  Scales how strongly the NIR rules
239			    influence candidate scores.  0.0 = uniform; 1.0 = full boost.
240			chord_weight: 0.0–1.0.  Additive multiplier bonus for candidates
241			    whose pitch class belongs to the current chord tones.
242			rest_probability: 0.0–1.0.  Probability of producing a rest
243			    (returning ``None``) at any given step.
244			pitch_diversity: 0.0–1.0.  Exponential penalty per recent
245			    repetition of the same pitch.  Lower values discourage
246			    repetition more aggressively.
247			tessitura_strength: 0.0–1.0.  Regression pull toward the centre
248			    of the register after the line strays (off by default; the
249			    generate path enables it).
250		"""
251
252		if nir_strength < 0 or nir_strength > 1:
253			raise ValueError("NIR strength must be between 0 and 1")
254
255		if rest_probability < 0 or rest_probability > 1:
256			raise ValueError("Rest probability must be between 0 and 1")
257
258		if pitch_diversity < 0 or pitch_diversity > 1:
259			raise ValueError("Pitch diversity must be between 0 and 1")
260
261		if chord_weight < 0 or chord_weight > 1:
262			raise ValueError("Chord weight must be between 0 and 1")
263
264		if tessitura_strength < 0 or tessitura_strength > 1:
265			raise ValueError("Tessitura strength must be between 0 and 1")
266
267		if low >= high:
268			raise ValueError("low must be below high")
269
270		# None defers to the composition (configure_defaults); the fallbacks
271		# keep a bare MelodicState() working standalone.
272		self._explicit_key = key is not None
273		self._explicit_mode = mode is not None
274		self._explicit_pool = False
275
276		self.key = key if key is not None else "C"
277		self.mode = mode if mode is not None else "ionian"
278		self.low = low
279		self.high = high
280		self.nir_strength = nir_strength
281		self.chord_weight = chord_weight
282		self.rest_probability = rest_probability
283		self.pitch_diversity = pitch_diversity
284		self.tessitura_strength = tessitura_strength
285
286		# The soft side of the CHORAL separation — replace or extend freely.
287		self.factors: typing.List[ScoringFactor] = list(DEFAULT_FACTORS)
288
289		self._rebuild_pool()
290
291		# History of last N absolute MIDI pitches (capped at 4, same as HarmonicState).
292		self.history: typing.List[int] = []
293
294
295	def _rebuild_pool (self) -> None:
296
297		"""Derive the pitch pool (the one hard constraint) from key/mode/register."""
298
299		self._tonic_pc: int = subsequence.chords.key_name_to_pc(self.key)
300
301		self._pitch_pool: typing.List[int] = subsequence.intervals.scale_notes(
302			self.key, self.mode, low=self.low, high=self.high
303		)
304
305
306	def configure_defaults (self, key: typing.Optional[str], mode: typing.Optional[str]) -> None:
307
308		"""Adopt the surrounding key/scale where this state left them unset.
309
310		Called by ``p.melody()`` every build.  It **tracks** the builder's
311		current key/scale (which is the section's effective key under a form),
312		so a state placed across sections follows each section's key — its
313		melodic *history* is untouched, only the pitch pool and tonic move.
314		An explicit constructor key/scale or an explicit pool always wins and
315		is never overridden.
316		"""
317
318		if self._explicit_pool:
319			return
320
321		changed = False
322
323		# Re-track on every call (not just the first): a persistent state used
324		# across sections must follow the live key, or the first section to
325		# place it would freeze the key forever.
326		if not self._explicit_key and key is not None and key != self.key:
327			self.key = key
328			changed = True
329
330		if not self._explicit_mode and mode is not None and mode != self.mode:
331			self.mode = mode
332			changed = True
333
334		if changed:
335			self._rebuild_pool()
336
337
338	def set_pool (self, pitches: typing.Sequence[int]) -> None:
339
340		"""Replace the pitch pool with explicit MIDI pitches — the experimental seam.
341
342		Admits sieve output, non-octave organisations, or any hand-picked
343		pool; key/mode no longer constrain candidates (the tonic pitch
344		class, for Rule C, stays the key's).
345		"""
346
347		pool = sorted(int(p) for p in pitches)
348
349		if not pool:
350			raise ValueError("set_pool() needs at least one pitch")
351
352		self._pitch_pool = pool
353		self._explicit_pool = True
354		self.low = pool[0]
355		self.high = max(pool[-1], pool[0] + 1)
356
357
358	def clone (self) -> "MelodicState":
359
360		"""An independent copy — settings, factors, pool, and history.
361
362		Value constructors (``Motif.generate``) copy the state they are
363		given and walk the copy, so a module-level live state is never
364		mutated by building a value.
365		"""
366
367		duplicate = MelodicState(
368			key = self.key if self._explicit_key else None,
369			mode = self.mode if self._explicit_mode else None,
370			low = self.low,
371			high = self.high,
372			nir_strength = self.nir_strength,
373			chord_weight = self.chord_weight,
374			rest_probability = self.rest_probability,
375			pitch_diversity = self.pitch_diversity,
376			tessitura_strength = self.tessitura_strength,
377		)
378
379		duplicate.key = self.key
380		duplicate.mode = self.mode
381		duplicate._rebuild_pool()
382
383		if self._explicit_pool:
384			duplicate.set_pool(self._pitch_pool)
385
386		duplicate.factors = list(self.factors)
387		duplicate.history = list(self.history)
388
389		return duplicate
390
391
392	def choose_next (
393		self,
394		chord_tones: typing.Optional[typing.List[int]],
395		rng: random.Random,
396		beat: typing.Optional[float] = None,
397		position: typing.Optional[float] = None,
398		contour_target: typing.Optional[float] = None,
399	) -> typing.Optional[int]:
400
401		"""Score all pitch-pool candidates and return the chosen pitch, or None for a rest.
402
403		``beat`` (the note's beat within its cycle), ``position`` (0–1
404		through a generated span), and ``contour_target`` (the envelope's
405		height there) thread caller context into the scoring factors.
406		"""
407
408		if self.rest_probability > 0.0 and rng.random() < self.rest_probability:
409			return None
410
411		if not self._pitch_pool:
412			return None
413
414		# Resolve chord tones to pitch classes for fast membership testing.
415		chord_tone_pcs = {t % 12 for t in chord_tones} if chord_tones else set()
416
417		scores = [
418			self._score_candidate(candidate, chord_tone_pcs, beat=beat, position=position, contour_target=contour_target)
419			for candidate in self._pitch_pool
420		]
421
422		# Weighted random choice: select using cumulative score as a probability weight.
423		total = sum(scores)
424
425		if total <= 0.0:
426			chosen = rng.choice(self._pitch_pool)
427
428		else:
429			r = rng.uniform(0.0, total)
430			cumulative = 0.0
431			chosen = self._pitch_pool[-1]
432
433			for pitch, score in zip(self._pitch_pool, scores):
434				cumulative += score
435				if r <= cumulative:
436					chosen = pitch
437					break
438
439		self.record(chosen)
440
441		return chosen
442
443
444	def _score_candidate (
445		self,
446		candidate: int,
447		chord_tone_pcs: typing.Set[int],
448		beat: typing.Optional[float] = None,
449		position: typing.Optional[float] = None,
450		contour_target: typing.Optional[float] = None,
451	) -> float:
452
453		"""Score one candidate: the product of every factor in :attr:`factors`."""
454
455		ctx = ScoringContext(
456			candidate = candidate,
457			history = tuple(self.history),
458			chord_tone_pcs = frozenset(chord_tone_pcs),
459			tonic_pc = self._tonic_pc,
460			low = self.low,
461			high = self.high,
462			beat = beat,
463			position = position,
464			contour_target = contour_target,
465		)
466
467		score = 1.0
468
469		for factor in self.factors:
470			score *= factor(self, ctx)
471
472		return max(0.0, score)
473
474	def record (self, pitch: int) -> None:
475
476		"""Append a pitch to the melodic history (capped at 4 entries).
477
478		Public so pinned notes — chosen by fiat, not by the walk — still
479		enter the NIR context.
480		"""
481
482		self.history.append(pitch)
483		if len(self.history) > 4:
484			self.history.pop(0)

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

MelodicState( key: Optional[str] = None, mode: Optional[str] = None, 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, tessitura_strength: float = 0.0)
213	def __init__ (
214		self,
215		key: typing.Optional[str] = None,
216		mode: typing.Optional[str] = None,
217		low: int = 48,
218		high: int = 72,
219		nir_strength: float = 0.5,
220		chord_weight: float = 0.4,
221		rest_probability: float = 0.0,
222		pitch_diversity: float = 0.6,
223		tessitura_strength: float = 0.0,
224	) -> None:
225
226		"""Initialise a melodic state for a given key, mode, and MIDI register.
227
228		Parameters:
229			key: Root note of the key (e.g. ``"C"``, ``"F#"``, ``"Bb"``).
230			    When omitted, the state adopts the **composition's** key the
231			    first time ``p.melody()`` uses it (falling back to ``"C"``).
232			mode: Scale mode name.  Accepts any mode registered with
233			      :func:`~subsequence.intervals.scale_pitch_classes` (e.g.
234			      ``"ionian"``, ``"aeolian"``, ``"dorian"``).  When omitted,
235			      adopts the composition's scale (falling back to ``"ionian"``).
236			low: Lowest MIDI note (inclusive) in the pitch pool.
237			high: Highest MIDI note (inclusive) in the pitch pool.
238			nir_strength: 0.0–1.0.  Scales how strongly the NIR rules
239			    influence candidate scores.  0.0 = uniform; 1.0 = full boost.
240			chord_weight: 0.0–1.0.  Additive multiplier bonus for candidates
241			    whose pitch class belongs to the current chord tones.
242			rest_probability: 0.0–1.0.  Probability of producing a rest
243			    (returning ``None``) at any given step.
244			pitch_diversity: 0.0–1.0.  Exponential penalty per recent
245			    repetition of the same pitch.  Lower values discourage
246			    repetition more aggressively.
247			tessitura_strength: 0.0–1.0.  Regression pull toward the centre
248			    of the register after the line strays (off by default; the
249			    generate path enables it).
250		"""
251
252		if nir_strength < 0 or nir_strength > 1:
253			raise ValueError("NIR strength must be between 0 and 1")
254
255		if rest_probability < 0 or rest_probability > 1:
256			raise ValueError("Rest probability must be between 0 and 1")
257
258		if pitch_diversity < 0 or pitch_diversity > 1:
259			raise ValueError("Pitch diversity must be between 0 and 1")
260
261		if chord_weight < 0 or chord_weight > 1:
262			raise ValueError("Chord weight must be between 0 and 1")
263
264		if tessitura_strength < 0 or tessitura_strength > 1:
265			raise ValueError("Tessitura strength must be between 0 and 1")
266
267		if low >= high:
268			raise ValueError("low must be below high")
269
270		# None defers to the composition (configure_defaults); the fallbacks
271		# keep a bare MelodicState() working standalone.
272		self._explicit_key = key is not None
273		self._explicit_mode = mode is not None
274		self._explicit_pool = False
275
276		self.key = key if key is not None else "C"
277		self.mode = mode if mode is not None else "ionian"
278		self.low = low
279		self.high = high
280		self.nir_strength = nir_strength
281		self.chord_weight = chord_weight
282		self.rest_probability = rest_probability
283		self.pitch_diversity = pitch_diversity
284		self.tessitura_strength = tessitura_strength
285
286		# The soft side of the CHORAL separation — replace or extend freely.
287		self.factors: typing.List[ScoringFactor] = list(DEFAULT_FACTORS)
288
289		self._rebuild_pool()
290
291		# History of last N absolute MIDI pitches (capped at 4, same as HarmonicState).
292		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"). When omitted, the state adopts the composition's key the first time p.melody() uses it (falling back to "C").
  • mode: Scale mode name. Accepts any mode registered with ~subsequence.intervals.scale_pitch_classes() (e.g. "ionian", "aeolian", "dorian"). When omitted, adopts the composition's scale (falling back to "ionian").
  • 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.
  • tessitura_strength: 0.0–1.0. Regression pull toward the centre of the register after the line strays (off by default; the generate path enables it).
key
mode
low
high
nir_strength
chord_weight
rest_probability
pitch_diversity
tessitura_strength
factors: List[Callable[[MelodicState, subsequence.melodic_state.ScoringContext], float]]
history: List[int]
def configure_defaults(self, key: Optional[str], mode: Optional[str]) -> None:
306	def configure_defaults (self, key: typing.Optional[str], mode: typing.Optional[str]) -> None:
307
308		"""Adopt the surrounding key/scale where this state left them unset.
309
310		Called by ``p.melody()`` every build.  It **tracks** the builder's
311		current key/scale (which is the section's effective key under a form),
312		so a state placed across sections follows each section's key — its
313		melodic *history* is untouched, only the pitch pool and tonic move.
314		An explicit constructor key/scale or an explicit pool always wins and
315		is never overridden.
316		"""
317
318		if self._explicit_pool:
319			return
320
321		changed = False
322
323		# Re-track on every call (not just the first): a persistent state used
324		# across sections must follow the live key, or the first section to
325		# place it would freeze the key forever.
326		if not self._explicit_key and key is not None and key != self.key:
327			self.key = key
328			changed = True
329
330		if not self._explicit_mode and mode is not None and mode != self.mode:
331			self.mode = mode
332			changed = True
333
334		if changed:
335			self._rebuild_pool()

Adopt the surrounding key/scale where this state left them unset.

Called by p.melody() every build. It tracks the builder's current key/scale (which is the section's effective key under a form), so a state placed across sections follows each section's key — its melodic history is untouched, only the pitch pool and tonic move. An explicit constructor key/scale or an explicit pool always wins and is never overridden.

def set_pool(self, pitches: Sequence[int]) -> None:
338	def set_pool (self, pitches: typing.Sequence[int]) -> None:
339
340		"""Replace the pitch pool with explicit MIDI pitches — the experimental seam.
341
342		Admits sieve output, non-octave organisations, or any hand-picked
343		pool; key/mode no longer constrain candidates (the tonic pitch
344		class, for Rule C, stays the key's).
345		"""
346
347		pool = sorted(int(p) for p in pitches)
348
349		if not pool:
350			raise ValueError("set_pool() needs at least one pitch")
351
352		self._pitch_pool = pool
353		self._explicit_pool = True
354		self.low = pool[0]
355		self.high = max(pool[-1], pool[0] + 1)

Replace the pitch pool with explicit MIDI pitches — the experimental seam.

Admits sieve output, non-octave organisations, or any hand-picked pool; key/mode no longer constrain candidates (the tonic pitch class, for Rule C, stays the key's).

def clone(self) -> MelodicState:
358	def clone (self) -> "MelodicState":
359
360		"""An independent copy — settings, factors, pool, and history.
361
362		Value constructors (``Motif.generate``) copy the state they are
363		given and walk the copy, so a module-level live state is never
364		mutated by building a value.
365		"""
366
367		duplicate = MelodicState(
368			key = self.key if self._explicit_key else None,
369			mode = self.mode if self._explicit_mode else None,
370			low = self.low,
371			high = self.high,
372			nir_strength = self.nir_strength,
373			chord_weight = self.chord_weight,
374			rest_probability = self.rest_probability,
375			pitch_diversity = self.pitch_diversity,
376			tessitura_strength = self.tessitura_strength,
377		)
378
379		duplicate.key = self.key
380		duplicate.mode = self.mode
381		duplicate._rebuild_pool()
382
383		if self._explicit_pool:
384			duplicate.set_pool(self._pitch_pool)
385
386		duplicate.factors = list(self.factors)
387		duplicate.history = list(self.history)
388
389		return duplicate

An independent copy — settings, factors, pool, and history.

Value constructors (Motif.generate) copy the state they are given and walk the copy, so a module-level live state is never mutated by building a value.

def choose_next( self, chord_tones: Optional[List[int]], rng: random.Random, beat: Optional[float] = None, position: Optional[float] = None, contour_target: Optional[float] = None) -> Optional[int]:
392	def choose_next (
393		self,
394		chord_tones: typing.Optional[typing.List[int]],
395		rng: random.Random,
396		beat: typing.Optional[float] = None,
397		position: typing.Optional[float] = None,
398		contour_target: typing.Optional[float] = None,
399	) -> typing.Optional[int]:
400
401		"""Score all pitch-pool candidates and return the chosen pitch, or None for a rest.
402
403		``beat`` (the note's beat within its cycle), ``position`` (0–1
404		through a generated span), and ``contour_target`` (the envelope's
405		height there) thread caller context into the scoring factors.
406		"""
407
408		if self.rest_probability > 0.0 and rng.random() < self.rest_probability:
409			return None
410
411		if not self._pitch_pool:
412			return None
413
414		# Resolve chord tones to pitch classes for fast membership testing.
415		chord_tone_pcs = {t % 12 for t in chord_tones} if chord_tones else set()
416
417		scores = [
418			self._score_candidate(candidate, chord_tone_pcs, beat=beat, position=position, contour_target=contour_target)
419			for candidate in self._pitch_pool
420		]
421
422		# Weighted random choice: select using cumulative score as a probability weight.
423		total = sum(scores)
424
425		if total <= 0.0:
426			chosen = rng.choice(self._pitch_pool)
427
428		else:
429			r = rng.uniform(0.0, total)
430			cumulative = 0.0
431			chosen = self._pitch_pool[-1]
432
433			for pitch, score in zip(self._pitch_pool, scores):
434				cumulative += score
435				if r <= cumulative:
436					chosen = pitch
437					break
438
439		self.record(chosen)
440
441		return chosen

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

beat (the note's beat within its cycle), position (0–1 through a generated span), and contour_target (the envelope's height there) thread caller context into the scoring factors.

def record(self, pitch: int) -> None:
474	def record (self, pitch: int) -> None:
475
476		"""Append a pitch to the melodic history (capped at 4 entries).
477
478		Public so pinned notes — chosen by fiat, not by the walk — still
479		enter the NIR context.
480		"""
481
482		self.history.append(pitch)
483		if len(self.history) > 4:
484			self.history.pop(0)

Append a pitch to the melodic history (capped at 4 entries).

Public so pinned notes — chosen by fiat, not by the walk — still enter the NIR context.

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

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

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

Create a Tuning from a file or programmatically:

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

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

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

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

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

Parse a Scala .scl file.

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

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

Raises ValueError for malformed files.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

def between( low: float, high: float, step: Optional[float] = None) -> subsequence.harmonic_rhythm.HarmonicRhythm:
72def between (low: float, high: float, step: typing.Optional[float] = None) -> HarmonicRhythm:
73
74	"""A harmonic rhythm that varies *between* two lengths (in beats).
75
76	Each chord lasts a random length in ``[low, high]``.  Pass ``step`` to snap
77	those lengths to a grid — e.g. ``between(WHOLE, 3 * WHOLE, step=WHOLE)`` gives
78	one, two, or three whole notes, never anything in between.
79
80	Reads aloud the way you'd describe it: "between one and three whole notes,
81	in whole-note steps."
82	"""
83
84	return HarmonicRhythm(low=low, high=high, step=step)

A harmonic rhythm that varies between two lengths (in beats).

Each chord lasts a random length in [low, high]. Pass step to snap those lengths to a grid — e.g. between(WHOLE, 3 * WHOLE, step=WHOLE) gives one, two, or three whole notes, never anything in between.

Reads aloud the way you'd describe it: "between one and three whole notes, in whole-note steps."

def parse_chord(name: str) -> Chord:
385def parse_chord (name: str) -> Chord:
386
387	"""Parse a chord name like ``"Cm7"`` or ``"Dbmaj7"`` into a :class:`Chord`.
388
389	The name is a root note (``A``–``G`` with an optional ``#`` or ``b``) followed
390	by a quality suffix: ``""`` major, ``m`` minor, ``dim`` diminished,
391	``+``/``aug`` augmented, ``7`` dominant 7th, ``maj7`` major 7th, ``m7`` minor
392	7th, ``m7b5``/``ø`` half-diminished 7th, ``sus2``, ``sus4``.  A few common
393	alternates (``min``, ``-``, ``M7``, …) are accepted too.
394
395	Raises ``ValueError`` for anything it can't read, so a typo surfaces at the
396	call site rather than as a silently wrong chord.
397
398	Example:
399		```python
400		parse_chord("Cm7")    # → Chord(root_pc=0, quality="minor_7th")
401		parse_chord("Dbmaj7") # → Chord(root_pc=1, quality="major_7th")
402		parse_chord("F#")     # → Chord(root_pc=6, quality="major")
403		```
404	"""
405
406	stripped = name.strip()
407	if not stripped or stripped[0] not in "ABCDEFG":
408		raise ValueError(f"Cannot parse chord name {name!r} — expected a root like 'C', 'F#', 'Bb' then a quality, e.g. 'Cm7'")
409
410	split = 2 if (len(stripped) > 1 and stripped[1] in "#b") else 1
411	root_name = stripped[:split]
412	suffix = stripped[split:]
413
414	if root_name not in NOTE_NAME_TO_PC:
415		raise ValueError(f"Cannot parse chord name {name!r} — unknown root {root_name!r}")
416	if suffix not in _SUFFIX_TO_QUALITY:
417		known = ", ".join(repr(key) for key in sorted(_SUFFIX_TO_QUALITY) if key)
418		raise ValueError(f"Cannot parse chord name {name!r} — unknown quality {suffix!r}. Known suffixes: {known}")
419
420	return Chord(root_pc=NOTE_NAME_TO_PC[root_name], quality=_SUFFIX_TO_QUALITY[suffix])

Parse a chord name like "Cm7" or "Dbmaj7" into a Chord.

The name is a root note (AG with an optional # or b) followed by a quality suffix: "" major, m minor, dim diminished, +/aug augmented, 7 dominant 7th, maj7 major 7th, m7 minor 7th, m7b5/ø half-diminished 7th, sus2, sus4. A few common alternates (min, -, M7, …) are accepted too.

Raises ValueError for anything it can't read, so a typo surfaces at the call site rather than as a silently wrong chord.

Example:
parse_chord("Cm7")    # → Chord(root_pc=0, quality="minor_7th")
parse_chord("Dbmaj7") # → Chord(root_pc=1, quality="major_7th")
parse_chord("F#")     # → Chord(root_pc=6, quality="major")
def register_chord_quality(name: str, intervals: List[int], suffix: Optional[str] = None) -> None:
304def register_chord_quality (
305	name: str,
306	intervals: typing.List[int],
307	suffix: typing.Optional[str] = None,
308) -> None:
309
310	"""Register a custom chord quality for use everywhere chords are used.
311
312	The counterpart to :func:`subsequence.intervals.register_scale` — it opens
313	the quality table so quartal stacks, clusters, and extended chords become
314	first-class symbolic chords: they work in progressions, graphs, voice
315	leading, and ``describe()`` output.
316
317	Built-in qualities (e.g. ``"minor"``) cannot be overwritten.  Custom names
318	may be re-registered freely — live reload re-runs registration on every
319	save, so this must not raise.
320
321	Parameters:
322		name: Quality name (used as ``Chord(root_pc, quality=name)``).
323		intervals: Semitone offsets from the root (e.g. ``[0, 5, 10]`` for a
324			quartal stack, ``[0, 3, 7, 10, 14]`` for a minor 9th).  Must start
325			with 0, ascend strictly, and stay within 0–24 (extensions reach
326			past the octave).
327		suffix: Optional chord-name suffix.  When given, ``parse_chord()``
328			accepts ``"A" + suffix`` and ``Chord.name()`` prints it — so
329			``register_chord_quality("minor_9th", [0, 3, 7, 10, 14], suffix="m9")``
330			makes ``"Am9"`` parse from then on.  Must not collide with a
331			built-in suffix.
332
333	Example:
334		```python
335		import subsequence
336
337		subsequence.register_chord_quality("quartal", [0, 5, 10], suffix="q4")
338		subsequence.parse_chord("Dq4")   # → Chord(root_pc=2, quality="quartal")
339		```
340	"""
341
342	if name in _BUILTIN_QUALITY_NAMES:
343		raise ValueError(
344			f"Cannot overwrite built-in chord quality '{name}'. "
345			"Choose a different name for your custom quality."
346		)
347
348	if not intervals:
349		raise ValueError("intervals must not be empty")
350	if not all(isinstance(i, int) and not isinstance(i, bool) for i in intervals):
351		raise ValueError("intervals must be whole numbers (semitone offsets)")
352	if intervals[0] != 0:
353		raise ValueError("intervals must start with 0 (the root)")
354	if any(b <= a for a, b in zip(intervals, intervals[1:])):
355		raise ValueError("intervals must be strictly ascending")
356	if any(i < 0 or i > 24 for i in intervals):
357		raise ValueError("intervals must contain values between 0 and 24")
358
359	if suffix is not None:
360		if suffix in _BUILTIN_SUFFIXES:
361			raise ValueError(
362				f"Suffix {suffix!r} is a built-in chord suffix and cannot be reused. "
363				"Choose a different suffix for your custom quality."
364			)
365		if not suffix or suffix[0] in "ABCDEFG#b0123456789":
366			raise ValueError(
367				f"Suffix {suffix!r} would be ambiguous in a chord name — "
368				"it must not be empty or start with a note letter, accidental, or digit"
369			)
370
371	# Re-registration: drop any suffix this quality registered previously, so
372	# renaming a suffix on live reload does not leave a stale alias behind.
373	for old_suffix in [s for s, q in _SUFFIX_TO_QUALITY.items() if q == name and s not in _BUILTIN_SUFFIXES]:
374		del _SUFFIX_TO_QUALITY[old_suffix]
375
376	CHORD_INTERVALS[name] = list(intervals)
377
378	if suffix is not None:
379		CHORD_SUFFIX[name] = suffix
380		_SUFFIX_TO_QUALITY[suffix] = name
381	else:
382		CHORD_SUFFIX.pop(name, None)

Register a custom chord quality for use everywhere chords are used.

The counterpart to subsequence.intervals.register_scale() — it opens the quality table so quartal stacks, clusters, and extended chords become first-class symbolic chords: they work in progressions, graphs, voice leading, and describe() output.

Built-in qualities (e.g. "minor") cannot be overwritten. Custom names may be re-registered freely — live reload re-runs registration on every save, so this must not raise.

Arguments:
  • name: Quality name (used as Chord(root_pc, quality=name)).
  • intervals: Semitone offsets from the root (e.g. [0, 5, 10] for a quartal stack, [0, 3, 7, 10, 14] for a minor 9th). Must start with 0, ascend strictly, and stay within 0–24 (extensions reach past the octave).
  • suffix: Optional chord-name suffix. When given, parse_chord() accepts "A" + suffix and Chord.name() prints it — so register_chord_quality("minor_9th", [0, 3, 7, 10, 14], suffix="m9") makes "Am9" parse from then on. Must not collide with a built-in suffix.
Example:
import subsequence

subsequence.register_chord_quality("quartal", [0, 5, 10], suffix="q4")
subsequence.parse_chord("Dq4")   # → Chord(root_pc=2, quality="quartal")
def register_scale( name: str, intervals: List[int], qualities: Optional[List[str]] = None) -> None:
340def register_scale (
341	name: str,
342	intervals: typing.List[int],
343	qualities: typing.Optional[typing.List[str]] = None
344) -> None:
345
346	"""
347	Register a custom scale for use with ``p.snap_to_scale()`` and
348	``scale_pitch_classes()``.
349
350	Built-in scale names (e.g. ``"minor"``, ``"hirajoshi"``) cannot be
351	overwritten.  Custom names may be re-registered freely — live reload
352	re-runs registration on every save, so this must not raise.
353
354	Parameters:
355		name: Scale name (used in ``p.snap_to_scale(key, name)``).  Must not
356			be the name of a built-in scale.
357		intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]``
358			for Hirajōshi). Must be whole numbers, start with 0, ascend
359			strictly, and stay within 0–11.
360		qualities: Optional chord quality per scale degree (e.g.
361			``["minor", "major", "minor", "major", "diminished"]``).
362			Required only if you want to use the scale with
363			``diatonic_chords()`` or ``diatonic_chord_sequence()``.
364
365	Raises:
366		ValueError: If *name* is a built-in scale, or *intervals* /
367			*qualities* fail the rules above.
368
369	Example::
370
371		import subsequence
372
373		subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11])
374
375		@comp.pattern(channel=0, length=4)
376		def melody (p):
377			p.note(60, beat=0)
378			p.snap_to_scale("C", "raga_bhairav")
379	"""
380
381	if name in _BUILTIN_SCALE_NAMES:
382		raise ValueError(
383			f"Cannot overwrite built-in scale '{name}'. "
384			"Choose a different name for your custom scale."
385		)
386
387	if not intervals:
388		raise ValueError("intervals must not be empty")
389	if not all(isinstance(i, int) for i in intervals):
390		raise ValueError("intervals must be whole numbers (semitone offsets)")
391	if intervals[0] != 0:
392		raise ValueError("intervals must start with 0")
393	if any(b <= a for a, b in zip(intervals, intervals[1:])):
394		raise ValueError("intervals must be strictly ascending")
395	if any(i < 0 or i > 11 for i in intervals):
396		raise ValueError("intervals must contain values between 0 and 11")
397	if qualities is not None and len(qualities) != len(intervals):
398		raise ValueError(
399			f"qualities length ({len(qualities)}) must match "
400			f"intervals length ({len(intervals)})"
401		)
402
403	INTERVAL_DEFINITIONS[name] = intervals
404	SCALE_MODE_MAP[name] = (name, qualities)

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

Built-in scale names (e.g. "minor", "hirajoshi") cannot be overwritten. Custom names may be re-registered freely — live reload re-runs registration on every save, so this must not raise.

Arguments:
  • name: Scale name (used in p.snap_to_scale(key, name)). Must not be the name of a built-in scale.
  • intervals: Semitone offsets from the root (e.g. [0, 2, 3, 7, 8] for Hirajōshi). Must be whole numbers, start with 0, ascend strictly, and stay within 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().
Raises:
  • ValueError: If name is a built-in scale, or intervals / qualities fail the rules above.

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

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

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

Sorted list of MIDI note numbers.

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

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

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

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

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

# Fix: derive key name from root_pitch so low is always in the scale
root_pitch = 64  # E4
key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]  # → "E"
subsequence.scale_notes(key, "minor", low=root_pitch, count=4)
# → [64, 66, 67, 69]  (E F# G A — starts on the root)
def bank_select(bank: int) -> Tuple[int, int]:
195def bank_select (bank: int) -> typing.Tuple[int, int]:
196
197	"""
198	Convert a 14-bit MIDI bank number to (MSB, LSB) for use with
199	``p.program_change()``.
200
201	MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and
202	CC 32 (Bank LSB).  Together they encode a 14-bit bank number in the
203	range 0–16,383:
204
205	    MSB = bank // 128   (upper 7 bits, sent on CC 0)
206	    LSB = bank % 128    (lower 7 bits, sent on CC 32)
207
208	Args:
209		bank: Integer bank number, 0–16,383.  Values outside this range are
210		      clamped.
211
212	Returns:
213		``(msb, lsb)`` tuple, each value in 0–127.
214
215	Example:
216		```python
217		msb, lsb = subsequence.bank_select(128)   # → (1, 0)
218		p.program_change(48, bank_msb=msb, bank_lsb=lsb)
219		```
220	"""
221
222	bank = max(0, min(16383, bank))
223	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)
@dataclasses.dataclass(frozen=True)
class Definitions:
 93@dataclasses.dataclass(frozen=True)
 94class Definitions:
 95
 96	"""
 97	The name-to-number tables read from a project definitions file.
 98
 99	One plain ``dict`` per section, always present — an absent or null section
100	is an empty dict.  The dicts merge directly into the existing parameters:
101	``notes`` into ``drum_note_map=``, ``cc`` into ``cc_name_map=``, ``nrpn``
102	into ``nrpn_name_map=``, while ``channels`` values feed ``channel=`` and
103	``programs`` values feed ``p.program_change()``.
104
105	The dataclass is frozen (attributes cannot be reassigned) but the dicts
106	themselves are ordinary mutable dicts, so they can be merged and extended
107	freely.
108
109	Example:
110		```python
111		defs = subsequence.load_definitions("project.yaml")
112		defs.channels["birds"]      # 3
113		defs.notes                  # {"ride_edge_soft": 53, ...}
114		```
115	"""
116
117	notes:    typing.Dict[str, int] = dataclasses.field(default_factory=dict)
118	cc:       typing.Dict[str, int] = dataclasses.field(default_factory=dict)
119	channels: typing.Dict[str, int] = dataclasses.field(default_factory=dict)
120	programs: typing.Dict[str, int] = dataclasses.field(default_factory=dict)
121	nrpn:     typing.Dict[str, int] = dataclasses.field(default_factory=dict)

The name-to-number tables read from a project definitions file.

One plain dict per section, always present — an absent or null section is an empty dict. The dicts merge directly into the existing parameters: notes into drum_note_map=, cc into cc_name_map=, nrpn into nrpn_name_map=, while channels values feed channel= and programs values feed p.program_change().

The dataclass is frozen (attributes cannot be reassigned) but the dicts themselves are ordinary mutable dicts, so they can be merged and extended freely.

Example:
defs = subsequence.load_definitions("project.yaml")
defs.channels["birds"]      # 3
defs.notes                  # {"ride_edge_soft": 53, ...}
Definitions( notes: Dict[str, int] = <factory>, cc: Dict[str, int] = <factory>, channels: Dict[str, int] = <factory>, programs: Dict[str, int] = <factory>, nrpn: Dict[str, int] = <factory>)
notes: Dict[str, int]
cc: Dict[str, int]
channels: Dict[str, int]
programs: Dict[str, int]
nrpn: Dict[str, int]
def load_definitions(path: Union[str, pathlib.Path]) -> Definitions:
124def load_definitions (path: typing.Union[str, pathlib.Path]) -> Definitions:
125
126	"""
127	Load and validate a project definitions file.
128
129	Reads the YAML file at ``path`` and returns a :class:`Definitions` whose
130	``notes`` / ``cc`` / ``channels`` / ``programs`` / ``nrpn`` dicts merge
131	straight into pattern parameters.  See the module docstring for the file
132	format, the value ranges, and the shared-vocabulary contract with the
133	Subsample sampler.
134
135	Validation is strict inside the sections listed above and lenient outside
136	them: unknown top-level sections are ignored, while a bad name, a non-whole
137	number (including YAML ``true``/``false``), or an out-of-range value is
138	rejected with an error naming the file, section, and entry.
139
140	Parameters:
141		path: The definitions file, as a path string or ``pathlib.Path``.
142
143	Returns:
144		A :class:`Definitions` with one name-to-number dict per section.
145
146	Raises:
147		ValueError: If the file is missing, unreadable, or not valid YAML; if
148			the top level or a consumed section is not a mapping; or if a name
149			or value inside a consumed section is invalid.  File-system and
150			YAML errors are wrapped so this is the only error type raised.
151
152	Example:
153		```python
154		defs = subsequence.load_definitions("project.yaml")
155
156		@comp.pattern(channel=defs.channels["kit"], drum_note_map=defs.notes)
157		def kit (p):
158			p.hit("ride_edge_soft", beats=[1, 3])
159		```
160	"""
161
162	p = pathlib.Path(path)
163
164	try:
165		with p.open(encoding="utf-8") as fh:
166			raw = yaml.safe_load(fh)
167	except (OSError, yaml.YAMLError) as exc:
168		raise ValueError(
169			f"definitions file {p} could not be read: {exc}"
170		) from exc
171
172	if raw is None:
173		return Definitions()
174
175	if not isinstance(raw, dict):
176		raise ValueError(
177			f"definitions file {p}: top level must be a mapping of "
178			f"sections (notes:, cc:, …), got {type(raw).__name__}"
179		)
180
181	tables: typing.Dict[str, typing.Dict[str, int]] = {}
182
183	for section in sorted(CONSUMED_SECTIONS):
184		section_raw = raw.get(section)
185
186		if section_raw is None:
187			continue
188
189		if not isinstance(section_raw, dict):
190			raise ValueError(
191				f"definitions file {p}: section {section!r} must be a "
192				f"mapping of name to number "
193				f"(got {type(section_raw).__name__})"
194			)
195
196		lo, hi = _SECTION_RANGES[section]
197		table: typing.Dict[str, int] = {}
198
199		for name_raw, value in section_raw.items():
200			name = str(name_raw)
201
202			if not _NAME_RE.fullmatch(name):
203				raise ValueError(
204					f"definitions file {p}: section {section!r}: name "
205					f"{name!r} must match [a-z][a-z0-9_]* (lowercase "
206					f"letters, digits, underscores — no dots)"
207				)
208
209			# bool is an int subclass — reject it first so ``x: true`` fails
210			# loudly instead of quietly becoming 1.
211			if isinstance(value, bool) or not isinstance(value, int):
212				raise ValueError(
213					f"definitions file {p}: section {section!r}: "
214					f"{name!r} must be a whole number (got {value!r})"
215				)
216
217			if not lo <= value <= hi:
218				raise ValueError(
219					f"definitions file {p}: section {section!r}: "
220					f"{name!r} = {value} is outside [{lo}, {hi}]"
221				)
222
223			table[name] = value
224
225		tables[section] = table
226
227	return Definitions(
228		notes    = tables.get("notes", {}),
229		cc       = tables.get("cc", {}),
230		channels = tables.get("channels", {}),
231		programs = tables.get("programs", {}),
232		nrpn     = tables.get("nrpn", {}),
233	)

Load and validate a project definitions file.

Reads the YAML file at path and returns a Definitions whose notes / cc / channels / programs / nrpn dicts merge straight into pattern parameters. See the module docstring for the file format, the value ranges, and the shared-vocabulary contract with the Subsample sampler.

Validation is strict inside the sections listed above and lenient outside them: unknown top-level sections are ignored, while a bad name, a non-whole number (including YAML true/false), or an out-of-range value is rejected with an error naming the file, section, and entry.

Arguments:
  • path: The definitions file, as a path string or pathlib.Path.
Returns:

A Definitions with one name-to-number dict per section.

Raises:
  • ValueError: If the file is missing, unreadable, or not valid YAML; if the top level or a consumed section is not a mapping; or if a name or value inside a consumed section is invalid. File-system and YAML errors are wrapped so this is the only error type raised.
Example:
defs = subsequence.load_definitions("project.yaml")

@comp.pattern(channel=defs.channels["kit"], drum_note_map=defs.notes)
def kit (p):
        p.hit("ride_edge_soft", beats=[1, 3])
def sieve(classes: Sequence[Tuple[int, int]], hi: int, lo: int = 0) -> List[int]:
3076def sieve (
3077	classes: typing.Sequence[typing.Tuple[int, int]],
3078	hi: int,
3079	lo: int = 0,
3080) -> typing.List[int]:
3081
3082	"""Xenakis sieve: the sorted integers in ``[lo, hi)`` in any of the classes.
3083
3084	A sieve (Xenakis's *crible*) is a logical formula over **residual
3085	classes** that denotes a subset of the integers.  This primary form takes
3086	a list of ``(modulus, residue)`` pairs and returns their **union** over a
3087	bounded range — every ``x`` in ``[lo, hi)`` with ``x % modulus == residue``
3088	for at least one class.  The integers index *any* ordered parameter, so
3089	one kernel builds custom scales (over 0–11 semitones), non-octave pitch
3090	pools, rhythm grids, and bar-selection masks.
3091
3092	For intersection and complement, compose :func:`residual_class` objects
3093	with ``&``, ``|``, ``~`` and evaluate the result (see :class:`Sieve`).
3094
3095	Parameters:
3096		classes: ``(modulus, residue)`` pairs.  ``modulus`` must be ≥ 1; the
3097			residue is taken modulo the modulus.
3098		hi: Exclusive upper bound.
3099		lo: Inclusive lower bound (default 0).
3100
3101	Returns:
3102		The sorted, de-duplicated integers in range that satisfy any class.
3103
3104	Raises:
3105		ValueError: If a modulus is below 1.
3106
3107	Example:
3108		```python
3109		sieve([(12, 0), (12, 2), (12, 4), (12, 5), (12, 7), (12, 9), (12, 11)], hi=12)
3110		# → [0, 2, 4, 5, 7, 9, 11]  — the major scale as a sieve
3111		sieve([(2, 0)], hi=12)          # → [0, 2, 4, 6, 8, 10]  — whole-tone
3112		sieve([(5, 0), (7, 1)], lo=60, hi=96)   # a non-octave pitch pool
3113		```
3114	"""
3115
3116	for modulus, _residue in classes:
3117		if modulus < 1:
3118			raise ValueError(f"sieve modulus must be at least 1 — got {modulus}")
3119
3120	hits = {
3121		x
3122		for x in range(lo, hi)
3123		for modulus, residue in classes
3124		if x % modulus == residue % modulus
3125	}
3126
3127	return sorted(hits)

Xenakis sieve: the sorted integers in [lo, hi) in any of the classes.

A sieve (Xenakis's crible) is a logical formula over residual classes that denotes a subset of the integers. This primary form takes a list of (modulus, residue) pairs and returns their union over a bounded range — every x in [lo, hi) with x % modulus == residue for at least one class. The integers index any ordered parameter, so one kernel builds custom scales (over 0–11 semitones), non-octave pitch pools, rhythm grids, and bar-selection masks.

For intersection and complement, compose residual_class() objects with &, |, ~ and evaluate the result (see Sieve).

Arguments:
  • classes: (modulus, residue) pairs. modulus must be ≥ 1; the residue is taken modulo the modulus.
  • hi: Exclusive upper bound.
  • lo: Inclusive lower bound (default 0).
Returns:

The sorted, de-duplicated integers in range that satisfy any class.

Raises:
  • ValueError: If a modulus is below 1.
Example:
sieve([(12, 0), (12, 2), (12, 4), (12, 5), (12, 7), (12, 9), (12, 11)], hi=12)
# → [0, 2, 4, 5, 7, 9, 11]  — the major scale as a sieve
sieve([(2, 0)], hi=12)          # → [0, 2, 4, 6, 8, 10]  — whole-tone
sieve([(5, 0), (7, 1)], lo=60, hi=96)   # a non-octave pitch pool
def residual_class(modulus: int, residue: int) -> subsequence.sequence_utils.Sieve:
3187def residual_class (modulus: int, residue: int) -> Sieve:
3188
3189	"""A single residual class ``{x : x % modulus == residue}`` as a :class:`Sieve`.
3190
3191	The atom of sieve algebra (Xenakis's notation ``modulus @ residue``).
3192	Combine with ``&`` ``|`` ``~`` and call :meth:`Sieve.evaluate`.
3193	"""
3194
3195	if modulus < 1:
3196		raise ValueError(f"residual-class modulus must be at least 1 — got {modulus}")
3197
3198	reduced = residue % modulus
3199
3200	return Sieve(lambda x: x % modulus == reduced)

A single residual class {x : x % modulus == residue} as a Sieve.

The atom of sieve algebra (Xenakis's notation modulus @ residue). Combine with & | ~ and call Sieve.evaluate().