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, PlacedNote, generators, describe_generator, transforms, describe_transform.

  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``, ``PlacedNote``, ``generators``, ``describe_generator``, ``transforms``, ``describe_transform``.
181"""
182
183import subsequence.cadences
184import subsequence.catalogue
185import subsequence.chords
186import subsequence.forms
187import subsequence.roles
188import subsequence.composition
189import subsequence.definitions
190import subsequence.groove
191import subsequence.harmonic_rhythm
192import subsequence.intervals
193import subsequence.melodic_state
194import subsequence.midi_utils
195import subsequence.motifs
196import subsequence.pattern
197import subsequence.progressions
198import subsequence.sequence_utils
199import subsequence.tuning
200
201
202Composition = subsequence.composition.Composition
203Motif = subsequence.motifs.Motif
204Phrase = subsequence.motifs.Phrase
205motif = subsequence.motifs.motif
206sentence = subsequence.motifs.sentence
207period = subsequence.motifs.period
208Cadence = subsequence.cadences.Cadence
209Section = subsequence.forms.Section
210Form = subsequence.forms.Form
211Degree = subsequence.motifs.Degree
212ChordTone = subsequence.motifs.ChordTone
213Approach = subsequence.motifs.Approach
214MotifEvent = subsequence.motifs.MotifEvent
215ControlEvent = subsequence.motifs.ControlEvent
216Progression = subsequence.progressions.Progression
217ChordSpan = subsequence.progressions.ChordSpan
218PitchSet = subsequence.progressions.PitchSet
219progression = subsequence.progressions.progression
220Chord = subsequence.chords.Chord
221Groove = subsequence.groove.Groove
222MelodicState = subsequence.melodic_state.MelodicState
223Tuning = subsequence.tuning.Tuning
224between = subsequence.harmonic_rhythm.between
225parse_chord = subsequence.chords.parse_chord
226register_chord_quality = subsequence.chords.register_chord_quality
227register_scale = subsequence.intervals.register_scale
228scale_notes = subsequence.intervals.scale_notes
229bank_select = subsequence.midi_utils.bank_select
230Definitions = subsequence.definitions.Definitions
231load_definitions = subsequence.definitions.load_definitions
232roles = subsequence.roles
233sieve = subsequence.sequence_utils.sieve
234residual_class = subsequence.sequence_utils.residual_class
235
236# One note read back off a pattern being built, so a caller can type
237# against what PatternBuilder.placed() returns.
238PlacedNote = subsequence.pattern.PlacedNote
239
240# The generator catalogue — what this package offers and what each one takes,
241# as plain data, so a control surface never holds its own list of parameters.
242generators = subsequence.catalogue.generators
243describe_generator = subsequence.catalogue.describe_generator
244
245# The transform catalogue — the same self-description for the verbs that
246# reshape notes already placed, so a surface never holds its own list.
247transforms = subsequence.catalogue.transforms
248describe_transform = subsequence.catalogue.describe_transform
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 pause (self) -> None:
3919
3920		"""
3921		Hold playback where it is, keeping the composition's place.
3922
3923		The clock stops advancing, sounding notes are released, and MIDI Stop
3924		is sent to any hardware following the clock output.  :meth:`resume`
3925		continues from the same pulse, beat and bar — where stopping and
3926		playing again would start the piece over.
3927
3928		Bar and cycle counters hold too, so patterns resume mid-phrase rather
3929		than jumping.  A note cut short by the pause is not re-struck on
3930		resume; it returns on its pattern's next cycle.
3931
3932		Idempotent and safe to call from any thread.  Ignored, with a log line,
3933		when the transport is not ours to hold — under ``clock_follow=True`` or
3934		an active Ableton Link session.
3935		"""
3936
3937		self._sequencer.pause()
3938
3939	def resume (self) -> None:
3940
3941		"""
3942		Continue playback from where :meth:`pause` held it.
3943
3944		Sends MIDI Continue rather than Start, so downstream hardware picks up
3945		where it left off instead of resetting to the top of its own pattern.
3946		Idempotent.
3947		"""
3948
3949		self._sequencer.resume()
3950
3951	@property
3952	def is_paused (self) -> bool:
3953
3954		"""True while playback is held by :meth:`pause`."""
3955
3956		return self._sequencer.paused
3957
3958	def mute (self, name: str) -> None:
3959
3960		"""
3961		Mute a running pattern by name.
3962		
3963		The pattern continues to 'run' and increment its cycle count in 
3964		the background, but it will not produce any MIDI notes until unmuted.
3965
3966		Parameters:
3967			name: The function name of the pattern to mute.
3968		"""
3969
3970		if name not in self._running_patterns:
3971			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3972
3973		# The performer takes ownership: if a transition's approach window had
3974		# muted this pattern, drop it from that set so the section boundary
3975		# does not silently unmute it ("performer mutes win").
3976		self._transition_muted.discard(name)
3977
3978		self._running_patterns[name]._muted = True
3979		logger.info(f"Muted pattern: {name}")
3980
3981	def unmute (self, name: str) -> None:
3982
3983		"""
3984		Unmute a previously muted pattern.
3985		"""
3986
3987		if name not in self._running_patterns:
3988			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3989
3990		# Symmetric ownership claim: an explicit unmute means the transition
3991		# machinery should no longer manage this pattern at the boundary.
3992		self._transition_muted.discard(name)
3993
3994		self._running_patterns[name]._muted = False
3995		logger.info(f"Unmuted pattern: {name}")
3996
3997	def unregister (self, name: str) -> None:
3998
3999		"""Fully remove a running pattern from rotation.
4000
4001		Unlike ``mute()`` (which keeps the pattern alive but silent),
4002		``unregister()`` tears the pattern down entirely.  It sets
4003		``pattern._removed = True`` so the sequencer's reschedule loop
4004		skips re-adding it on the next pulse; sends ``note_off`` for any
4005		of the pattern's currently-sounding notes on the primary
4006		destination AND on every mirror destination (so drones and
4007		sustaining notes stop immediately); and removes the entry from
4008		``_running_patterns`` so it no longer appears in ``live_info()``,
4009		the terminal grid, or any other consumer that enumerates running
4010		patterns.
4011
4012		Already-queued events in the sequencer's event queue play out —
4013		note_offs are paired with their note_ons at queue time, so notes
4014		end at their natural duration; only drones rely on the targeted
4015		``_stop_pattern_notes`` pass.
4016
4017		Idempotent: silently logs a ``debug`` and returns if the pattern
4018		is already absent.  Useful from both the live REPL
4019		(``composition.live()``) and the file watcher
4020		(``composition.watch()``), which calls this for any pattern
4021		removed from the watched file between reloads.
4022
4023		Parameters:
4024			name: Function name of the pattern to remove.
4025		"""
4026
4027		if name not in self._running_patterns:
4028			logger.debug(f"unregister() no-op: pattern '{name}' not running")
4029			return
4030
4031		pattern = self._running_patterns[name]
4032
4033		# Mark for removal first so the reschedule loop sees the flag even if
4034		# it fires concurrently with the note-off pass below.
4035		pattern._removed = True
4036
4037		# Stop sustaining notes (including drones) on every destination this
4038		# pattern outputs to.  Fire-and-forget across threads via the event
4039		# loop; ``_stop_pattern_notes`` acquires the queue lock internally.
4040		if self._sequencer._event_loop is not None:
4041			asyncio.run_coroutine_threadsafe(
4042				self._sequencer._stop_pattern_notes(pattern),
4043				loop = self._sequencer._event_loop,
4044			)
4045
4046		def _finalise_removal () -> None:
4047			self._running_patterns.pop(name, None)
4048
4049			# Forget any pending (not-yet-graduated) declaration too, so a
4050			# later live reload cannot resurrect the pattern.
4051			self._pending_patterns = [
4052				pending for pending in self._pending_patterns
4053				if pending.builder_fn.__name__ != name
4054			]
4055
4056			logger.info(f"Unregistered pattern: {name}")
4057
4058		# The running-patterns dict is iterated by the display, web UI, and
4059		# reschedule loop on the event loop thread — mutate it there when this
4060		# call arrives from another thread (e.g. the live TCP server).
4061		loop = self._sequencer._event_loop
4062
4063		try:
4064			on_loop = loop is not None and asyncio.get_running_loop() is loop
4065		except RuntimeError:
4066			on_loop = False
4067
4068		if loop is not None and loop.is_running() and not on_loop:
4069			loop.call_soon_threadsafe(_finalise_removal)
4070		else:
4071			_finalise_removal()
4072
4073	def mirror (self, name: str, device: int, channel: int, drum_note_map: typing.Optional[typing.Dict[str, int]] = None) -> None:
4074
4075		"""
4076		Add a mirror destination to a running pattern.
4077
4078		Every note, CC, pitch bend, NRPN/RPN, program change, SysEx, and drone
4079		event the pattern emits will also be sent to ``(device, channel)``,
4080		starting from the next cycle rebuild.  Idempotent on ``(device, channel)``
4081		— calling with the same destination twice does not double-fan; calling
4082		again with a different ``drum_note_map`` re-points it in place.
4083
4084		Parameters:
4085			name: Function name of the pattern to mirror.
4086			device: Output device index (the integer returned from
4087				``midi_output()``; 0 = primary device).
4088			channel: MIDI channel using this composition's numbering convention
4089				(1-16 by default; 0-15 if ``zero_indexed_channels=True``).
4090			drum_note_map: Optional per-destination drum map.  When set, mirrored
4091				drum hits are re-resolved by name through it, so a named voice
4092				lands on this device's own note number — see the README
4093				"MIDI mirroring" section.
4094
4095		Bandwidth: each mirror adds another full copy of the pattern's events.
4096		See the README "MIDI mirroring" section for the tradeoffs.
4097		"""
4098
4099		if name not in self._running_patterns:
4100			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4101
4102		resolved_channel = self._resolve_channel(channel)
4103		prefix = (device, resolved_channel)
4104		entry: subsequence.pattern.MirrorSpec = prefix if drum_note_map is None else (device, resolved_channel, drum_note_map)
4105
4106		pattern = self._running_patterns[name]
4107
4108		# Mirror-to-self check: comparing the (device, channel) prefix against the
4109		# live pattern's resolved destination.  Unlike the decorator path this is
4110		# always concrete.
4111		if prefix == (pattern.device, pattern.channel):
4112			logger.warning(
4113				f"Mirror destination {prefix} matches '{name}'s primary destination "
4114				f"— every event will double-fire on this (device, channel).  This is almost "
4115				f"certainly unintended."
4116			)
4117
4118		# Idempotent on (device, channel): replace any existing entry for the same
4119		# destination (so its map can be re-pointed), else append.
4120		existing_index = next((idx for idx, e in enumerate(pattern.mirrors) if (e[0], e[1]) == prefix), None)
4121		if existing_index is None:
4122			pattern.mirrors.append(entry)
4123			logger.info(f"Mirror added: {name} -> device={device}, channel={resolved_channel}")
4124		elif pattern.mirrors[existing_index] != entry:
4125			pattern.mirrors[existing_index] = entry
4126			logger.info(f"Mirror updated: {name} -> device={device}, channel={resolved_channel}")
4127		else:
4128			logger.debug(f"Mirror already present on {name}: device={device}, channel={resolved_channel}")
4129
4130	def unmirror (self, name: str, device: int, channel: int) -> None:
4131
4132		"""
4133		Remove a single mirror destination from a running pattern.
4134
4135		Matches on ``(device, channel)`` only — any attached ``drum_note_map`` is
4136		ignored.  Idempotent: silently does nothing if the destination is not
4137		currently mirrored.  The change applies on the next cycle rebuild.
4138		"""
4139
4140		if name not in self._running_patterns:
4141			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4142
4143		resolved_channel = self._resolve_channel(channel)
4144		prefix = (device, resolved_channel)
4145
4146		pattern = self._running_patterns[name]
4147
4148		filtered = [e for e in pattern.mirrors if (e[0], e[1]) != prefix]
4149		if len(filtered) != len(pattern.mirrors):
4150			pattern.mirrors[:] = filtered
4151			logger.info(f"Mirror removed: {name} -> device={device}, channel={resolved_channel}")
4152		else:
4153			logger.debug(f"unmirror() no-op on {name}: device={device}, channel={resolved_channel} not in mirrors")
4154
4155	def unmirror_all (self, name: str) -> None:
4156
4157		"""
4158		Remove every mirror destination from a running pattern.
4159		"""
4160
4161		if name not in self._running_patterns:
4162			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4163
4164		pattern = self._running_patterns[name]
4165
4166		if pattern.mirrors:
4167			pattern.mirrors.clear()
4168			logger.info(f"All mirrors cleared on pattern: {name}")
4169
4170	def tweak (self, name: str, **kwargs: typing.Any) -> None:
4171
4172		"""Override parameters for a running pattern.
4173
4174		Values set here are available inside the pattern's builder
4175		function via ``p.param()``.  They persist across rebuilds
4176		until explicitly changed or cleared.  Changes take effect
4177		on the next rebuild cycle.
4178
4179		Parameters:
4180			name: The function name of the pattern.
4181			``**kwargs``: Parameter names and their new values.
4182
4183		Example (from the live REPL)::
4184
4185			composition.tweak("bass", pitches=[48, 52, 55, 60])
4186		"""
4187
4188		if name not in self._running_patterns:
4189			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4190
4191		self._running_patterns[name]._tweaks.update(kwargs)
4192		logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}")
4193
4194	def clear_tweak (self, name: str, *param_names: str) -> None:
4195
4196		"""Remove tweaked parameters from a running pattern.
4197
4198		If no parameter names are given, all tweaks for the pattern
4199		are cleared and every ``p.param()`` call reverts to its
4200		default.
4201
4202		Parameters:
4203			name: The function name of the pattern.
4204			*param_names: Specific parameter names to clear.  If
4205				omitted, all tweaks are removed.
4206		"""
4207
4208		if name not in self._running_patterns:
4209			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4210
4211		if not param_names:
4212			self._running_patterns[name]._tweaks.clear()
4213			logger.info(f"Cleared all tweaks for pattern '{name}'")
4214		else:
4215			for param_name in param_names:
4216				self._running_patterns[name]._tweaks.pop(param_name, None)
4217			logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}")
4218
4219	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
4220
4221		"""Return a copy of the current tweaks for a running pattern.
4222
4223		Parameters:
4224			name: The function name of the pattern.
4225		"""
4226
4227		if name not in self._running_patterns:
4228			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4229
4230		return dict(self._running_patterns[name]._tweaks)
4231
4232	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
4233
4234		"""
4235		Register a custom function to run on a repeating beat-based cycle.
4236
4237		Subsequence automatically runs synchronous functions in a thread pool
4238		so they don't block the timing-critical MIDI clock. Async functions
4239		are run directly on the event loop.
4240
4241		Parameters:
4242			fn: The function to call.
4243			cycle_beats: How often to call it (e.g., 4 = every bar).
4244			reschedule_lookahead: How far in advance to schedule the next call.
4245			wait_for_initial: If True, run the function once during startup
4246				and wait for it to complete before playback begins. This
4247				ensures ``composition.data`` is populated before patterns
4248				first build. Implies ``defer=True`` for the repeating
4249				schedule.
4250			defer: If True, skip the pulse-0 fire and defer the first
4251				repeating call to just before the second cycle boundary.
4252
4253		Raises:
4254			RuntimeError: If called after ``play()`` has started — scheduled
4255				tasks register at startup, so a late registration would be
4256				silently ignored otherwise.
4257		"""
4258
4259		if self._sequencer.running:
4260			raise RuntimeError("schedule() must be called before play() - scheduled tasks register at startup")
4261
4262		self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer))
4263
4264	def form (
4265		self,
4266		sections: typing.Union[
4267			"subsequence.forms.Form",
4268			typing.List[typing.Any],
4269			typing.Iterator[typing.Tuple[str, int]],
4270			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
4271		],
4272		loop: bool = False,
4273		start: typing.Optional[str] = None,
4274		at_end: str = "stop",
4275		key: typing.Optional[str] = None,
4276		scale: typing.Optional[str] = None,
4277	) -> None:
4278
4279		"""
4280		Define the structure (sections) of the composition.
4281
4282		You can define form in four ways:
4283
4284		1. **Form value**: a frozen :class:`~subsequence.forms.Form` of
4285		   :class:`~subsequence.forms.Section` values — the payload home
4286		   (energy, key per section); editable, navigable.
4287		2. **Sequence (List)**: a fixed order of ``(name, bars)`` tuples
4288		   or Sections (lists coerce — they are the same form).
4289		3. **Graph (Dict)**: dynamic transitions based on weights.
4290		4. **Generator**: a Python generator that yields ``(name, bars)`` pairs.
4291
4292		Form-value and list forms are **navigable**: ``form_jump()`` and
4293		``form_next()`` work on them (the jump lands on the next occurrence
4294		of the name, wrapping).
4295
4296		Re-binding ``form()`` during playback takes effect at the next bar —
4297		the clock reads the current form state on every bar, so the new form
4298		advances from there (its first section plays from its first bar).
4299
4300		Parameters:
4301			sections: The form definition (Form, List, Dict, or Generator).
4302			loop: Sugar for ``at_end="loop"``.
4303			start: The section to start with (Graph mode only).
4304			at_end: What happens when a sequence form runs out —
4305				``"stop"`` (the form finishes and patterns see no section;
4306				default), ``"hold"`` (the final section repeats until
4307				navigated away from), or ``"loop"`` (start over).  Graphs
4308				end via their terminal sections instead.
4309			key: A form-level key — the **form tier** of the key-source
4310				chain (``Section.key`` overrides it; it overrides the
4311				composition key).  Re-anchors key-relative content for the
4312				whole form.  When *sections* is a ``Form`` value carrying its
4313				own ``key``, that value is used unless this argument overrides.
4314			scale: A form-level scale/mode, paired with ``key``.
4315
4316		Example:
4317			```python
4318			# A simple pop structure
4319			comp.form([
4320				("verse", 8),
4321				("chorus", 8),
4322				("verse", 8),
4323				("chorus", 16)
4324			])
4325
4326			# The same structure with payloads, held open at the end
4327			S = subsequence.Section
4328			comp.form(subsequence.Form([
4329				S("verse", 8, energy=0.5), S("chorus", 8, energy=0.9),
4330			]), at_end="hold")
4331			```
4332		"""
4333
4334		# Seed FormState at form() time (per-call salt) so build-time walks —
4335		# the frozen clones form_freeze will take — are deterministic without
4336		# play(); the play-time stream is re-dealt name-keyed in _run().
4337		self._form_count += 1
4338
4339		self._form_state = subsequence.form_state.FormState(
4340			sections,
4341			loop = loop,
4342			start = start,
4343			rng = self._stream(f"form:{self._form_count}"),
4344			at_end = at_end,
4345		)
4346
4347		# A Form value carries energy payloads — that counts as an energy
4348		# source for the min_energy registration check in _run().
4349		self._form_has_payload = isinstance(sections, subsequence.forms.Form) or (
4350			isinstance(sections, list) and any(isinstance(element, subsequence.forms.Section) for element in sections)
4351		)
4352
4353		# Form-tier key/scale: an explicit argument wins; otherwise a Form
4354		# value's own key/scale seeds the tier.  Re-binding the form drops any
4355		# stale per-section resolution cache.
4356		if isinstance(sections, subsequence.forms.Form):
4357			self._form_key = key if key is not None else sections.key
4358			self._form_scale = scale if scale is not None else sections.scale
4359		else:
4360			self._form_key = key
4361			self._form_scale = scale
4362
4363		self._resolved_section_cache = {}
4364
4365	def form_freeze (self, sections: typing.Optional[int] = None) -> "subsequence.forms.Form":
4366
4367		"""Freeze the graph form's walk into an editable :class:`~subsequence.forms.Form`.
4368
4369		Walks a **clone** of the live form state — the same RNG state, so the
4370		frozen path is exactly the path the live graph would have played —
4371		and returns it as a Form value: inspect it, edit it
4372		(``path.replace(3, bars=16)``), and rebind it with
4373		``composition.form(path, at_end=...)``.  The live form state is
4374		untouched (rebinding replaces it).
4375
4376		Parameters:
4377			sections: Number of sections to freeze.  Without it, the walk
4378				runs until a terminal section; a graph with no terminal
4379				sections requires ``sections=`` explicitly.
4380
4381		Raises:
4382			ValueError: If no graph form is bound (a list form is already a
4383				frozen sequence), the form has already finished, or the walk
4384				cannot terminate.
4385
4386		Example::
4387
4388			composition.form({...}, start="intro")
4389			path = composition.form_freeze()          # the walk, frozen
4390			composition.form(path, at_end="stop")     # rebind the editable value
4391		"""
4392
4393		fs = self._form_state
4394
4395		if fs is None or fs._graph is None or fs._section_bars is None:
4396			raise ValueError(
4397				"form_freeze() freezes a graph form's walk — call form() with a dict first "
4398				"(a list form is already a frozen sequence)"
4399			)
4400
4401		if fs._current is None:
4402			raise ValueError("the form has already finished — nothing left to freeze")
4403
4404		if sections is not None and sections < 1:
4405			raise ValueError("sections must be at least 1")
4406
4407		if sections is None and not fs._terminal_sections:
4408			raise ValueError(
4409				"this graph has no terminal section, so the walk would never end — "
4410				"pass sections=n to bound it"
4411			)
4412
4413		# Clone the RNG state: the frozen walk reproduces the live form's
4414		# future draws without consuming them.
4415		rng = random.Random()
4416		rng.setstate(fs._rng.getstate())
4417
4418		walked = [fs._current]
4419		next_name = fs._next_section_name		# already decided by the live state
4420
4421		while next_name is not None:
4422			if sections is not None and len(walked) >= sections:
4423				break
4424			if sections is None and len(walked) >= 10000:
4425				raise ValueError(
4426					"form_freeze() walked 10000 sections without reaching a terminal — "
4427					"the terminals look unreachable; pass sections=n to bound the walk"
4428				)
4429
4430			walked.append(subsequence.forms.Section(name = next_name, bars = fs._section_bars[next_name]))
4431			next_name = None if next_name in fs._terminal_sections else fs._graph.choose_next(next_name, rng)
4432
4433		# Carry the form-tier key/scale onto the frozen value so a freeze →
4434		# rebind round-trip is lossless (an explicit form(key=) on rebind
4435		# still overrides).
4436		return subsequence.forms.Form(walked, key = self._form_key, scale = self._form_scale)
4437
4438	def energy (self, energies: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]]) -> None:
4439
4440		"""Set per-section energy — the arranging dial, as one plain dict.
4441
4442		``{"verse": 0.5, "chorus": 0.9, "build": (0.3, 1.0)}`` — a float is
4443		the section's level; a ``(start, end)`` tuple interpolates across the
4444		section (a build).  Patterns read ``p.energy`` (0.5 when nothing is
4445		configured) and gate themselves, or declare ``min_energy=`` on
4446		``pattern()`` for automatic muting.
4447
4448		The dict **overrides** any energy payload carried by bound
4449		:class:`~subsequence.forms.Section` values — it is the later,
4450		performance-level dial.  Re-calling replaces the whole mapping
4451		(idempotent, live-reload friendly).
4452
4453		Example::
4454
4455			composition.energy({"intro": 0.2, "verse": 0.55, "drop": 0.95})
4456		"""
4457
4458		validated: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]] = {}
4459
4460		for name, value in energies.items():
4461			if isinstance(value, tuple):
4462				if len(value) != 2:
4463					raise ValueError(f"energy ramp for {name!r} must be (start, end), got {value!r}")
4464				start_level, end_level = float(value[0]), float(value[1])
4465				for level in (start_level, end_level):
4466					if not 0.0 <= level <= 1.0:
4467						raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4468				validated[name] = (start_level, end_level)
4469			else:
4470				level = float(value)
4471				if not 0.0 <= level <= 1.0:
4472					raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4473				validated[name] = level
4474
4475		self._energy_map = validated
4476
4477	def _current_energy (self, info: typing.Optional[subsequence.form_state.SectionInfo]) -> float:
4478
4479		"""Resolve the energy for a section snapshot.
4480
4481		Priority: the ``energy()`` dict (ramps interpolate by section
4482		progress) > the bound Section payload > 0.5.
4483		"""
4484
4485		if info is None:
4486			return 0.5
4487
4488		spec = self._energy_map.get(info.name)
4489
4490		if spec is None:
4491			return info.energy
4492
4493		if isinstance(spec, tuple):
4494			start_level, end_level = spec
4495
4496			# A build reaches its declared end ON the final bar, so the ramp spans
4497			# bar 0 → bar (bars-1).  (info.progress is bar/bars, which would top
4498			# out one bar short and never deliver end.)  A one-bar section sits at
4499			# the destination level.
4500			span = info.bars - 1
4501			fraction = info.bar / span if span > 0 else 1.0
4502
4503			return start_level + (end_level - start_level) * fraction
4504
4505		return spec
4506
4507	def on_section (self, callback: typing.Callable[..., typing.Any]) -> None:
4508
4509		"""Register a callback fired on every section change.
4510
4511		The callback receives the new :class:`~subsequence.form_state.SectionInfo`
4512		(or ``None`` when the form finishes).  It fires from the form clock,
4513		one lookahead-beat **early** — in time to affect the new section's
4514		first patterns — and once at play start for the opening section.
4515
4516		Example::
4517
4518			composition.on_section(lambda info: print(f"now: {info.name if info else 'end'}"))
4519		"""
4520
4521		self.on_event("section", callback)
4522
4523	def transition (
4524		self,
4525		before: str,
4526		fill: typing.Optional[typing.Any] = None,
4527		channel: typing.Optional[int] = None,
4528		beat: float = 0.0,
4529		mute: typing.Optional[typing.List[str]] = None,
4530		beats: typing.Optional[float] = None,
4531		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4532		device: subsequence.midi_utils.DeviceId = None,
4533	) -> None:
4534
4535		"""Declare boundary material — the automatic fill or mute, one line.
4536
4537		``before`` names the incoming section (``"chorus"``), or ``"*"`` for
4538		any *different* section (repeats don't fire it).  Two actions,
4539		combinable:
4540
4541		- ``fill=`` (+ ``channel=``, ``beat=``): a Motif played in the last
4542		  bar before the boundary, starting at ``beat`` of that bar.  Drum
4543		  names resolve through ``drum_note_map=`` if given, otherwise the
4544		  map is borrowed from a registered pattern on the same channel.
4545		- ``mute=`` (+ ``beats=``): pattern names muted over the approach
4546		  and unmuted at the boundary.  Muting is **bar-granular** (the
4547		  existing rule), so ``beats`` rounds up to whole bars.  Performer
4548		  mutes win: a pattern you muted yourself stays muted.
4549
4550		Transitions stack — call once per rule.  Registration is additive
4551		and idempotent per identical rule.
4552
4553		Example::
4554
4555			composition.transition(before="*", fill=FILL, channel=10, beat=2.0)
4556			composition.transition(before="drop", mute=["pads"], beats=4)
4557		"""
4558
4559		if fill is None and mute is None:
4560			raise ValueError("transition() needs fill= and/or mute= — it declares what happens at the boundary")
4561
4562		if fill is not None:
4563			if channel is None:
4564				raise ValueError("transition(fill=) needs channel= — the fill must land somewhere")
4565			if not hasattr(fill, "events") or not hasattr(fill, "length"):
4566				raise TypeError(f"fill must be a Motif-like value with .events/.length, got {type(fill).__name__}")
4567
4568		if mute is not None and beats is None:
4569			beats = float(self.time_signature[0])		# one bar by default
4570
4571		rule = _Transition(
4572			before = before,
4573			fill = fill,
4574			channel = self._resolve_channel(channel) if channel is not None else None,
4575			beat = float(beat),
4576			mute = list(mute) if mute is not None else None,
4577			beats = beats,
4578			drum_note_map = drum_note_map,
4579			device = device,			# resolved at fire time — names aren't known until play()
4580		)
4581
4582		if rule not in self._transitions:
4583			self._transitions.append(rule)
4584
4585	def _transition_drum_map (self, channel: typing.Optional[int]) -> typing.Optional[typing.Dict[str, int]]:
4586
4587		"""Borrow a drum map from a registered pattern on the same channel."""
4588
4589		if channel is None:
4590			return None
4591
4592		for pending in self._pending_patterns:
4593			if pending.channel == channel and pending.drum_note_map:
4594				return pending.drum_note_map
4595
4596		for running in self._running_patterns.values():
4597			candidate = getattr(running, "_drum_note_map", None)
4598			if running.channel == channel and candidate:
4599				return typing.cast(typing.Dict[str, int], candidate)
4600
4601		return None
4602
4603	def _fire_fill (self, rule: _Transition, start_pulse: int) -> None:
4604
4605		"""Build a transition fill as a one-shot pattern and schedule it."""
4606
4607		assert rule.fill is not None and rule.channel is not None
4608
4609		drum_map = rule.drum_note_map if rule.drum_note_map is not None else self._transition_drum_map(rule.channel)
4610
4611		pattern = subsequence.pattern.Pattern(
4612			channel = rule.channel,
4613			length = float(rule.fill.length),
4614			device = self._resolve_device_id(rule.device),
4615		)
4616
4617		harmony_view: typing.Optional[HarmonyView] = None
4618		if not self._harmony_horizon.is_empty:
4619			harmony_view = HarmonyView(self._harmony_horizon, start_pulse / self._sequencer.pulses_per_beat)
4620
4621		# The fill sounds in the outgoing section's final bar, so a degree-
4622		# bearing fill resolves against THAT section's effective key/scale —
4623		# previously it took the composition key, ignoring the section.
4624		fill_section = self._form_state.get_section_info() if self._form_state else None
4625		fill_key, fill_scale = self._effective_key_scale(fill_section)
4626
4627		builder = subsequence.pattern_builder.PatternBuilder(
4628			pattern = pattern,
4629			cycle = 0,
4630			drum_note_map = drum_map,
4631			section = fill_section,
4632			bar = self._builder_bar,
4633			conductor = self.conductor,
4634			rng = self._stream(f"transition:{rule.before}:{start_pulse}") or random.Random(),
4635			tweaks = {},
4636			default_grid = 16,
4637			data = self.data,
4638			key = fill_key,
4639			scale = fill_scale,
4640			time_signature = self.time_signature,
4641			harmony = harmony_view,
4642		)
4643
4644		try:
4645			builder.motif(rule.fill)
4646		except Exception:
4647			logger.exception("transition fill failed to build — the boundary plays without it")
4648			return
4649
4650		self._schedule_one_shot(pattern, start_pulse)
4651
4652	def _check_transitions (self, boundary_pulse: int, section_changed: bool) -> None:
4653
4654		"""The form clock's boundary hook: fire fills, manage approach mutes.
4655
4656		Called once per bar (lookahead-early, with the bar-line pulse).
4657		Fill rules fire when the current bar is the section's last before a
4658		matching boundary; mute rules close over the approach window
4659		(rounded up to whole bars — muting is bar-granular) and reopen at
4660		the boundary.  Performer mutes are never touched.
4661		"""
4662
4663		if section_changed and self._transition_muted:
4664			# The boundary arrived — restore only what we muted ourselves.
4665			for name in self._transition_muted:
4666				running = self._running_patterns.get(name)
4667				if running is not None:
4668					running._muted = False
4669			self._transition_muted.clear()
4670
4671		if not self._transitions or self._form_state is None:
4672			return
4673
4674		info = self._form_state.get_section_info()
4675
4676		if info is None or info.next_section is None:
4677			return
4678
4679		bar_beats = float(self.time_signature[0])
4680		bars_remaining = info.bars - info.bar
4681
4682		for rule in self._transitions:
4683
4684			if rule.before == "*":
4685				if info.next_section == info.name:
4686					continue		# a repeat is not a boundary
4687			elif info.next_section != rule.before:
4688				continue
4689
4690			if rule.fill is not None and bars_remaining == 1:
4691				self._fire_fill(rule, boundary_pulse + int(round(rule.beat * self._sequencer.pulses_per_beat)))
4692
4693			if rule.mute:
4694				window_beats = rule.beats if rule.beats is not None else bar_beats
4695				window_bars = max(1, int((window_beats + bar_beats - 1e-9) // bar_beats))
4696
4697				if bars_remaining <= window_bars:
4698					for name in rule.mute:
4699						running = self._running_patterns.get(name)
4700						if running is None or name in self._transition_muted:
4701							continue
4702						if running._muted:
4703							continue		# the performer's mute — not ours to manage
4704						running._muted = True
4705						self._transition_muted.add(name)
4706
4707	@staticmethod
4708	def _resolve_length (
4709		beats: typing.Optional[float],
4710		bars: typing.Optional[float],
4711		steps: typing.Optional[float],
4712		step_duration: typing.Optional[float],
4713		default: float = 4.0,
4714		beats_per_bar: int = 4,
4715	) -> typing.Tuple[float, int]:
4716
4717		"""
4718		Resolve the beat_length and default_grid from the duration parameters.
4719
4720		Two modes:
4721
4722		- **Duration mode** (no ``step_duration``): specify ``beats=`` or ``bars=``.
4723		  ``beats=4`` = 4 quarter notes; ``bars=2`` = 8 beats.
4724		- **Step mode** (with ``step_duration``): specify ``steps=`` and ``step_duration=``.
4725		  ``steps=6, step_duration=dur.SIXTEENTH`` = 6 sixteenth notes = 1.5 beats.
4726
4727		Constraints:
4728
4729		- ``beats`` and ``bars`` are mutually exclusive.
4730		- ``steps`` requires ``step_duration``; ``step_duration`` requires ``steps``.
4731		- ``steps`` cannot be combined with ``beats`` or ``bars``.
4732
4733		Returns:
4734			(beat_length, default_grid) — beat_length in beats (quarter notes);
4735			default_grid the number of grid steps (16th-notes in beat mode, or the
4736			explicit ``steps`` value directly in step mode).
4737		"""
4738
4739		if beats is not None and bars is not None:
4740			raise ValueError("Specify only one of beats= or bars=")
4741
4742		if steps is not None and (beats is not None or bars is not None):
4743			raise ValueError("steps= cannot be combined with beats= or bars=")
4744
4745		if step_duration is not None and steps is None:
4746			raise ValueError("step_duration= requires steps= (e.g. steps=6, step_duration=dur.SIXTEENTH)")
4747
4748		if steps is not None:
4749			if step_duration is None:
4750				raise ValueError("steps= requires step_duration= (e.g. step_duration=dur.SIXTEENTH)")
4751			return steps * step_duration, int(steps)
4752
4753		if bars is not None:
4754			raw = bars * beats_per_bar
4755		elif beats is not None:
4756			raw = beats
4757		else:
4758			raw = default
4759
4760		return raw, round(raw / subsequence.constants.durations.SIXTEENTH)
4761
4762	def pattern (
4763		self,
4764		channel: int,
4765		beats: typing.Optional[float] = None,
4766		bars: typing.Optional[float] = None,
4767		steps: typing.Optional[float] = None,
4768		step_duration: typing.Optional[float] = None,
4769		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4770		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4771		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4772		reschedule_lookahead: float = 1,
4773		voice_leading: bool = False,
4774		device: subsequence.midi_utils.DeviceId = None,
4775		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4776		min_energy: typing.Optional[float] = None,
4777	) -> typing.Callable:
4778
4779		"""
4780		Register a function as a repeating MIDI pattern.
4781
4782		The decorated function will be called once per cycle to 'rebuild' its
4783		content. This allows for generative logic that evolves over time.
4784
4785		Two ways to specify pattern length:
4786
4787		- **Duration mode** (default): use ``beats=`` or ``bars=``.
4788		  The grid defaults to sixteenth-note resolution.
4789		- **Step mode**: use ``steps=`` paired with ``step_duration=``.
4790		  The grid equals the step count, so ``p.hit_steps()`` indices map
4791		  directly to steps.
4792
4793		Parameters:
4794			channel: MIDI channel. By default uses 1-based numbering (1-16).
4795				Set ``zero_indexed_channels=True`` on the ``Composition`` to use
4796				0-based numbering (0-15), matching the raw MIDI protocol, instead.
4797			beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar.
4798			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4). ``bars=2`` = 8 beats.
4799			steps: Step count for step mode. Requires ``step_duration=``.
4800			step_duration: Duration of one step in beats (e.g. ``dur.SIXTEENTH``).
4801				Requires ``steps=``.
4802			drum_note_map: Optional mapping for drum instruments.
4803			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4804				Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``.
4805			nrpn_name_map: Optional mapping of NRPN parameter names (strings) to
4806				14-bit parameter numbers (0–16383).  Enables string-based names
4807				in ``p.nrpn()`` and ``p.nrpn_ramp()`` — typically a
4808				device-specific dictionary (e.g. Sequential Take 5's
4809				``Osc1FreqFine`` → 9).
4810			reschedule_lookahead: Beats in advance to compute the next cycle.
4811			voice_leading: If True, chords in this pattern will automatically
4812				use inversions that minimize voice movement.
4813			mirrors: Optional list of additional ``(device, channel)`` destinations
4814				to duplicate every event from this pattern onto.  Notes, CCs, pitch
4815				bend, NRPN/RPN bursts, program changes, SysEx, and drone events are
4816				all mirrored; OSC events are not (OSC is not bound to a MIDI port).
4817				``device`` is the integer index returned by ``midi_output()`` (0 =
4818				primary).  ``channel`` follows this composition's channel-numbering
4819				convention.  See also ``mirror()`` / ``unmirror()`` for live toggling.
4820			min_energy: Automatic energy gating — the pattern is silent while
4821				the current section's energy (``composition.energy()`` dict,
4822				or the bound Section payload) is below this threshold.
4823				Composes with ``mute()``: a performer mute always wins.
4824
4825		Example:
4826			```python
4827			@comp.pattern(channel=1, beats=4)
4828			def chords (p):
4829				p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)
4830
4831			@comp.pattern(channel=1, bars=2)
4832			def long_phrase (p):
4833				...
4834
4835			@comp.pattern(channel=1, steps=6, step_duration=dur.SIXTEENTH)
4836			def riff (p):
4837				p.sequence(steps=[0, 1, 3, 5], pitches=60)
4838			```
4839		"""
4840
4841		channel = self._resolve_channel(channel)
4842
4843		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4844
4845		# Resolve device string name to index if possible now; otherwise store
4846		# the raw DeviceId and resolve it in _run() once all devices are open.
4847		resolved_device: subsequence.midi_utils.DeviceId = device
4848
4849		# Mirror-to-self check is only reliable when the primary device is a
4850		# concrete integer at decoration time.  ``None`` resolves to device 0
4851		# downstream, so we treat it as 0 here too.  Strings are deferred to
4852		# ``_run()`` and we skip the check for them.
4853		primary: typing.Optional[typing.Tuple[int, int]]
4854		if isinstance(resolved_device, str):
4855			primary = None
4856		else:
4857			primary = (resolved_device if resolved_device is not None else 0, channel)
4858		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4859
4860		def decorator (fn: typing.Callable) -> typing.Callable:
4861
4862			"""
4863			Wrap the builder function and register it as a pending pattern.
4864			During live sessions, hot-swap an existing pattern's builder instead.
4865			"""
4866
4867			# Record this declaration so the live-reload deletion diff knows the
4868			# pattern is still present in the source (see _apply_source_async).
4869			self._declared_names.add(fn.__name__)
4870
4871			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
4872			if self._is_live and fn.__name__ in self._running_patterns:
4873				running = self._running_patterns[fn.__name__]
4874				running._builder_fn = fn
4875				running._wants_chord = _fn_has_parameter(fn, "chord")
4876				logger.info(f"Hot-swapped pattern: {fn.__name__}")
4877				return fn
4878
4879			# Names key the seeded stream, mutes, tweaks, and reroll/lock — a
4880			# duplicate means two scheduled copies sharing one stream with
4881			# only one reachable by name.  Warn loudly at registration.
4882			if any(existing.builder_fn.__name__ == fn.__name__ for existing in self._pending_patterns):
4883				logger.warning(
4884					f"Duplicate pattern name '{fn.__name__}': both copies will be "
4885					f"scheduled, they share one seeded stream, and only one is "
4886					f"reachable by name — rename one of them."
4887				)
4888
4889			pending = _PendingPattern(
4890				builder_fn = fn,
4891				channel = channel,  # already resolved to 0-indexed
4892				length = beat_length,
4893				default_grid = default_grid,
4894				drum_note_map = drum_note_map,
4895				cc_name_map = cc_name_map,
4896				nrpn_name_map = nrpn_name_map,
4897				reschedule_lookahead = reschedule_lookahead,
4898				voice_leading = voice_leading,
4899				# For int/None: resolve immediately.  For str: store 0 as
4900				# placeholder; _resolve_pending_devices() fixes it in _run().
4901				device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device,
4902				raw_device = resolved_device,
4903				mirrors = resolved_mirrors,
4904				min_energy = min_energy,
4905			)
4906
4907			self._pending_patterns.append(pending)
4908
4909			return fn
4910
4911		return decorator
4912
4913	def layer (
4914		self,
4915		*builder_fns: typing.Callable,
4916		channel: int,
4917		beats: typing.Optional[float] = None,
4918		bars: typing.Optional[float] = None,
4919		steps: typing.Optional[float] = None,
4920		step_duration: typing.Optional[float] = None,
4921		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4922		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4923		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4924		reschedule_lookahead: float = 1,
4925		voice_leading: bool = False,
4926		device: subsequence.midi_utils.DeviceId = None,
4927		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4928	) -> None:
4929
4930		"""
4931		Combine multiple functions into a single MIDI pattern.
4932
4933		This is useful for composing complex patterns out of reusable
4934		building blocks (e.g., a 'kick' function and a 'snare' function).
4935
4936		See ``pattern()`` for the full description of ``beats``, ``bars``,
4937		``steps``, and ``step_duration``.
4938
4939		Parameters:
4940			builder_fns: One or more pattern builder functions.
4941			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
4942			beats: Duration in beats (quarter notes).
4943			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
4944			steps: Step count for step mode. Requires ``step_duration=``.
4945			step_duration: Duration of one step in beats. Requires ``steps=``.
4946			drum_note_map: Optional mapping for drum instruments.
4947			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4948			nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit
4949				parameter numbers.
4950			reschedule_lookahead: Beats in advance to compute the next cycle.
4951			voice_leading: If True, chords use smooth voice leading.
4952			mirrors: Optional list of additional ``(device, channel)`` destinations
4953				to duplicate every event onto.  See ``pattern()`` for details.
4954		"""
4955
4956		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4957
4958		# Resolve channel up-front so the mirror-to-self check has the canonical
4959		# primary form to compare against.
4960		resolved_channel = self._resolve_channel(channel)
4961
4962		# See pattern() for the same comment about None / str handling.
4963		primary: typing.Optional[typing.Tuple[int, int]]
4964		if isinstance(device, str):
4965			primary = None
4966		else:
4967			primary = (device if device is not None else 0, resolved_channel)
4968		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4969
4970		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
4971
4972		if wants_chord:
4973
4974			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
4975
4976				for fn in builder_fns:
4977					if _fn_has_parameter(fn, "chord"):
4978						fn(p, chord)
4979					else:
4980						fn(p)
4981
4982		else:
4983
4984			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
4985
4986				for fn in builder_fns:
4987					fn(p)
4988
4989		# Give the merged builder a stable, unique name derived from its
4990		# components so multiple layer() calls don't all register under
4991		# "merged_builder" and collide in _running_patterns (which made
4992		# mute/tweak/unregister/live_info reach only the LAST layer).  "+" can't
4993		# appear in a Python identifier, so this never clashes with a real
4994		# pattern function's name.
4995		base_name = ("+".join(fn.__name__ for fn in builder_fns) or "layer") + f"@ch{resolved_channel}"
4996		merged_name = base_name
4997		suffix = 2
4998
4999		# Two layers with the same components (e.g. on different saves of a
5000		# live file) must map to the same names pass-over-pass, while two
5001		# DIFFERENT layers sharing components in one pass must not collide.
5002		while merged_name in self._declared_names:
5003			merged_name = f"{base_name}#{suffix}"
5004			suffix += 1
5005
5006		merged_builder.__name__ = merged_name
5007
5008		# Record the declaration for the live-reload deletion diff, and hot-swap
5009		# in place when this layer is already running so a reload picks up edits
5010		# to the component functions without losing the pattern's cycle count,
5011		# tweaks, or mirrors (mirrors the pattern() decorator's hot-swap).
5012		self._declared_names.add(merged_builder.__name__)
5013
5014		if self._is_live and merged_builder.__name__ in self._running_patterns:
5015			running = self._running_patterns[merged_builder.__name__]
5016			running._builder_fn = merged_builder
5017			running._wants_chord = wants_chord
5018			logger.info(f"Hot-swapped layer: {merged_builder.__name__}")
5019			return
5020
5021		pending = _PendingPattern(
5022			builder_fn = merged_builder,
5023			channel = resolved_channel,  # already resolved to 0-indexed above
5024			length = beat_length,
5025			default_grid = default_grid,
5026			drum_note_map = drum_note_map,
5027			cc_name_map = cc_name_map,
5028			nrpn_name_map = nrpn_name_map,
5029			reschedule_lookahead = reschedule_lookahead,
5030			voice_leading = voice_leading,
5031			mirrors = resolved_mirrors,
5032			device = 0 if (device is None or isinstance(device, str)) else device,
5033			raw_device = device,
5034		)
5035
5036		self._pending_patterns.append(pending)
5037
5038	def chords (
5039		self,
5040		*,
5041		channel: int,
5042		progression: subsequence.progressions.ProgressionSource,
5043		harmonic_rhythm: subsequence.progressions.HarmonicRhythmSpec,
5044		bars: typing.Optional[float] = None,
5045		beats: typing.Optional[float] = None,
5046		voicing: subsequence.progressions.VoicingSpec = (3, 4),
5047		velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY,
5048		detached: typing.Optional[float] = None,
5049		root: int = 60,
5050		key: typing.Optional[str] = None,
5051		seed: typing.Optional[int] = None,
5052		device: subsequence.midi_utils.DeviceId = None,
5053		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5054	) -> subsequence.progressions.Progression:
5055
5056		"""Declare a self-contained chord part: a progression at a chosen harmonic rhythm.
5057
5058		The one-call form of ``p.progression()`` — it registers a pattern on
5059		*channel* that plays *progression* across *bars* (or *beats*), each chord
5060		lasting a length drawn from *harmonic_rhythm* (the musical term for how often
5061		the chords change).  It needs no ``composition.harmony()`` call and, with an
5062		explicit chord list or a ``key=``, no composition key either — so a
5063		drums-plus-one-chord-part sketch stays simple.
5064
5065		The progression is realised once, up front, and the same timeline plays every
5066		cycle (a stable phrase).  That timeline is returned so you can see exactly what
5067		was chosen — ``print(comp.chords(...))``.
5068
5069		Parameters:
5070			channel: MIDI channel for the chord part.
5071			progression: A chord-graph style name to generate from, or an explicit list
5072				of chords (``Chord`` objects or names like ``["Cm7", "Dbmaj7"]``).
5073			harmonic_rhythm: How long each chord lasts — a number, a list of lengths,
5074				or ``between(low, high, step=...)``.  See ``p.progression()``.
5075			bars / beats: Length of the part (defaults to 4 beats if neither is given).  ``bars`` uses the
5076				composition's time signature.
5077			voicing: Notes per chord — an int, or a ``(low, high)`` range (e.g. ``(3, 4)``).
5078			velocity: MIDI velocity, or a ``(low, high)`` tuple for per-voice humanisation.
5079			detached: Beats of silence before each next chord (``duration = length - detached``).
5080			root: MIDI root the voicings are centred on (e.g. 48 = C3).
5081			key: Key for a generated progression; defaults to the composition key.
5082			seed: Seed for the (otherwise fixed) realisation; defaults to the
5083				composition seed, so the part is reproducible.
5084			device: Optional output-device override.
5085			mirrors: Optional additional ``(device, channel)`` destinations.
5086
5087		Returns:
5088			The realised :class:`~subsequence.progressions.Progression`.
5089		"""
5090
5091		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5092		resolved_channel = self._resolve_channel(channel)
5093		resolved_key = key if key is not None else self.key
5094
5095		rng = random.Random(seed if seed is not None else self._seed)
5096		timeline = subsequence.progressions.realize(
5097			source = progression,
5098			harmonic_rhythm = harmonic_rhythm,
5099			key = resolved_key,
5100			length = beat_length,
5101			rng = rng,
5102			scale = self.scale or "ionian",
5103		)
5104
5105		captured_root = root
5106		captured_velocity = velocity
5107		captured_detached = detached
5108		captured_voicing = voicing
5109
5110		def chords_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5111
5112			"""Replay the realised timeline as block chords each cycle (voicing per chord)."""
5113
5114			for chord, start, length in timeline:
5115				ring = length - captured_detached if (captured_detached and captured_detached < length) else length
5116				voices = subsequence.progressions.resolve_voices(captured_voicing, p.rng)
5117				p.chord(chord, root=captured_root, beat=start, duration=ring, count=voices, velocity=captured_velocity)
5118
5119		# Unique, stable name so multiple chord parts don't collide in
5120		# _running_patterns — including two parts on the SAME channel, which
5121		# get a deterministic #2/#3 suffix in declaration order.
5122		base_name = f"chords@ch{resolved_channel}"
5123		chords_name = base_name
5124		suffix = 2
5125
5126		while chords_name in self._declared_names:
5127			chords_name = f"{base_name}#{suffix}"
5128			suffix += 1
5129
5130		chords_builder.__name__ = chords_name
5131		self._declared_names.add(chords_name)
5132
5133		primary: typing.Optional[typing.Tuple[int, int]]
5134		if isinstance(device, str):
5135			primary = None
5136		else:
5137			primary = (device if device is not None else 0, resolved_channel)
5138		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5139
5140		if self._is_live and chords_builder.__name__ in self._running_patterns:
5141			running = self._running_patterns[chords_builder.__name__]
5142			running._builder_fn = chords_builder
5143			running._wants_chord = False
5144			logger.info(f"Hot-swapped chords: {chords_builder.__name__}")
5145			return timeline
5146
5147		pending = _PendingPattern(
5148			builder_fn = chords_builder,
5149			channel = resolved_channel,
5150			length = beat_length,
5151			default_grid = default_grid,
5152			drum_note_map = None,
5153			reschedule_lookahead = 1,
5154			voice_leading = False,
5155			mirrors = resolved_mirrors,
5156			device = 0 if (device is None or isinstance(device, str)) else device,
5157			raw_device = device,
5158		)
5159		self._pending_patterns.append(pending)
5160		return timeline
5161
5162	def phrase_part (
5163		self,
5164		*,
5165		channel: int,
5166		part: typing.Optional[str] = None,
5167		root: int = 60,
5168		bars: typing.Optional[float] = None,
5169		beats: typing.Optional[float] = None,
5170		velocity: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
5171		fit: typing.Optional[float] = None,
5172		device: subsequence.midi_utils.DeviceId = None,
5173		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5174	) -> None:
5175
5176		"""Declare a part that plays each section's bound Motif/Phrase.
5177
5178		The one-call consumer for :meth:`section_motifs` — it registers a
5179		pattern on *channel* that walks whatever value is bound to the
5180		current section for *part* (stateless position from the cycle
5181		counter, via ``p.phrase()``).  A section with no binding for the
5182		part is **silent** for that part — bind material or don't; no
5183		fallback guessing.
5184
5185		Parameters:
5186			channel: MIDI channel for the part.
5187			part: The part label to read from the registry (``None`` = the
5188				unlabelled binding).
5189			root: Register anchor for degree resolution.
5190			bars / beats: Cycle length of the part (defaults to 4 beats);
5191				the phrase is sliced one cycle window at a time.
5192			velocity: Optional override applied to every note.
5193			fit: Passed through (active with the melody engine stage).
5194			device: Optional output-device override.
5195			mirrors: Optional additional ``(device, channel)`` destinations.
5196
5197		Example::
5198
5199			composition.section_motifs("verse",  verse_line,  part="lead")
5200			composition.section_motifs("chorus", chorus_line, part="lead")
5201			composition.phrase_part(channel=4, part="lead", root=72, bars=2)
5202		"""
5203
5204		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5205		resolved_channel = self._resolve_channel(channel)
5206
5207		captured_part = part
5208		captured_root = root
5209		captured_velocity = velocity
5210		captured_fit = fit
5211
5212		def phrase_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5213
5214			"""Walk the current section's bound value (silent when unbound)."""
5215
5216			value = p.section_motif(captured_part)
5217
5218			if value is None:
5219				return	# unbound section: silence for this part, by design
5220
5221			p.phrase(value, root=captured_root, velocity=captured_velocity, fit=captured_fit)
5222
5223		# Unique, stable name so multiple phrase parts don't collide —
5224		# including two parts on the SAME channel (deterministic #2/#3
5225		# suffixes in declaration order, the chords() convention).
5226		base_name = f"phrase@{captured_part}@ch{resolved_channel}" if captured_part else f"phrase@ch{resolved_channel}"
5227		phrase_name = base_name
5228		suffix = 2
5229
5230		while phrase_name in self._declared_names:
5231			phrase_name = f"{base_name}#{suffix}"
5232			suffix += 1
5233
5234		phrase_builder.__name__ = phrase_name
5235		self._declared_names.add(phrase_name)
5236
5237		primary: typing.Optional[typing.Tuple[int, int]]
5238		if isinstance(device, str):
5239			primary = None
5240		else:
5241			primary = (device if device is not None else 0, resolved_channel)
5242		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5243
5244		if self._is_live and phrase_builder.__name__ in self._running_patterns:
5245			running = self._running_patterns[phrase_builder.__name__]
5246			running._builder_fn = phrase_builder
5247			running._wants_chord = False
5248			logger.info(f"Hot-swapped phrase part: {phrase_builder.__name__}")
5249			return
5250
5251		pending = _PendingPattern(
5252			builder_fn = phrase_builder,
5253			channel = resolved_channel,
5254			length = beat_length,
5255			default_grid = default_grid,
5256			drum_note_map = None,
5257			reschedule_lookahead = 1,
5258			voice_leading = False,
5259			mirrors = resolved_mirrors,
5260			device = 0 if (device is None or isinstance(device, str)) else device,
5261			raw_device = device,
5262		)
5263		self._pending_patterns.append(pending)
5264
5265	def trigger (
5266		self,
5267		fn: typing.Callable,
5268		channel: int,
5269		beats: typing.Optional[float] = None,
5270		bars: typing.Optional[float] = None,
5271		steps: typing.Optional[float] = None,
5272		step_duration: typing.Optional[float] = None,
5273		quantize: float = 0,
5274		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
5275		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
5276		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
5277		chord: bool = False,
5278		device: subsequence.midi_utils.DeviceId = None,
5279		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5280	) -> None:
5281
5282		"""
5283		Trigger a one-shot pattern immediately or on a quantized boundary.
5284
5285		This is useful for real-time response to sensors, OSC messages, or other
5286		external events. The builder function is called immediately with a fresh
5287		PatternBuilder, and the generated events are injected into the queue at
5288		the specified quantize boundary.
5289
5290		The builder function has the same API as a ``@composition.pattern``
5291		decorated function and can use all PatternBuilder methods: ``p.note()``,
5292		``p.euclidean()``, ``p.arpeggio()``, and so on.
5293
5294		See ``pattern()`` for the full description of ``beats``, ``bars``,
5295		``steps``, and ``step_duration``. Default is 1 beat.
5296
5297		Parameters:
5298			fn: The pattern builder function (same signature as ``@comp.pattern``).
5299			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
5300			beats: Duration in beats (quarter notes, default 1).
5301			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
5302			steps: Step count for step mode. Requires ``step_duration=``.
5303			step_duration: Duration of one step in beats. Requires ``steps=``.
5304			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
5305				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
5306				constants from ``subsequence.constants.durations``.
5307			drum_note_map: Optional drum name mapping for this pattern.
5308			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
5309			nrpn_name_map: Optional mapping of NRPN parameter names to
5310				14-bit parameter numbers.
5311			chord: If ``True``, the builder function receives the current chord as
5312				a second parameter (same as ``@composition.pattern``).
5313			mirrors: Optional list of additional ``(device, channel)`` destinations
5314				to fire this one-shot onto in parallel with the primary destination.
5315
5316		Example:
5317			```python
5318			# Immediate single note (channels are 1-16 by default)
5319			composition.trigger(
5320				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
5321				channel=1
5322			)
5323
5324			# Quantized fill (next bar) — channel 10 is the GM drum channel
5325			import subsequence.constants.durations as dur
5326			composition.trigger(
5327				lambda p: p.euclidean("snare", pulses=7, velocity=90),
5328				channel=10,
5329				drum_note_map=gm_drums.GM_DRUM_MAP,
5330				quantize=dur.WHOLE
5331			)
5332
5333			# With chord context — the builder receives the chord as a second
5334			# argument when chord=True.
5335			composition.trigger(
5336				lambda p, chord: p.arpeggio(chord.tones(root=60), spacing=dur.SIXTEENTH),
5337				channel=1,
5338				quantize=dur.QUARTER,
5339				chord=True
5340			)
5341			```
5342		"""
5343
5344		# Resolve channel numbering
5345		resolved_channel = self._resolve_channel(channel)
5346
5347		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, default=1.0, beats_per_bar=self.time_signature[0])
5348
5349		# Resolve device index — for trigger() this is always concrete by call time,
5350		# so the mirror-to-self check has the full primary tuple available.
5351		resolved_device_idx = self._resolve_device_id(device)
5352		resolved_mirrors = self._resolve_mirrors(mirrors, primary=(resolved_device_idx, resolved_channel))
5353
5354		# Create a temporary Pattern
5355		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx, mirrors=resolved_mirrors)
5356
5357		# Resolve the section context once: the one-shot inherits the section's
5358		# effective key/scale (so a triggered degree resolves like everywhere
5359		# else) and a harmony view at the current playhead (so ChordTone /
5360		# Approach resolve too).
5361		trigger_section = self._form_state.get_section_info() if self._form_state else None
5362		trigger_key, trigger_scale = self._effective_key_scale(trigger_section)
5363
5364		trigger_harmony: typing.Optional[HarmonyView] = None
5365		if not self._harmony_horizon.is_empty:
5366			trigger_harmony = HarmonyView(self._harmony_horizon, self._sequencer.pulse_count / self._sequencer.pulses_per_beat)
5367
5368		# Create a PatternBuilder
5369		builder = subsequence.pattern_builder.PatternBuilder(
5370			pattern=pattern,
5371			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
5372			drum_note_map=drum_note_map,
5373			cc_name_map=cc_name_map,
5374			nrpn_name_map=nrpn_name_map,
5375			section=trigger_section,
5376			bar=self._builder_bar,
5377			conductor=self.conductor,
5378			rng=random.Random(),  # Fresh random state for each trigger
5379			tweaks={},
5380			default_grid=default_grid,
5381			data=self.data,
5382			# A one-shot resolves key-relative content against the same
5383			# effective key/scale as the section it fires into (previously
5384			# omitted entirely — degrees raised even in a keyed composition).
5385			key=trigger_key,
5386			scale=trigger_scale,
5387			time_signature=self.time_signature,
5388			held_notes=self._sequencer._held_notes,
5389			harmony=trigger_harmony,
5390			energy=self._current_energy(trigger_section)
5391		)
5392
5393		# Call the builder function
5394		try:
5395
5396			current_chord = self.current_chord() if chord else None
5397
5398			if current_chord is not None:
5399				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
5400				fn(builder, injected)
5401
5402			else:
5403				fn(builder)
5404
5405		except Exception:
5406			logger.exception("Error in trigger builder — pattern will be silent")
5407			return
5408
5409		# Calculate the start pulse based on quantize
5410		current_pulse = self._sequencer.pulse_count
5411		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
5412
5413		if quantize == 0:
5414			# Immediate: use current pulse
5415			start_pulse = current_pulse
5416
5417		else:
5418			# Quantize to the next multiple of (quantize * pulses_per_beat)
5419			quantize_pulses = int(quantize * pulses_per_beat)
5420			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
5421
5422		self._schedule_one_shot(pattern, start_pulse)
5423
5424	def _schedule_one_shot (self, pattern: subsequence.pattern.Pattern, start_pulse: int) -> None:
5425
5426		"""Schedule a one-shot pattern at an absolute pulse, thread-safely."""
5427
5428		try:
5429			# Probe only: raises RuntimeError when not on the event loop.
5430			asyncio.get_running_loop()
5431			asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse))
5432
5433		except RuntimeError:
5434			# Not on the event loop — hand the coroutine to the loop thread.
5435			if self._sequencer._event_loop is not None:
5436				asyncio.run_coroutine_threadsafe(
5437					self._sequencer.schedule_pattern(pattern, start_pulse),
5438					loop=self._sequencer._event_loop
5439				)
5440			else:
5441				logger.warning("trigger() called before playback started; pattern ignored")
5442
5443	@property
5444	def is_clock_following (self) -> bool:
5445
5446		"""True if either the primary or any additional device is following external clock."""
5447
5448		return self._clock_follow or any(cf for _, _, cf in self._additional_inputs)
5449
5450
5451	def play (self) -> None:
5452
5453		"""
5454		Start the composition.
5455
5456		This call blocks until the program is interrupted (e.g., via Ctrl+C).
5457		It initializes the MIDI hardware, launches the background sequencer,
5458		and begins playback.
5459		"""
5460
5461		try:
5462			asyncio.run(self._run())
5463
5464		except KeyboardInterrupt:
5465			pass
5466
5467
5468	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
5469
5470		"""Render the composition to a MIDI file without real-time playback.
5471
5472		Runs the sequencer as fast as possible (no timing delays) and stops
5473		when the first active limit is reached.  The result is saved as a
5474		standard MIDI file that can be imported into any DAW.
5475
5476		All patterns, scheduled callbacks, and harmony logic run exactly as
5477		they would during live playback — BPM transitions, generative fills,
5478		and probabilistic gates all work in render mode.  The only difference
5479		is that time is simulated rather than wall-clock driven.
5480
5481		Parameters:
5482			bars: Number of bars to render, or ``None`` for no bar limit
5483			      (default ``None``).  When both *bars* and *max_minutes* are
5484			      active, playback stops at whichever limit is reached first.
5485			filename: Output MIDI filename (default ``"render.mid"``).
5486			max_minutes: Safety cap on the length of rendered MIDI in minutes
5487			             (default ``60.0``).  Pass ``None`` to disable the time
5488			             cap — you must then provide an explicit *bars* value.
5489
5490		Raises:
5491			ValueError: If both *bars* and *max_minutes* are ``None``, which
5492			            would produce an infinite render.
5493
5494		Examples:
5495			```python
5496			# Default: renders up to 60 minutes of MIDI content.
5497			composition.render()
5498
5499			# Render exactly 64 bars (time cap still active as backstop).
5500			composition.render(bars=64, filename="demo.mid")
5501
5502			# Render up to 5 minutes of an infinite generative composition.
5503			composition.render(max_minutes=5, filename="five_min.mid")
5504
5505			# Remove the time cap — must supply bars instead.
5506			composition.render(bars=128, max_minutes=None, filename="long.mid")
5507			```
5508		"""
5509
5510		if bars is None and max_minutes is None:
5511			raise ValueError(
5512				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
5513				"Passing both as None would produce an infinite render."
5514			)
5515
5516		self._sequencer.recording = True
5517		self._sequencer.record_filename = filename
5518		self._sequencer.render_mode = True
5519		self._sequencer.render_bars = bars if bars is not None else 0
5520		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
5521		asyncio.run(self._run())
5522
5523	def _broadcast_osc_status (self, bar: int) -> None:
5524
5525		"""
5526		Send the per-bar OSC status snapshot: bar number, current tempo,
5527		and (when active) the current chord name and form section.
5528		"""
5529
5530		if self._osc_server:
5531			self._osc_server.send("/bar", bar)
5532			self._osc_server.send("/bpm", self._sequencer.current_bpm)
5533
5534			sounding = self.current_chord()
5535			if sounding is not None:
5536				self._osc_server.send("/chord", sounding.name())
5537
5538			if self._form_state:
5539				info = self._form_state.get_section_info()
5540				if info:
5541					self._osc_server.send("/section", info.name)
5542
5543	async def _run (self) -> None:
5544
5545		"""
5546		Async entry point that schedules all patterns and runs the sequencer.
5547		"""
5548
5549		# 1. Pre-calculate MIDI input indices and configure sequencer clock follow.
5550		if self._input_device is not None:
5551			self._sequencer.input_device_name = self._input_device
5552			self._sequencer.clock_follow = self._clock_follow
5553			self._sequencer.clock_device_idx = 0
5554
5555			if not self._clock_follow:
5556				# Find first additional input that wants to be the clock master.
5557				for idx, (_, _, cf) in enumerate(self._additional_inputs, start=1):
5558					if cf:
5559						self._sequencer.clock_follow = True
5560						self._sequencer.clock_device_idx = idx
5561						break
5562
5563		# Populate input device name mapping early (before opening ports) so we can
5564		# resolve CC mappings to integer device indices immediately.
5565		if self._sequencer.input_device_name:
5566			self._input_device_names[self._sequencer.input_device_name] = 0
5567			if self._input_device_alias is not None:
5568				self._input_device_names[self._input_device_alias] = 0
5569
5570		for idx, (dev_name, alias, _) in enumerate(self._additional_inputs, start=1):
5571			self._input_device_names[dev_name] = idx
5572			if alias:
5573				self._input_device_names[alias] = idx
5574
5575		# 2. Pre-calculate output device names.
5576		if self._sequencer.output_device_name:
5577			self._output_device_names[self._sequencer.output_device_name] = 0
5578			# Primary device (index 0) is open by now (_init_midi_output ran in
5579			# the Sequencer constructor), so its latency can be set safely here.
5580			if self._output_latency_ms:
5581				self._sequencer.set_device_latency(0, self._output_latency_ms)
5582
5583		# 3. Resolve name-based INPUT device ids in cc_map/cc_forward early — the
5584		# input-names map is fully populated above, and the callback thread needs
5585		# integer indices as soon as ports open.  OUTPUT names (cc_forward
5586		# output_device=, pattern device=) resolve after the additional outputs
5587		# are opened below; resolving them here matched against a map containing
5588		# only the primary and silently routed everything to device 0.
5589		for mapping in self._cc_mappings:
5590			raw = mapping.get('input_device')
5591			if isinstance(raw, str):
5592				mapping['input_device'] = self._resolve_input_device_id(raw)
5593		for fwd in self._cc_forwards:
5594			raw_in = fwd.get('input_device')
5595			if isinstance(raw_in, str):
5596				fwd['input_device'] = self._resolve_input_device_id(raw_in)
5597
5598		# 4. Share CC input mappings, forwards, and a reference to composition.data
5599		# with the sequencer BEFORE opening the ports. This ensures that any initial
5600		# messages in the OS buffer are correctly mapped as soon as the port opens.
5601		self._sequencer.cc_mappings = self._cc_mappings
5602		self._sequencer.cc_forwards = self._cc_forwards
5603		self._sequencer._composition_data = self.data
5604
5605		# Held-note input: create the tracker and resolve its channel/device
5606		# filter so the callback thread can buffer matching note events.
5607		if self._note_input is not None:
5608			if self._input_device is None and not self._additional_inputs:
5609				raise RuntimeError("note_input() requires a MIDI input — call composition.midi_input(device) first")
5610			raw_dev = self._note_input.get('input_device')
5611			if isinstance(raw_dev, str):
5612				raw_dev = self._resolve_input_device_id(raw_dev)
5613			self._sequencer._note_input_channel = self._note_input['channel']
5614			self._sequencer._note_input_device = raw_dev
5615			self._sequencer._held_notes = subsequence.held_notes.HeldNotes(
5616				release_ms = self._note_input['release_ms'],
5617				latch = self._note_input['latch'],
5618			)
5619
5620		# 5. Open MIDI input ports early. Even without a deliberate sleep, opening
5621		# them before pattern building minimizes the window for missed messages.
5622		# Primary input
5623		self._sequencer._open_midi_inputs()
5624
5625		# Additional inputs
5626		for idx, (dev_name, alias, cf) in enumerate(self._additional_inputs, start=1):
5627			# Use the pre-calculated index
5628			callback = self._sequencer._make_input_callback(idx)
5629			open_name, port = subsequence.midi_utils.select_input_device(dev_name, callback)
5630			if open_name and port is not None:
5631				self._sequencer.add_input_device(open_name, port)
5632			else:
5633				logger.warning(f"Could not open additional input device '{dev_name}'")
5634
5635		# 6. Open additional MIDI output devices.
5636		for out in self._additional_outputs:
5637			open_name, port = subsequence.midi_utils.select_output_device(out.device)
5638			if open_name and port is not None:
5639				idx = self._sequencer.add_output_device(open_name, port, out.latency_ms)
5640				self._output_device_names[open_name] = idx
5641				if out.alias is not None:
5642					self._output_device_names[out.alias] = idx
5643			else:
5644				logger.warning(f"Could not open additional output device '{out.device}'")
5645
5646		# Warn if latency compensation adds noticeable whole-rig delay: the
5647		# slowest device defines the alignment point, so every faster device is
5648		# delayed up to that amount and live-input feel suffers.
5649		self._warn_if_high_latency()
5650
5651		# Resolve any name-based output device IDs on patterns that may have been added
5652		# for additional output devices.
5653		self._resolve_pending_devices()
5654
5655		# Resolve cc_forward output-device names now that every output port and
5656		# alias is registered (resolving earlier silently routed to device 0).
5657		for fwd in self._cc_forwards:
5658			raw_out = fwd.get('output_device')
5659			if isinstance(raw_out, str):
5660				fwd['output_device'] = self._resolve_device_id(raw_out)
5661
5662		# Pass clock output flag (suppressed automatically when clock_follow=True).
5663		self._sequencer.clock_output = self._clock_output and not self.is_clock_following
5664
5665		# Create Ableton Link clock if comp.link() was called.
5666		if self._link_quantum is not None:
5667			self._sequencer._link_clock = subsequence.link_clock.LinkClock(
5668				bpm = self.bpm,
5669				quantum = self._link_quantum,
5670				loop = asyncio.get_running_loop(),
5671			)
5672
5673		# Deal play-time streams.  Every stream is NAME-keyed (crc32 of
5674		# "seed:name", see _stream_seed) rather than dealt from one master in
5675		# registration order: adding or removing one consumer can never shift
5676		# another's stream, and patterns added live derive identically in
5677		# _build_pattern_from_pending.  When no seed is set, components keep
5678		# their own unseeded RNGs (existing behaviour).
5679		if self._seed is not None:
5680
5681			harmony_stream = self._stream("play:harmony")
5682			if self._harmonic_state is not None and harmony_stream is not None:
5683				self._harmonic_state.rng = harmony_stream
5684
5685			form_stream = self._stream("play:form")
5686			if self._form_state is not None and form_stream is not None:
5687				self._form_state._rng = form_stream
5688
5689		# The clocks fire BEFORE pattern rebuilds at the same pulse, and their
5690		# lookahead is RAISED to the maximum pattern lookahead (never patterns
5691		# clamped down): when a pattern rebuilds for its next cycle, the form
5692		# state and the harmony window already describe that cycle.
5693		bar_beats = float(self.time_signature[0])
5694
5695		pattern_lookaheads = [pending.reschedule_lookahead for pending in self._pending_patterns]
5696		pattern_lookaheads += [pattern.reschedule_lookahead for pattern in self._running_patterns.values()]
5697		max_pattern_lookahead = max(pattern_lookaheads, default = 1)
5698
5699		clock_lookahead = max(1.0, float(self._harmony_reschedule_lookahead), float(max_pattern_lookahead))
5700
5701		if clock_lookahead > bar_beats:
5702			logger.warning(
5703				"A pattern's reschedule_lookahead (%.2g beats) exceeds the bar length (%.2g) — "
5704				"the harmony/form clocks fire at most one bar ahead, so that pattern may "
5705				"rebuild before the window covers its cycle start.",
5706				clock_lookahead, bar_beats,
5707			)
5708			clock_lookahead = bar_beats
5709
5710		# Minimum span >= maximum lookahead: the clock cannot prepare a chord
5711		# boundary that arrives sooner than it fires.  Harmonic motion faster
5712		# than this floor stays available at the part level (p.progression),
5713		# where placement is not clock-bound.
5714		def _check_span_floor (progression: typing.Optional[Progression], label: str) -> None:
5715			if progression is None:
5716				return
5717			shortest = min(span.beats for span in progression.spans)
5718			if shortest < clock_lookahead - 1e-9:
5719				raise ValueError(
5720					f"{label}: shortest chord span ({shortest:g} beats) is below the clock "
5721					f"lookahead ({clock_lookahead:g} beats — the largest pattern lookahead). "
5722					"Lengthen the span, lower the pattern lookaheads, or place fast harmony "
5723					"at the part level with p.progression()."
5724				)
5725
5726		_check_span_floor(self._bound_progression, "harmony(progression=)")
5727		for section_name, section_progression in self._section_progressions.items():
5728			_check_span_floor(section_progression, f"section_chords({section_name!r})")
5729
5730		# Key-relative section progressions resolve late, per occurrence — so
5731		# verify they WILL resolve now, before playback, rather than surfacing
5732		# a silent skip (or a dead clock) mid-render.  For each occurrence's
5733		# effective key+scale: a missing key, or a degree/scale that does not
5734		# resolve, is raised here with an actionable message.
5735		fs = self._form_state
5736
5737		for section_name, section_progression in self._section_progressions.items():
5738			if section_progression.is_concrete:
5739				continue
5740
5741			# The (key, scale) contexts this section may be resolved against.
5742			contexts: typing.List[typing.Tuple[typing.Optional[str], typing.Optional[str]]] = []
5743			if fs is not None and fs._sequence is not None and any(s.name == section_name for s in fs._sequence):
5744				for section in fs._sequence:
5745					if section.name != section_name:
5746						continue
5747					ctx = (section.key or self._form_key or self.key, section.scale or self._form_scale or self.scale)
5748					if ctx not in contexts:
5749						contexts.append(ctx)
5750			else:
5751				contexts.append((self._form_key or self.key, self._form_scale or self.scale))
5752
5753			for ctx_key, ctx_scale in contexts:
5754				if ctx_key is None:
5755					raise ValueError(
5756						f"section_chords({section_name!r}) is key-relative (degrees/romans) but no key "
5757						"resolves for it — set key= on the Composition, a form key (form(key=...)), or "
5758						f"a Section.key on every {section_name!r} section."
5759					)
5760				try:
5761					section_progression.resolve(ctx_key, ctx_scale or "ionian")
5762				except ValueError as error:
5763					raise ValueError(
5764						f"section_chords({section_name!r}) does not resolve against its effective key "
5765						f"{ctx_key} {ctx_scale or 'ionian'}: {error}"
5766					)
5767
5768		# min_energy with nothing feeding p.energy is a silent no-op — warn loudly.
5769		energy_gated = [p.builder_fn.__name__ for p in self._pending_patterns if p.min_energy is not None]
5770
5771		if energy_gated and not self._energy_map and not self._form_has_payload:
5772			logger.warning(
5773				f"min_energy is set on {', '.join(energy_gated)} but no energy source is "
5774				"configured — p.energy is always 0.5 (call composition.energy() or bind a "
5775				"Form whose Sections carry energy)"
5776			)
5777
5778		# The form clock MUST be registered before the harmonic clock: same-pulse
5779		# fixed callbacks fire in registration order (and all fixed callbacks fire
5780		# before callback sequences), and on a section-boundary bar the harmonic
5781		# clock reads the current section (via _get_section_progression) to decide
5782		# whether to walk that section's chords.  Registering harmony first would
5783		# make it read the OLD section on every boundary, shifting section_chords()
5784		# replays by one bar and bleeding them across sections.
5785		if self._form_state is not None:
5786
5787			await schedule_form(
5788				sequencer = self._sequencer,
5789				form_state = self._form_state,
5790				reschedule_lookahead = clock_lookahead,
5791				on_bar = self._check_transitions,
5792				# Re-read every bar so a mid-playback form() re-bind advances
5793				# the NEW state instead of the abandoned object.
5794				get_form_state = lambda: self._form_state,
5795			)
5796
5797		self._harmony_horizon.reset()
5798		self._harmonic_clock_started = False
5799
5800		if self._harmonic_state is not None or self._bound_progression is not None or self._section_progressions:
5801			await self._start_harmonic_clock(bar_beats, clock_lookahead)
5802
5803		# Bar counter - always active so p.bar is available to all builders.
5804		def _advance_builder_bar (pulse: int) -> None:
5805			self._builder_bar += 1
5806
5807		first_bar_pulse = int(self.time_signature[0] * self._sequencer.pulses_per_beat)
5808
5809		await self._sequencer.schedule_callback_repeating(
5810			callback = _advance_builder_bar,
5811			interval_beats = self.time_signature[0],
5812			start_pulse = first_bar_pulse,
5813			# Same raised lookahead as the form/harmony clocks: a pattern
5814			# rebuilding lookahead-early for its next cycle must read the bar
5815			# that cycle starts in, not the previous one.
5816			reschedule_lookahead = clock_lookahead
5817		)
5818
5819		# Run wait_for_initial=True scheduled functions and block until all complete.
5820		# This ensures composition.data is populated before patterns build.
5821		initial_tasks = [t for t in self._pending_scheduled if t.wait_for_initial]
5822
5823		if initial_tasks:
5824
5825			names = ", ".join(getattr(t.fn, '__name__', repr(t.fn)) for t in initial_tasks)
5826			logger.info(f"Waiting for initial scheduled {'function' if len(initial_tasks) == 1 else 'functions'} before start: {names}")
5827
5828			async def _run_initial (fn: typing.Callable) -> None:
5829
5830				accepts_ctx = _fn_has_parameter(fn, "p")
5831				ctx = ScheduleContext(cycle=0)
5832
5833				try:
5834					if inspect.iscoroutinefunction(fn):
5835						await (fn(ctx) if accepts_ctx else fn())
5836					else:
5837						loop = asyncio.get_running_loop()
5838						call = (lambda: fn(ctx)) if accepts_ctx else fn
5839						await loop.run_in_executor(None, call)
5840				except Exception as exc:
5841					logger.warning(f"Initial run of {getattr(fn, '__name__', repr(fn))!r} failed: {exc}")
5842
5843			await asyncio.gather(*[_run_initial(t.fn) for t in initial_tasks])
5844
5845		for pending_task in self._pending_scheduled:
5846
5847			accepts_ctx = _fn_has_parameter(pending_task.fn, "p")
5848
5849			# A wait_for_initial task already ran once as cycle 0 (the blocking
5850			# pre-roll above), so its repeating wrapper starts at cycle 1 — keeping
5851			# ScheduleContext.cycle monotonic across the initial and repeating runs.
5852			wrapped = _make_safe_callback(
5853				pending_task.fn,
5854				accepts_context = accepts_ctx,
5855				start_cycle = 1 if pending_task.wait_for_initial else 0,
5856			)
5857
5858			# wait_for_initial=True implies defer — no point firing at pulse 0
5859			# after the blocking run just completed.  defer=True skips the
5860			# backshift fire so the first repeating call happens one full cycle
5861			# later.
5862			if pending_task.wait_for_initial or pending_task.defer:
5863				start_pulse = int(pending_task.cycle_beats * self._sequencer.pulses_per_beat)
5864			else:
5865				start_pulse = 0
5866
5867			await self._sequencer.schedule_callback_repeating(
5868				callback = wrapped,
5869				interval_beats = pending_task.cycle_beats,
5870				start_pulse = start_pulse,
5871				reschedule_lookahead = pending_task.reschedule_lookahead
5872			)
5873
5874		# Build Pattern objects from pending registrations.
5875		patterns: typing.List[subsequence.pattern.Pattern] = []
5876
5877		for i, pending in enumerate(self._pending_patterns):
5878
5879			pattern = self._build_pattern_from_pending(pending)
5880			patterns.append(pattern)
5881
5882		await schedule_patterns(
5883			sequencer = self._sequencer,
5884			patterns = patterns,
5885			start_pulse = 0
5886		)
5887
5888		# Populate the running patterns dict for live hot-swap and mute/unmute.
5889		for i, pending in enumerate(self._pending_patterns):
5890			name = pending.builder_fn.__name__
5891			self._running_patterns[name] = patterns[i]
5892
5893		# Everything pending is running now; drop the declarations so a later
5894		# live reload cannot graduate stale copies.
5895		self._pending_patterns = []
5896
5897		if self._display is not None and not self._sequencer.render_mode:
5898			self._display.start()
5899			self._sequencer.on_event("bar",  self._display.update)
5900			self._sequencer.on_event("beat", self._display.update)
5901
5902		if self._live_server is not None:
5903			await self._live_server.start()
5904
5905		if self._osc_server is not None:
5906			await self._osc_server.start()
5907			self._sequencer.osc_server = self._osc_server
5908			self._sequencer.on_event("bar", self._broadcast_osc_status)
5909
5910		# Start keystroke listener if hotkeys are enabled and not in render mode.
5911		if self._hotkeys_enabled and not self._sequencer.render_mode:
5912			self._keystroke_listener = subsequence.keystroke.KeystrokeListener()
5913			self._keystroke_listener.start()
5914
5915			if self._keystroke_listener.active:
5916				# Listener started successfully — register the bar handler
5917				# and show all bindings so the user knows what's available.
5918				self._sequencer.on_event("bar", self._process_hotkeys)
5919				self._list_hotkeys()
5920			# If not active, KeystrokeListener.start() already logged a warning.
5921
5922		if self._web_ui_enabled and not self._sequencer.render_mode:
5923			self._web_ui_server = subsequence.web_ui.WebUI(self, http_host=self._web_ui_http_host, ws_host=self._web_ui_ws_host)
5924			self._web_ui_server.start()
5925
5926		try:
5927			await run_until_stopped(self._sequencer)
5928		finally:
5929			# Tear down every service even if run_until_stopped (or an earlier
5930			# stop) raised, and guard each individually, so one failure can't
5931			# strand the rest — most importantly the keystroke listener's
5932			# terminal restore.
5933			if self._web_ui_server is not None:
5934				try:
5935					self._web_ui_server.stop()
5936				except Exception:
5937					logger.exception("Error stopping web UI")
5938
5939			if self._live_server is not None:
5940				try:
5941					await self._live_server.stop()
5942				except Exception:
5943					logger.exception("Error stopping live server")
5944
5945			if self._live_reloader is not None:
5946				try:
5947					self._live_reloader.stop()
5948				except Exception:
5949					logger.exception("Error stopping live reloader")
5950
5951			if self._osc_server is not None:
5952				try:
5953					await self._osc_server.stop()
5954				except Exception:
5955					logger.exception("Error stopping OSC server")
5956				self._sequencer.osc_server = None
5957
5958			if self._display is not None:
5959				try:
5960					self._display.stop()
5961				except Exception:
5962					logger.exception("Error stopping display")
5963
5964			if self._keystroke_listener is not None:
5965				try:
5966					self._keystroke_listener.stop()
5967				except Exception:
5968					logger.exception("Error stopping keystroke listener")
5969				self._keystroke_listener = None
5970
5971	def _build_pattern_from_pending (self, pending: _PendingPattern, start_pulse: int = 0) -> subsequence.pattern.Pattern:
5972
5973		"""
5974		Create a Pattern from a pending registration using a temporary subclass.
5975
5976		The pattern's play stream is dealt here, keyed by NAME (crc32 of
5977		"seed:name" plus any reroll nonce), so registration order is
5978		irrelevant and a pattern added live gets exactly the stream it would
5979		have had at startup.  ``start_pulse`` anchors the first cycle on the
5980		beat axis so the initial build reads the harmony window at the right
5981		place (the sequencer keeps the anchor current on every reschedule).
5982		"""
5983
5984		composition_ref = self
5985		rng = self._stream(pending.builder_fn.__name__)
5986
5987		class _DecoratorPattern (subsequence.pattern.Pattern):
5988
5989			"""
5990			Pattern subclass that delegates to a builder function on each reschedule.
5991			"""
5992
5993			def __init__ (self, pending: _PendingPattern, pattern_rng: typing.Optional[random.Random] = None) -> None:
5994
5995				"""
5996				Initialize the decorator pattern from pending registration details.
5997				"""
5998
5999				super().__init__(
6000					channel = pending.channel,
6001					length = pending.length,
6002					reschedule_lookahead = pending.reschedule_lookahead,
6003					device = pending.device,
6004					mirrors = pending.mirrors,
6005				)
6006
6007				self._builder_fn = pending.builder_fn
6008				self._drum_note_map = pending.drum_note_map
6009				self._cc_name_map = pending.cc_name_map
6010				self._nrpn_name_map = pending.nrpn_name_map
6011				self._default_grid: int = pending.default_grid
6012				self._wants_chord = _fn_has_parameter(pending.builder_fn, "chord")
6013				self._cycle_count = 0
6014				self._rng = pattern_rng
6015				self._muted = False
6016				self._min_energy = pending.min_energy
6017				self._energy_gated = False
6018				self._voice_leading_state: typing.Optional[subsequence.voicings.VoiceLeadingState] = (
6019					subsequence.voicings.VoiceLeadingState() if pending.voice_leading else None
6020				)
6021				self._tweaks: typing.Dict[str, typing.Any] = {}
6022
6023				# Anchor of the cycle being built, on the absolute pulse axis.
6024				# The sequencer updates this on every reschedule; the initial
6025				# value is the pattern's first scheduled start.
6026				self._cycle_start_pulse = start_pulse
6027
6028				self._rebuild()
6029
6030			def _rebuild (self) -> None:
6031
6032				"""
6033				Clear steps and call the builder function to repopulate.
6034				"""
6035
6036				self.steps = {}
6037				self.cc_events = []
6038				self.osc_events = []
6039				self.raw_note_events = []
6040				current_cycle = self._cycle_count
6041				self._cycle_count += 1
6042
6043				# lock(): re-deal the stream from its effective seed every
6044				# rebuild so a locked pattern realizes identically each cycle.
6045				# Checked here (engine-side) so it survives live reload.
6046				if self._builder_fn.__name__ in composition_ref._locked_names:
6047					locked_seed = composition_ref._stream_seed(self._builder_fn.__name__)
6048					if locked_seed is not None:
6049						self._rng = random.Random(locked_seed)
6050
6051				if self._muted:
6052					return
6053
6054				section_info = composition_ref._form_state.get_section_info() if composition_ref._form_state else None
6055				energy = composition_ref._current_energy(section_info)
6056				effective_key, effective_scale = composition_ref._effective_key_scale(section_info)
6057
6058				# Automatic energy gating: below the threshold the pattern is
6059				# silent this cycle (composing with _muted — a performer mute
6060				# always wins).  Gate flips log once.
6061				if self._min_energy is not None:
6062					gated = energy < self._min_energy
6063
6064					if gated != self._energy_gated:
6065						state_word = "closed" if gated else "open"
6066						logger.info(
6067							f"Pattern '{self._builder_fn.__name__}': energy gate {state_word} "
6068							f"(energy {energy:.2f}, min_energy {self._min_energy:g})"
6069						)
6070						self._energy_gated = gated
6071
6072					if gated:
6073						return
6074
6075				# The harmony view for this cycle, anchored at its start beat —
6076				# under variable harmonic rhythm the window, not the engine's
6077				# mutating singleton, is the source of truth.
6078				harmony_view: typing.Optional[HarmonyView] = None
6079
6080				if not composition_ref._harmony_horizon.is_empty:
6081					origin_beat = self._cycle_start_pulse / composition_ref._sequencer.pulses_per_beat
6082					harmony_view = HarmonyView(composition_ref._harmony_horizon, origin_beat)
6083
6084				builder = subsequence.pattern_builder.PatternBuilder(
6085					pattern = self,
6086					cycle = current_cycle,
6087					drum_note_map = self._drum_note_map,
6088					cc_name_map = self._cc_name_map,
6089					nrpn_name_map = self._nrpn_name_map,
6090					section = section_info,
6091					bar = composition_ref._builder_bar,
6092					conductor = composition_ref.conductor,
6093					rng = self._rng,
6094					tweaks = self._tweaks,
6095					default_grid = self._default_grid,
6096					data = composition_ref.data,
6097					# The effective key/scale re-anchors key-relative content
6098					# (degrees, romans, generated material) to the section /
6099					# form / composition tier in force — mode travels too.
6100					key = effective_key,
6101					scale = effective_scale,
6102					time_signature = composition_ref.time_signature,
6103					held_notes = composition_ref._sequencer._held_notes,
6104					harmony = harmony_view,
6105					section_motifs = composition_ref._section_motifs,
6106					energy = energy,
6107					# So p.scratch() can take a child stream keyed off this
6108					# pattern's, rather than drawing from the pattern's own.
6109					stream_seed = composition_ref._stream_seed(self._builder_fn.__name__),
6110				)
6111
6112				try:
6113
6114					if self._wants_chord:
6115
6116						# The two-parameter convention: the injected chord is
6117						# the cycle-start snapshot from the window (falling
6118						# back to the engine before the clock has run).
6119						chord = harmony_view.chord if harmony_view is not None else (
6120							composition_ref._harmonic_state.get_current_chord()
6121							if composition_ref._harmonic_state is not None else None
6122						)
6123
6124						if chord is not None:
6125							injected = _InjectedChord(
6126								chord,
6127								self._voice_leading_state,
6128								next_chord = harmony_view.next_chord if harmony_view is not None else None,
6129								beats_remaining = harmony_view.until_change if harmony_view is not None else None,
6130							)
6131							self._builder_fn(builder, injected)
6132						else:
6133							self._builder_fn(builder)
6134
6135					else:
6136						self._builder_fn(builder)
6137
6138				except Exception:
6139					# Discard whatever the builder placed before it raised —
6140					# otherwise a half-built pattern plays and the log lies.
6141					self.steps = {}
6142					self.cc_events = []
6143					self.osc_events = []
6144					self.raw_note_events = []
6145					logger.exception("Error in pattern builder '%s' (cycle %d) - pattern will be silent this cycle", self._builder_fn.__name__, current_cycle)
6146
6147				# Auto-apply global tuning if set and not already applied by the builder.
6148				if (
6149					composition_ref._tuning is not None
6150					and not builder._tuning_applied
6151					and not (composition_ref._tuning_exclude_drums and self._drum_note_map)
6152				):
6153					import subsequence.tuning as _tuning_mod
6154					_tuning_mod.apply_tuning_to_pattern(
6155						self,
6156						composition_ref._tuning,
6157						bend_range=composition_ref._tuning_bend_range,
6158						channels=composition_ref._tuning_channels,
6159						reference_note=composition_ref._tuning_reference_note,
6160					)
6161
6162			def on_reschedule (self) -> None:
6163
6164				"""
6165				Rebuild the pattern from the builder function before the next cycle.
6166				"""
6167
6168				self._rebuild()
6169
6170		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 pause(self) -> None:
3918	def pause (self) -> None:
3919
3920		"""
3921		Hold playback where it is, keeping the composition's place.
3922
3923		The clock stops advancing, sounding notes are released, and MIDI Stop
3924		is sent to any hardware following the clock output.  :meth:`resume`
3925		continues from the same pulse, beat and bar — where stopping and
3926		playing again would start the piece over.
3927
3928		Bar and cycle counters hold too, so patterns resume mid-phrase rather
3929		than jumping.  A note cut short by the pause is not re-struck on
3930		resume; it returns on its pattern's next cycle.
3931
3932		Idempotent and safe to call from any thread.  Ignored, with a log line,
3933		when the transport is not ours to hold — under ``clock_follow=True`` or
3934		an active Ableton Link session.
3935		"""
3936
3937		self._sequencer.pause()

Hold playback where it is, keeping the composition's place.

The clock stops advancing, sounding notes are released, and MIDI Stop is sent to any hardware following the clock output. resume() continues from the same pulse, beat and bar — where stopping and playing again would start the piece over.

Bar and cycle counters hold too, so patterns resume mid-phrase rather than jumping. A note cut short by the pause is not re-struck on resume; it returns on its pattern's next cycle.

Idempotent and safe to call from any thread. Ignored, with a log line, when the transport is not ours to hold — under clock_follow=True or an active Ableton Link session.

def resume(self) -> None:
3939	def resume (self) -> None:
3940
3941		"""
3942		Continue playback from where :meth:`pause` held it.
3943
3944		Sends MIDI Continue rather than Start, so downstream hardware picks up
3945		where it left off instead of resetting to the top of its own pattern.
3946		Idempotent.
3947		"""
3948
3949		self._sequencer.resume()

Continue playback from where pause() held it.

Sends MIDI Continue rather than Start, so downstream hardware picks up where it left off instead of resetting to the top of its own pattern. Idempotent.

is_paused: bool
3951	@property
3952	def is_paused (self) -> bool:
3953
3954		"""True while playback is held by :meth:`pause`."""
3955
3956		return self._sequencer.paused

True while playback is held by pause().

def mute(self, name: str) -> None:
3958	def mute (self, name: str) -> None:
3959
3960		"""
3961		Mute a running pattern by name.
3962		
3963		The pattern continues to 'run' and increment its cycle count in 
3964		the background, but it will not produce any MIDI notes until unmuted.
3965
3966		Parameters:
3967			name: The function name of the pattern to mute.
3968		"""
3969
3970		if name not in self._running_patterns:
3971			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3972
3973		# The performer takes ownership: if a transition's approach window had
3974		# muted this pattern, drop it from that set so the section boundary
3975		# does not silently unmute it ("performer mutes win").
3976		self._transition_muted.discard(name)
3977
3978		self._running_patterns[name]._muted = True
3979		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:
3981	def unmute (self, name: str) -> None:
3982
3983		"""
3984		Unmute a previously muted pattern.
3985		"""
3986
3987		if name not in self._running_patterns:
3988			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
3989
3990		# Symmetric ownership claim: an explicit unmute means the transition
3991		# machinery should no longer manage this pattern at the boundary.
3992		self._transition_muted.discard(name)
3993
3994		self._running_patterns[name]._muted = False
3995		logger.info(f"Unmuted pattern: {name}")

Unmute a previously muted pattern.

def unregister(self, name: str) -> None:
3997	def unregister (self, name: str) -> None:
3998
3999		"""Fully remove a running pattern from rotation.
4000
4001		Unlike ``mute()`` (which keeps the pattern alive but silent),
4002		``unregister()`` tears the pattern down entirely.  It sets
4003		``pattern._removed = True`` so the sequencer's reschedule loop
4004		skips re-adding it on the next pulse; sends ``note_off`` for any
4005		of the pattern's currently-sounding notes on the primary
4006		destination AND on every mirror destination (so drones and
4007		sustaining notes stop immediately); and removes the entry from
4008		``_running_patterns`` so it no longer appears in ``live_info()``,
4009		the terminal grid, or any other consumer that enumerates running
4010		patterns.
4011
4012		Already-queued events in the sequencer's event queue play out —
4013		note_offs are paired with their note_ons at queue time, so notes
4014		end at their natural duration; only drones rely on the targeted
4015		``_stop_pattern_notes`` pass.
4016
4017		Idempotent: silently logs a ``debug`` and returns if the pattern
4018		is already absent.  Useful from both the live REPL
4019		(``composition.live()``) and the file watcher
4020		(``composition.watch()``), which calls this for any pattern
4021		removed from the watched file between reloads.
4022
4023		Parameters:
4024			name: Function name of the pattern to remove.
4025		"""
4026
4027		if name not in self._running_patterns:
4028			logger.debug(f"unregister() no-op: pattern '{name}' not running")
4029			return
4030
4031		pattern = self._running_patterns[name]
4032
4033		# Mark for removal first so the reschedule loop sees the flag even if
4034		# it fires concurrently with the note-off pass below.
4035		pattern._removed = True
4036
4037		# Stop sustaining notes (including drones) on every destination this
4038		# pattern outputs to.  Fire-and-forget across threads via the event
4039		# loop; ``_stop_pattern_notes`` acquires the queue lock internally.
4040		if self._sequencer._event_loop is not None:
4041			asyncio.run_coroutine_threadsafe(
4042				self._sequencer._stop_pattern_notes(pattern),
4043				loop = self._sequencer._event_loop,
4044			)
4045
4046		def _finalise_removal () -> None:
4047			self._running_patterns.pop(name, None)
4048
4049			# Forget any pending (not-yet-graduated) declaration too, so a
4050			# later live reload cannot resurrect the pattern.
4051			self._pending_patterns = [
4052				pending for pending in self._pending_patterns
4053				if pending.builder_fn.__name__ != name
4054			]
4055
4056			logger.info(f"Unregistered pattern: {name}")
4057
4058		# The running-patterns dict is iterated by the display, web UI, and
4059		# reschedule loop on the event loop thread — mutate it there when this
4060		# call arrives from another thread (e.g. the live TCP server).
4061		loop = self._sequencer._event_loop
4062
4063		try:
4064			on_loop = loop is not None and asyncio.get_running_loop() is loop
4065		except RuntimeError:
4066			on_loop = False
4067
4068		if loop is not None and loop.is_running() and not on_loop:
4069			loop.call_soon_threadsafe(_finalise_removal)
4070		else:
4071			_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:
4073	def mirror (self, name: str, device: int, channel: int, drum_note_map: typing.Optional[typing.Dict[str, int]] = None) -> None:
4074
4075		"""
4076		Add a mirror destination to a running pattern.
4077
4078		Every note, CC, pitch bend, NRPN/RPN, program change, SysEx, and drone
4079		event the pattern emits will also be sent to ``(device, channel)``,
4080		starting from the next cycle rebuild.  Idempotent on ``(device, channel)``
4081		— calling with the same destination twice does not double-fan; calling
4082		again with a different ``drum_note_map`` re-points it in place.
4083
4084		Parameters:
4085			name: Function name of the pattern to mirror.
4086			device: Output device index (the integer returned from
4087				``midi_output()``; 0 = primary device).
4088			channel: MIDI channel using this composition's numbering convention
4089				(1-16 by default; 0-15 if ``zero_indexed_channels=True``).
4090			drum_note_map: Optional per-destination drum map.  When set, mirrored
4091				drum hits are re-resolved by name through it, so a named voice
4092				lands on this device's own note number — see the README
4093				"MIDI mirroring" section.
4094
4095		Bandwidth: each mirror adds another full copy of the pattern's events.
4096		See the README "MIDI mirroring" section for the tradeoffs.
4097		"""
4098
4099		if name not in self._running_patterns:
4100			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4101
4102		resolved_channel = self._resolve_channel(channel)
4103		prefix = (device, resolved_channel)
4104		entry: subsequence.pattern.MirrorSpec = prefix if drum_note_map is None else (device, resolved_channel, drum_note_map)
4105
4106		pattern = self._running_patterns[name]
4107
4108		# Mirror-to-self check: comparing the (device, channel) prefix against the
4109		# live pattern's resolved destination.  Unlike the decorator path this is
4110		# always concrete.
4111		if prefix == (pattern.device, pattern.channel):
4112			logger.warning(
4113				f"Mirror destination {prefix} matches '{name}'s primary destination "
4114				f"— every event will double-fire on this (device, channel).  This is almost "
4115				f"certainly unintended."
4116			)
4117
4118		# Idempotent on (device, channel): replace any existing entry for the same
4119		# destination (so its map can be re-pointed), else append.
4120		existing_index = next((idx for idx, e in enumerate(pattern.mirrors) if (e[0], e[1]) == prefix), None)
4121		if existing_index is None:
4122			pattern.mirrors.append(entry)
4123			logger.info(f"Mirror added: {name} -> device={device}, channel={resolved_channel}")
4124		elif pattern.mirrors[existing_index] != entry:
4125			pattern.mirrors[existing_index] = entry
4126			logger.info(f"Mirror updated: {name} -> device={device}, channel={resolved_channel}")
4127		else:
4128			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:
4130	def unmirror (self, name: str, device: int, channel: int) -> None:
4131
4132		"""
4133		Remove a single mirror destination from a running pattern.
4134
4135		Matches on ``(device, channel)`` only — any attached ``drum_note_map`` is
4136		ignored.  Idempotent: silently does nothing if the destination is not
4137		currently mirrored.  The change applies on the next cycle rebuild.
4138		"""
4139
4140		if name not in self._running_patterns:
4141			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4142
4143		resolved_channel = self._resolve_channel(channel)
4144		prefix = (device, resolved_channel)
4145
4146		pattern = self._running_patterns[name]
4147
4148		filtered = [e for e in pattern.mirrors if (e[0], e[1]) != prefix]
4149		if len(filtered) != len(pattern.mirrors):
4150			pattern.mirrors[:] = filtered
4151			logger.info(f"Mirror removed: {name} -> device={device}, channel={resolved_channel}")
4152		else:
4153			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:
4155	def unmirror_all (self, name: str) -> None:
4156
4157		"""
4158		Remove every mirror destination from a running pattern.
4159		"""
4160
4161		if name not in self._running_patterns:
4162			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4163
4164		pattern = self._running_patterns[name]
4165
4166		if pattern.mirrors:
4167			pattern.mirrors.clear()
4168			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:
4170	def tweak (self, name: str, **kwargs: typing.Any) -> None:
4171
4172		"""Override parameters for a running pattern.
4173
4174		Values set here are available inside the pattern's builder
4175		function via ``p.param()``.  They persist across rebuilds
4176		until explicitly changed or cleared.  Changes take effect
4177		on the next rebuild cycle.
4178
4179		Parameters:
4180			name: The function name of the pattern.
4181			``**kwargs``: Parameter names and their new values.
4182
4183		Example (from the live REPL)::
4184
4185			composition.tweak("bass", pitches=[48, 52, 55, 60])
4186		"""
4187
4188		if name not in self._running_patterns:
4189			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4190
4191		self._running_patterns[name]._tweaks.update(kwargs)
4192		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:
4194	def clear_tweak (self, name: str, *param_names: str) -> None:
4195
4196		"""Remove tweaked parameters from a running pattern.
4197
4198		If no parameter names are given, all tweaks for the pattern
4199		are cleared and every ``p.param()`` call reverts to its
4200		default.
4201
4202		Parameters:
4203			name: The function name of the pattern.
4204			*param_names: Specific parameter names to clear.  If
4205				omitted, all tweaks are removed.
4206		"""
4207
4208		if name not in self._running_patterns:
4209			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4210
4211		if not param_names:
4212			self._running_patterns[name]._tweaks.clear()
4213			logger.info(f"Cleared all tweaks for pattern '{name}'")
4214		else:
4215			for param_name in param_names:
4216				self._running_patterns[name]._tweaks.pop(param_name, None)
4217			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]:
4219	def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]:
4220
4221		"""Return a copy of the current tweaks for a running pattern.
4222
4223		Parameters:
4224			name: The function name of the pattern.
4225		"""
4226
4227		if name not in self._running_patterns:
4228			raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}")
4229
4230		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:
4232	def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None:
4233
4234		"""
4235		Register a custom function to run on a repeating beat-based cycle.
4236
4237		Subsequence automatically runs synchronous functions in a thread pool
4238		so they don't block the timing-critical MIDI clock. Async functions
4239		are run directly on the event loop.
4240
4241		Parameters:
4242			fn: The function to call.
4243			cycle_beats: How often to call it (e.g., 4 = every bar).
4244			reschedule_lookahead: How far in advance to schedule the next call.
4245			wait_for_initial: If True, run the function once during startup
4246				and wait for it to complete before playback begins. This
4247				ensures ``composition.data`` is populated before patterns
4248				first build. Implies ``defer=True`` for the repeating
4249				schedule.
4250			defer: If True, skip the pulse-0 fire and defer the first
4251				repeating call to just before the second cycle boundary.
4252
4253		Raises:
4254			RuntimeError: If called after ``play()`` has started — scheduled
4255				tasks register at startup, so a late registration would be
4256				silently ignored otherwise.
4257		"""
4258
4259		if self._sequencer.running:
4260			raise RuntimeError("schedule() must be called before play() - scheduled tasks register at startup")
4261
4262		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:
4264	def form (
4265		self,
4266		sections: typing.Union[
4267			"subsequence.forms.Form",
4268			typing.List[typing.Any],
4269			typing.Iterator[typing.Tuple[str, int]],
4270			typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
4271		],
4272		loop: bool = False,
4273		start: typing.Optional[str] = None,
4274		at_end: str = "stop",
4275		key: typing.Optional[str] = None,
4276		scale: typing.Optional[str] = None,
4277	) -> None:
4278
4279		"""
4280		Define the structure (sections) of the composition.
4281
4282		You can define form in four ways:
4283
4284		1. **Form value**: a frozen :class:`~subsequence.forms.Form` of
4285		   :class:`~subsequence.forms.Section` values — the payload home
4286		   (energy, key per section); editable, navigable.
4287		2. **Sequence (List)**: a fixed order of ``(name, bars)`` tuples
4288		   or Sections (lists coerce — they are the same form).
4289		3. **Graph (Dict)**: dynamic transitions based on weights.
4290		4. **Generator**: a Python generator that yields ``(name, bars)`` pairs.
4291
4292		Form-value and list forms are **navigable**: ``form_jump()`` and
4293		``form_next()`` work on them (the jump lands on the next occurrence
4294		of the name, wrapping).
4295
4296		Re-binding ``form()`` during playback takes effect at the next bar —
4297		the clock reads the current form state on every bar, so the new form
4298		advances from there (its first section plays from its first bar).
4299
4300		Parameters:
4301			sections: The form definition (Form, List, Dict, or Generator).
4302			loop: Sugar for ``at_end="loop"``.
4303			start: The section to start with (Graph mode only).
4304			at_end: What happens when a sequence form runs out —
4305				``"stop"`` (the form finishes and patterns see no section;
4306				default), ``"hold"`` (the final section repeats until
4307				navigated away from), or ``"loop"`` (start over).  Graphs
4308				end via their terminal sections instead.
4309			key: A form-level key — the **form tier** of the key-source
4310				chain (``Section.key`` overrides it; it overrides the
4311				composition key).  Re-anchors key-relative content for the
4312				whole form.  When *sections* is a ``Form`` value carrying its
4313				own ``key``, that value is used unless this argument overrides.
4314			scale: A form-level scale/mode, paired with ``key``.
4315
4316		Example:
4317			```python
4318			# A simple pop structure
4319			comp.form([
4320				("verse", 8),
4321				("chorus", 8),
4322				("verse", 8),
4323				("chorus", 16)
4324			])
4325
4326			# The same structure with payloads, held open at the end
4327			S = subsequence.Section
4328			comp.form(subsequence.Form([
4329				S("verse", 8, energy=0.5), S("chorus", 8, energy=0.9),
4330			]), at_end="hold")
4331			```
4332		"""
4333
4334		# Seed FormState at form() time (per-call salt) so build-time walks —
4335		# the frozen clones form_freeze will take — are deterministic without
4336		# play(); the play-time stream is re-dealt name-keyed in _run().
4337		self._form_count += 1
4338
4339		self._form_state = subsequence.form_state.FormState(
4340			sections,
4341			loop = loop,
4342			start = start,
4343			rng = self._stream(f"form:{self._form_count}"),
4344			at_end = at_end,
4345		)
4346
4347		# A Form value carries energy payloads — that counts as an energy
4348		# source for the min_energy registration check in _run().
4349		self._form_has_payload = isinstance(sections, subsequence.forms.Form) or (
4350			isinstance(sections, list) and any(isinstance(element, subsequence.forms.Section) for element in sections)
4351		)
4352
4353		# Form-tier key/scale: an explicit argument wins; otherwise a Form
4354		# value's own key/scale seeds the tier.  Re-binding the form drops any
4355		# stale per-section resolution cache.
4356		if isinstance(sections, subsequence.forms.Form):
4357			self._form_key = key if key is not None else sections.key
4358			self._form_scale = scale if scale is not None else sections.scale
4359		else:
4360			self._form_key = key
4361			self._form_scale = scale
4362
4363		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:
4365	def form_freeze (self, sections: typing.Optional[int] = None) -> "subsequence.forms.Form":
4366
4367		"""Freeze the graph form's walk into an editable :class:`~subsequence.forms.Form`.
4368
4369		Walks a **clone** of the live form state — the same RNG state, so the
4370		frozen path is exactly the path the live graph would have played —
4371		and returns it as a Form value: inspect it, edit it
4372		(``path.replace(3, bars=16)``), and rebind it with
4373		``composition.form(path, at_end=...)``.  The live form state is
4374		untouched (rebinding replaces it).
4375
4376		Parameters:
4377			sections: Number of sections to freeze.  Without it, the walk
4378				runs until a terminal section; a graph with no terminal
4379				sections requires ``sections=`` explicitly.
4380
4381		Raises:
4382			ValueError: If no graph form is bound (a list form is already a
4383				frozen sequence), the form has already finished, or the walk
4384				cannot terminate.
4385
4386		Example::
4387
4388			composition.form({...}, start="intro")
4389			path = composition.form_freeze()          # the walk, frozen
4390			composition.form(path, at_end="stop")     # rebind the editable value
4391		"""
4392
4393		fs = self._form_state
4394
4395		if fs is None or fs._graph is None or fs._section_bars is None:
4396			raise ValueError(
4397				"form_freeze() freezes a graph form's walk — call form() with a dict first "
4398				"(a list form is already a frozen sequence)"
4399			)
4400
4401		if fs._current is None:
4402			raise ValueError("the form has already finished — nothing left to freeze")
4403
4404		if sections is not None and sections < 1:
4405			raise ValueError("sections must be at least 1")
4406
4407		if sections is None and not fs._terminal_sections:
4408			raise ValueError(
4409				"this graph has no terminal section, so the walk would never end — "
4410				"pass sections=n to bound it"
4411			)
4412
4413		# Clone the RNG state: the frozen walk reproduces the live form's
4414		# future draws without consuming them.
4415		rng = random.Random()
4416		rng.setstate(fs._rng.getstate())
4417
4418		walked = [fs._current]
4419		next_name = fs._next_section_name		# already decided by the live state
4420
4421		while next_name is not None:
4422			if sections is not None and len(walked) >= sections:
4423				break
4424			if sections is None and len(walked) >= 10000:
4425				raise ValueError(
4426					"form_freeze() walked 10000 sections without reaching a terminal — "
4427					"the terminals look unreachable; pass sections=n to bound the walk"
4428				)
4429
4430			walked.append(subsequence.forms.Section(name = next_name, bars = fs._section_bars[next_name]))
4431			next_name = None if next_name in fs._terminal_sections else fs._graph.choose_next(next_name, rng)
4432
4433		# Carry the form-tier key/scale onto the frozen value so a freeze →
4434		# rebind round-trip is lossless (an explicit form(key=) on rebind
4435		# still overrides).
4436		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:
4438	def energy (self, energies: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]]) -> None:
4439
4440		"""Set per-section energy — the arranging dial, as one plain dict.
4441
4442		``{"verse": 0.5, "chorus": 0.9, "build": (0.3, 1.0)}`` — a float is
4443		the section's level; a ``(start, end)`` tuple interpolates across the
4444		section (a build).  Patterns read ``p.energy`` (0.5 when nothing is
4445		configured) and gate themselves, or declare ``min_energy=`` on
4446		``pattern()`` for automatic muting.
4447
4448		The dict **overrides** any energy payload carried by bound
4449		:class:`~subsequence.forms.Section` values — it is the later,
4450		performance-level dial.  Re-calling replaces the whole mapping
4451		(idempotent, live-reload friendly).
4452
4453		Example::
4454
4455			composition.energy({"intro": 0.2, "verse": 0.55, "drop": 0.95})
4456		"""
4457
4458		validated: typing.Dict[str, typing.Union[float, typing.Tuple[float, float]]] = {}
4459
4460		for name, value in energies.items():
4461			if isinstance(value, tuple):
4462				if len(value) != 2:
4463					raise ValueError(f"energy ramp for {name!r} must be (start, end), got {value!r}")
4464				start_level, end_level = float(value[0]), float(value[1])
4465				for level in (start_level, end_level):
4466					if not 0.0 <= level <= 1.0:
4467						raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4468				validated[name] = (start_level, end_level)
4469			else:
4470				level = float(value)
4471				if not 0.0 <= level <= 1.0:
4472					raise ValueError(f"energy for {name!r} must be 0.0–1.0, got {value!r}")
4473				validated[name] = level
4474
4475		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:
4507	def on_section (self, callback: typing.Callable[..., typing.Any]) -> None:
4508
4509		"""Register a callback fired on every section change.
4510
4511		The callback receives the new :class:`~subsequence.form_state.SectionInfo`
4512		(or ``None`` when the form finishes).  It fires from the form clock,
4513		one lookahead-beat **early** — in time to affect the new section's
4514		first patterns — and once at play start for the opening section.
4515
4516		Example::
4517
4518			composition.on_section(lambda info: print(f"now: {info.name if info else 'end'}"))
4519		"""
4520
4521		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:
4523	def transition (
4524		self,
4525		before: str,
4526		fill: typing.Optional[typing.Any] = None,
4527		channel: typing.Optional[int] = None,
4528		beat: float = 0.0,
4529		mute: typing.Optional[typing.List[str]] = None,
4530		beats: typing.Optional[float] = None,
4531		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4532		device: subsequence.midi_utils.DeviceId = None,
4533	) -> None:
4534
4535		"""Declare boundary material — the automatic fill or mute, one line.
4536
4537		``before`` names the incoming section (``"chorus"``), or ``"*"`` for
4538		any *different* section (repeats don't fire it).  Two actions,
4539		combinable:
4540
4541		- ``fill=`` (+ ``channel=``, ``beat=``): a Motif played in the last
4542		  bar before the boundary, starting at ``beat`` of that bar.  Drum
4543		  names resolve through ``drum_note_map=`` if given, otherwise the
4544		  map is borrowed from a registered pattern on the same channel.
4545		- ``mute=`` (+ ``beats=``): pattern names muted over the approach
4546		  and unmuted at the boundary.  Muting is **bar-granular** (the
4547		  existing rule), so ``beats`` rounds up to whole bars.  Performer
4548		  mutes win: a pattern you muted yourself stays muted.
4549
4550		Transitions stack — call once per rule.  Registration is additive
4551		and idempotent per identical rule.
4552
4553		Example::
4554
4555			composition.transition(before="*", fill=FILL, channel=10, beat=2.0)
4556			composition.transition(before="drop", mute=["pads"], beats=4)
4557		"""
4558
4559		if fill is None and mute is None:
4560			raise ValueError("transition() needs fill= and/or mute= — it declares what happens at the boundary")
4561
4562		if fill is not None:
4563			if channel is None:
4564				raise ValueError("transition(fill=) needs channel= — the fill must land somewhere")
4565			if not hasattr(fill, "events") or not hasattr(fill, "length"):
4566				raise TypeError(f"fill must be a Motif-like value with .events/.length, got {type(fill).__name__}")
4567
4568		if mute is not None and beats is None:
4569			beats = float(self.time_signature[0])		# one bar by default
4570
4571		rule = _Transition(
4572			before = before,
4573			fill = fill,
4574			channel = self._resolve_channel(channel) if channel is not None else None,
4575			beat = float(beat),
4576			mute = list(mute) if mute is not None else None,
4577			beats = beats,
4578			drum_note_map = drum_note_map,
4579			device = device,			# resolved at fire time — names aren't known until play()
4580		)
4581
4582		if rule not in self._transitions:
4583			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:
4762	def pattern (
4763		self,
4764		channel: int,
4765		beats: typing.Optional[float] = None,
4766		bars: typing.Optional[float] = None,
4767		steps: typing.Optional[float] = None,
4768		step_duration: typing.Optional[float] = None,
4769		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4770		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4771		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4772		reschedule_lookahead: float = 1,
4773		voice_leading: bool = False,
4774		device: subsequence.midi_utils.DeviceId = None,
4775		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4776		min_energy: typing.Optional[float] = None,
4777	) -> typing.Callable:
4778
4779		"""
4780		Register a function as a repeating MIDI pattern.
4781
4782		The decorated function will be called once per cycle to 'rebuild' its
4783		content. This allows for generative logic that evolves over time.
4784
4785		Two ways to specify pattern length:
4786
4787		- **Duration mode** (default): use ``beats=`` or ``bars=``.
4788		  The grid defaults to sixteenth-note resolution.
4789		- **Step mode**: use ``steps=`` paired with ``step_duration=``.
4790		  The grid equals the step count, so ``p.hit_steps()`` indices map
4791		  directly to steps.
4792
4793		Parameters:
4794			channel: MIDI channel. By default uses 1-based numbering (1-16).
4795				Set ``zero_indexed_channels=True`` on the ``Composition`` to use
4796				0-based numbering (0-15), matching the raw MIDI protocol, instead.
4797			beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar.
4798			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4). ``bars=2`` = 8 beats.
4799			steps: Step count for step mode. Requires ``step_duration=``.
4800			step_duration: Duration of one step in beats (e.g. ``dur.SIXTEENTH``).
4801				Requires ``steps=``.
4802			drum_note_map: Optional mapping for drum instruments.
4803			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4804				Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``.
4805			nrpn_name_map: Optional mapping of NRPN parameter names (strings) to
4806				14-bit parameter numbers (0–16383).  Enables string-based names
4807				in ``p.nrpn()`` and ``p.nrpn_ramp()`` — typically a
4808				device-specific dictionary (e.g. Sequential Take 5's
4809				``Osc1FreqFine`` → 9).
4810			reschedule_lookahead: Beats in advance to compute the next cycle.
4811			voice_leading: If True, chords in this pattern will automatically
4812				use inversions that minimize voice movement.
4813			mirrors: Optional list of additional ``(device, channel)`` destinations
4814				to duplicate every event from this pattern onto.  Notes, CCs, pitch
4815				bend, NRPN/RPN bursts, program changes, SysEx, and drone events are
4816				all mirrored; OSC events are not (OSC is not bound to a MIDI port).
4817				``device`` is the integer index returned by ``midi_output()`` (0 =
4818				primary).  ``channel`` follows this composition's channel-numbering
4819				convention.  See also ``mirror()`` / ``unmirror()`` for live toggling.
4820			min_energy: Automatic energy gating — the pattern is silent while
4821				the current section's energy (``composition.energy()`` dict,
4822				or the bound Section payload) is below this threshold.
4823				Composes with ``mute()``: a performer mute always wins.
4824
4825		Example:
4826			```python
4827			@comp.pattern(channel=1, beats=4)
4828			def chords (p):
4829				p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9)
4830
4831			@comp.pattern(channel=1, bars=2)
4832			def long_phrase (p):
4833				...
4834
4835			@comp.pattern(channel=1, steps=6, step_duration=dur.SIXTEENTH)
4836			def riff (p):
4837				p.sequence(steps=[0, 1, 3, 5], pitches=60)
4838			```
4839		"""
4840
4841		channel = self._resolve_channel(channel)
4842
4843		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4844
4845		# Resolve device string name to index if possible now; otherwise store
4846		# the raw DeviceId and resolve it in _run() once all devices are open.
4847		resolved_device: subsequence.midi_utils.DeviceId = device
4848
4849		# Mirror-to-self check is only reliable when the primary device is a
4850		# concrete integer at decoration time.  ``None`` resolves to device 0
4851		# downstream, so we treat it as 0 here too.  Strings are deferred to
4852		# ``_run()`` and we skip the check for them.
4853		primary: typing.Optional[typing.Tuple[int, int]]
4854		if isinstance(resolved_device, str):
4855			primary = None
4856		else:
4857			primary = (resolved_device if resolved_device is not None else 0, channel)
4858		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4859
4860		def decorator (fn: typing.Callable) -> typing.Callable:
4861
4862			"""
4863			Wrap the builder function and register it as a pending pattern.
4864			During live sessions, hot-swap an existing pattern's builder instead.
4865			"""
4866
4867			# Record this declaration so the live-reload deletion diff knows the
4868			# pattern is still present in the source (see _apply_source_async).
4869			self._declared_names.add(fn.__name__)
4870
4871			# Hot-swap: if we're live and a pattern with this name exists, replace its builder.
4872			if self._is_live and fn.__name__ in self._running_patterns:
4873				running = self._running_patterns[fn.__name__]
4874				running._builder_fn = fn
4875				running._wants_chord = _fn_has_parameter(fn, "chord")
4876				logger.info(f"Hot-swapped pattern: {fn.__name__}")
4877				return fn
4878
4879			# Names key the seeded stream, mutes, tweaks, and reroll/lock — a
4880			# duplicate means two scheduled copies sharing one stream with
4881			# only one reachable by name.  Warn loudly at registration.
4882			if any(existing.builder_fn.__name__ == fn.__name__ for existing in self._pending_patterns):
4883				logger.warning(
4884					f"Duplicate pattern name '{fn.__name__}': both copies will be "
4885					f"scheduled, they share one seeded stream, and only one is "
4886					f"reachable by name — rename one of them."
4887				)
4888
4889			pending = _PendingPattern(
4890				builder_fn = fn,
4891				channel = channel,  # already resolved to 0-indexed
4892				length = beat_length,
4893				default_grid = default_grid,
4894				drum_note_map = drum_note_map,
4895				cc_name_map = cc_name_map,
4896				nrpn_name_map = nrpn_name_map,
4897				reschedule_lookahead = reschedule_lookahead,
4898				voice_leading = voice_leading,
4899				# For int/None: resolve immediately.  For str: store 0 as
4900				# placeholder; _resolve_pending_devices() fixes it in _run().
4901				device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device,
4902				raw_device = resolved_device,
4903				mirrors = resolved_mirrors,
4904				min_energy = min_energy,
4905			)
4906
4907			self._pending_patterns.append(pending)
4908
4909			return fn
4910
4911		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:
4913	def layer (
4914		self,
4915		*builder_fns: typing.Callable,
4916		channel: int,
4917		beats: typing.Optional[float] = None,
4918		bars: typing.Optional[float] = None,
4919		steps: typing.Optional[float] = None,
4920		step_duration: typing.Optional[float] = None,
4921		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
4922		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
4923		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
4924		reschedule_lookahead: float = 1,
4925		voice_leading: bool = False,
4926		device: subsequence.midi_utils.DeviceId = None,
4927		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
4928	) -> None:
4929
4930		"""
4931		Combine multiple functions into a single MIDI pattern.
4932
4933		This is useful for composing complex patterns out of reusable
4934		building blocks (e.g., a 'kick' function and a 'snare' function).
4935
4936		See ``pattern()`` for the full description of ``beats``, ``bars``,
4937		``steps``, and ``step_duration``.
4938
4939		Parameters:
4940			builder_fns: One or more pattern builder functions.
4941			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
4942			beats: Duration in beats (quarter notes).
4943			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
4944			steps: Step count for step mode. Requires ``step_duration=``.
4945			step_duration: Duration of one step in beats. Requires ``steps=``.
4946			drum_note_map: Optional mapping for drum instruments.
4947			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
4948			nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit
4949				parameter numbers.
4950			reschedule_lookahead: Beats in advance to compute the next cycle.
4951			voice_leading: If True, chords use smooth voice leading.
4952			mirrors: Optional list of additional ``(device, channel)`` destinations
4953				to duplicate every event onto.  See ``pattern()`` for details.
4954		"""
4955
4956		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, beats_per_bar=self.time_signature[0])
4957
4958		# Resolve channel up-front so the mirror-to-self check has the canonical
4959		# primary form to compare against.
4960		resolved_channel = self._resolve_channel(channel)
4961
4962		# See pattern() for the same comment about None / str handling.
4963		primary: typing.Optional[typing.Tuple[int, int]]
4964		if isinstance(device, str):
4965			primary = None
4966		else:
4967			primary = (device if device is not None else 0, resolved_channel)
4968		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
4969
4970		wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns)
4971
4972		if wants_chord:
4973
4974			def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None:
4975
4976				for fn in builder_fns:
4977					if _fn_has_parameter(fn, "chord"):
4978						fn(p, chord)
4979					else:
4980						fn(p)
4981
4982		else:
4983
4984			def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:  # type: ignore[misc]
4985
4986				for fn in builder_fns:
4987					fn(p)
4988
4989		# Give the merged builder a stable, unique name derived from its
4990		# components so multiple layer() calls don't all register under
4991		# "merged_builder" and collide in _running_patterns (which made
4992		# mute/tweak/unregister/live_info reach only the LAST layer).  "+" can't
4993		# appear in a Python identifier, so this never clashes with a real
4994		# pattern function's name.
4995		base_name = ("+".join(fn.__name__ for fn in builder_fns) or "layer") + f"@ch{resolved_channel}"
4996		merged_name = base_name
4997		suffix = 2
4998
4999		# Two layers with the same components (e.g. on different saves of a
5000		# live file) must map to the same names pass-over-pass, while two
5001		# DIFFERENT layers sharing components in one pass must not collide.
5002		while merged_name in self._declared_names:
5003			merged_name = f"{base_name}#{suffix}"
5004			suffix += 1
5005
5006		merged_builder.__name__ = merged_name
5007
5008		# Record the declaration for the live-reload deletion diff, and hot-swap
5009		# in place when this layer is already running so a reload picks up edits
5010		# to the component functions without losing the pattern's cycle count,
5011		# tweaks, or mirrors (mirrors the pattern() decorator's hot-swap).
5012		self._declared_names.add(merged_builder.__name__)
5013
5014		if self._is_live and merged_builder.__name__ in self._running_patterns:
5015			running = self._running_patterns[merged_builder.__name__]
5016			running._builder_fn = merged_builder
5017			running._wants_chord = wants_chord
5018			logger.info(f"Hot-swapped layer: {merged_builder.__name__}")
5019			return
5020
5021		pending = _PendingPattern(
5022			builder_fn = merged_builder,
5023			channel = resolved_channel,  # already resolved to 0-indexed above
5024			length = beat_length,
5025			default_grid = default_grid,
5026			drum_note_map = drum_note_map,
5027			cc_name_map = cc_name_map,
5028			nrpn_name_map = nrpn_name_map,
5029			reschedule_lookahead = reschedule_lookahead,
5030			voice_leading = voice_leading,
5031			mirrors = resolved_mirrors,
5032			device = 0 if (device is None or isinstance(device, str)) else device,
5033			raw_device = device,
5034		)
5035
5036		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:
5038	def chords (
5039		self,
5040		*,
5041		channel: int,
5042		progression: subsequence.progressions.ProgressionSource,
5043		harmonic_rhythm: subsequence.progressions.HarmonicRhythmSpec,
5044		bars: typing.Optional[float] = None,
5045		beats: typing.Optional[float] = None,
5046		voicing: subsequence.progressions.VoicingSpec = (3, 4),
5047		velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY,
5048		detached: typing.Optional[float] = None,
5049		root: int = 60,
5050		key: typing.Optional[str] = None,
5051		seed: typing.Optional[int] = None,
5052		device: subsequence.midi_utils.DeviceId = None,
5053		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5054	) -> subsequence.progressions.Progression:
5055
5056		"""Declare a self-contained chord part: a progression at a chosen harmonic rhythm.
5057
5058		The one-call form of ``p.progression()`` — it registers a pattern on
5059		*channel* that plays *progression* across *bars* (or *beats*), each chord
5060		lasting a length drawn from *harmonic_rhythm* (the musical term for how often
5061		the chords change).  It needs no ``composition.harmony()`` call and, with an
5062		explicit chord list or a ``key=``, no composition key either — so a
5063		drums-plus-one-chord-part sketch stays simple.
5064
5065		The progression is realised once, up front, and the same timeline plays every
5066		cycle (a stable phrase).  That timeline is returned so you can see exactly what
5067		was chosen — ``print(comp.chords(...))``.
5068
5069		Parameters:
5070			channel: MIDI channel for the chord part.
5071			progression: A chord-graph style name to generate from, or an explicit list
5072				of chords (``Chord`` objects or names like ``["Cm7", "Dbmaj7"]``).
5073			harmonic_rhythm: How long each chord lasts — a number, a list of lengths,
5074				or ``between(low, high, step=...)``.  See ``p.progression()``.
5075			bars / beats: Length of the part (defaults to 4 beats if neither is given).  ``bars`` uses the
5076				composition's time signature.
5077			voicing: Notes per chord — an int, or a ``(low, high)`` range (e.g. ``(3, 4)``).
5078			velocity: MIDI velocity, or a ``(low, high)`` tuple for per-voice humanisation.
5079			detached: Beats of silence before each next chord (``duration = length - detached``).
5080			root: MIDI root the voicings are centred on (e.g. 48 = C3).
5081			key: Key for a generated progression; defaults to the composition key.
5082			seed: Seed for the (otherwise fixed) realisation; defaults to the
5083				composition seed, so the part is reproducible.
5084			device: Optional output-device override.
5085			mirrors: Optional additional ``(device, channel)`` destinations.
5086
5087		Returns:
5088			The realised :class:`~subsequence.progressions.Progression`.
5089		"""
5090
5091		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5092		resolved_channel = self._resolve_channel(channel)
5093		resolved_key = key if key is not None else self.key
5094
5095		rng = random.Random(seed if seed is not None else self._seed)
5096		timeline = subsequence.progressions.realize(
5097			source = progression,
5098			harmonic_rhythm = harmonic_rhythm,
5099			key = resolved_key,
5100			length = beat_length,
5101			rng = rng,
5102			scale = self.scale or "ionian",
5103		)
5104
5105		captured_root = root
5106		captured_velocity = velocity
5107		captured_detached = detached
5108		captured_voicing = voicing
5109
5110		def chords_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5111
5112			"""Replay the realised timeline as block chords each cycle (voicing per chord)."""
5113
5114			for chord, start, length in timeline:
5115				ring = length - captured_detached if (captured_detached and captured_detached < length) else length
5116				voices = subsequence.progressions.resolve_voices(captured_voicing, p.rng)
5117				p.chord(chord, root=captured_root, beat=start, duration=ring, count=voices, velocity=captured_velocity)
5118
5119		# Unique, stable name so multiple chord parts don't collide in
5120		# _running_patterns — including two parts on the SAME channel, which
5121		# get a deterministic #2/#3 suffix in declaration order.
5122		base_name = f"chords@ch{resolved_channel}"
5123		chords_name = base_name
5124		suffix = 2
5125
5126		while chords_name in self._declared_names:
5127			chords_name = f"{base_name}#{suffix}"
5128			suffix += 1
5129
5130		chords_builder.__name__ = chords_name
5131		self._declared_names.add(chords_name)
5132
5133		primary: typing.Optional[typing.Tuple[int, int]]
5134		if isinstance(device, str):
5135			primary = None
5136		else:
5137			primary = (device if device is not None else 0, resolved_channel)
5138		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5139
5140		if self._is_live and chords_builder.__name__ in self._running_patterns:
5141			running = self._running_patterns[chords_builder.__name__]
5142			running._builder_fn = chords_builder
5143			running._wants_chord = False
5144			logger.info(f"Hot-swapped chords: {chords_builder.__name__}")
5145			return timeline
5146
5147		pending = _PendingPattern(
5148			builder_fn = chords_builder,
5149			channel = resolved_channel,
5150			length = beat_length,
5151			default_grid = default_grid,
5152			drum_note_map = None,
5153			reschedule_lookahead = 1,
5154			voice_leading = False,
5155			mirrors = resolved_mirrors,
5156			device = 0 if (device is None or isinstance(device, str)) else device,
5157			raw_device = device,
5158		)
5159		self._pending_patterns.append(pending)
5160		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:
5162	def phrase_part (
5163		self,
5164		*,
5165		channel: int,
5166		part: typing.Optional[str] = None,
5167		root: int = 60,
5168		bars: typing.Optional[float] = None,
5169		beats: typing.Optional[float] = None,
5170		velocity: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
5171		fit: typing.Optional[float] = None,
5172		device: subsequence.midi_utils.DeviceId = None,
5173		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5174	) -> None:
5175
5176		"""Declare a part that plays each section's bound Motif/Phrase.
5177
5178		The one-call consumer for :meth:`section_motifs` — it registers a
5179		pattern on *channel* that walks whatever value is bound to the
5180		current section for *part* (stateless position from the cycle
5181		counter, via ``p.phrase()``).  A section with no binding for the
5182		part is **silent** for that part — bind material or don't; no
5183		fallback guessing.
5184
5185		Parameters:
5186			channel: MIDI channel for the part.
5187			part: The part label to read from the registry (``None`` = the
5188				unlabelled binding).
5189			root: Register anchor for degree resolution.
5190			bars / beats: Cycle length of the part (defaults to 4 beats);
5191				the phrase is sliced one cycle window at a time.
5192			velocity: Optional override applied to every note.
5193			fit: Passed through (active with the melody engine stage).
5194			device: Optional output-device override.
5195			mirrors: Optional additional ``(device, channel)`` destinations.
5196
5197		Example::
5198
5199			composition.section_motifs("verse",  verse_line,  part="lead")
5200			composition.section_motifs("chorus", chorus_line, part="lead")
5201			composition.phrase_part(channel=4, part="lead", root=72, bars=2)
5202		"""
5203
5204		beat_length, default_grid = self._resolve_length(beats, bars, None, None, beats_per_bar=self.time_signature[0])
5205		resolved_channel = self._resolve_channel(channel)
5206
5207		captured_part = part
5208		captured_root = root
5209		captured_velocity = velocity
5210		captured_fit = fit
5211
5212		def phrase_builder (p: subsequence.pattern_builder.PatternBuilder) -> None:
5213
5214			"""Walk the current section's bound value (silent when unbound)."""
5215
5216			value = p.section_motif(captured_part)
5217
5218			if value is None:
5219				return	# unbound section: silence for this part, by design
5220
5221			p.phrase(value, root=captured_root, velocity=captured_velocity, fit=captured_fit)
5222
5223		# Unique, stable name so multiple phrase parts don't collide —
5224		# including two parts on the SAME channel (deterministic #2/#3
5225		# suffixes in declaration order, the chords() convention).
5226		base_name = f"phrase@{captured_part}@ch{resolved_channel}" if captured_part else f"phrase@ch{resolved_channel}"
5227		phrase_name = base_name
5228		suffix = 2
5229
5230		while phrase_name in self._declared_names:
5231			phrase_name = f"{base_name}#{suffix}"
5232			suffix += 1
5233
5234		phrase_builder.__name__ = phrase_name
5235		self._declared_names.add(phrase_name)
5236
5237		primary: typing.Optional[typing.Tuple[int, int]]
5238		if isinstance(device, str):
5239			primary = None
5240		else:
5241			primary = (device if device is not None else 0, resolved_channel)
5242		resolved_mirrors = self._resolve_mirrors(mirrors, primary=primary)
5243
5244		if self._is_live and phrase_builder.__name__ in self._running_patterns:
5245			running = self._running_patterns[phrase_builder.__name__]
5246			running._builder_fn = phrase_builder
5247			running._wants_chord = False
5248			logger.info(f"Hot-swapped phrase part: {phrase_builder.__name__}")
5249			return
5250
5251		pending = _PendingPattern(
5252			builder_fn = phrase_builder,
5253			channel = resolved_channel,
5254			length = beat_length,
5255			default_grid = default_grid,
5256			drum_note_map = None,
5257			reschedule_lookahead = 1,
5258			voice_leading = False,
5259			mirrors = resolved_mirrors,
5260			device = 0 if (device is None or isinstance(device, str)) else device,
5261			raw_device = device,
5262		)
5263		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:
5265	def trigger (
5266		self,
5267		fn: typing.Callable,
5268		channel: int,
5269		beats: typing.Optional[float] = None,
5270		bars: typing.Optional[float] = None,
5271		steps: typing.Optional[float] = None,
5272		step_duration: typing.Optional[float] = None,
5273		quantize: float = 0,
5274		drum_note_map: typing.Optional[typing.Dict[str, int]] = None,
5275		cc_name_map: typing.Optional[typing.Dict[str, int]] = None,
5276		nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None,
5277		chord: bool = False,
5278		device: subsequence.midi_utils.DeviceId = None,
5279		mirrors: typing.Optional[typing.Iterable[subsequence.pattern.MirrorSpec]] = None,
5280	) -> None:
5281
5282		"""
5283		Trigger a one-shot pattern immediately or on a quantized boundary.
5284
5285		This is useful for real-time response to sensors, OSC messages, or other
5286		external events. The builder function is called immediately with a fresh
5287		PatternBuilder, and the generated events are injected into the queue at
5288		the specified quantize boundary.
5289
5290		The builder function has the same API as a ``@composition.pattern``
5291		decorated function and can use all PatternBuilder methods: ``p.note()``,
5292		``p.euclidean()``, ``p.arpeggio()``, and so on.
5293
5294		See ``pattern()`` for the full description of ``beats``, ``bars``,
5295		``steps``, and ``step_duration``. Default is 1 beat.
5296
5297		Parameters:
5298			fn: The pattern builder function (same signature as ``@comp.pattern``).
5299			channel: MIDI channel (1-16, or 0-15 with ``zero_indexed_channels=True``).
5300			beats: Duration in beats (quarter notes, default 1).
5301			bars: Duration in bars (uses the composition's time signature — 4 beats each in 4/4).
5302			steps: Step count for step mode. Requires ``step_duration=``.
5303			step_duration: Duration of one step in beats. Requires ``steps=``.
5304			quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default),
5305				``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*``
5306				constants from ``subsequence.constants.durations``.
5307			drum_note_map: Optional drum name mapping for this pattern.
5308			cc_name_map: Optional mapping of CC names to MIDI CC numbers.
5309			nrpn_name_map: Optional mapping of NRPN parameter names to
5310				14-bit parameter numbers.
5311			chord: If ``True``, the builder function receives the current chord as
5312				a second parameter (same as ``@composition.pattern``).
5313			mirrors: Optional list of additional ``(device, channel)`` destinations
5314				to fire this one-shot onto in parallel with the primary destination.
5315
5316		Example:
5317			```python
5318			# Immediate single note (channels are 1-16 by default)
5319			composition.trigger(
5320				lambda p: p.note(60, beat=0, velocity=100, duration=0.5),
5321				channel=1
5322			)
5323
5324			# Quantized fill (next bar) — channel 10 is the GM drum channel
5325			import subsequence.constants.durations as dur
5326			composition.trigger(
5327				lambda p: p.euclidean("snare", pulses=7, velocity=90),
5328				channel=10,
5329				drum_note_map=gm_drums.GM_DRUM_MAP,
5330				quantize=dur.WHOLE
5331			)
5332
5333			# With chord context — the builder receives the chord as a second
5334			# argument when chord=True.
5335			composition.trigger(
5336				lambda p, chord: p.arpeggio(chord.tones(root=60), spacing=dur.SIXTEENTH),
5337				channel=1,
5338				quantize=dur.QUARTER,
5339				chord=True
5340			)
5341			```
5342		"""
5343
5344		# Resolve channel numbering
5345		resolved_channel = self._resolve_channel(channel)
5346
5347		beat_length, default_grid = self._resolve_length(beats, bars, steps, step_duration, default=1.0, beats_per_bar=self.time_signature[0])
5348
5349		# Resolve device index — for trigger() this is always concrete by call time,
5350		# so the mirror-to-self check has the full primary tuple available.
5351		resolved_device_idx = self._resolve_device_id(device)
5352		resolved_mirrors = self._resolve_mirrors(mirrors, primary=(resolved_device_idx, resolved_channel))
5353
5354		# Create a temporary Pattern
5355		pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx, mirrors=resolved_mirrors)
5356
5357		# Resolve the section context once: the one-shot inherits the section's
5358		# effective key/scale (so a triggered degree resolves like everywhere
5359		# else) and a harmony view at the current playhead (so ChordTone /
5360		# Approach resolve too).
5361		trigger_section = self._form_state.get_section_info() if self._form_state else None
5362		trigger_key, trigger_scale = self._effective_key_scale(trigger_section)
5363
5364		trigger_harmony: typing.Optional[HarmonyView] = None
5365		if not self._harmony_horizon.is_empty:
5366			trigger_harmony = HarmonyView(self._harmony_horizon, self._sequencer.pulse_count / self._sequencer.pulses_per_beat)
5367
5368		# Create a PatternBuilder
5369		builder = subsequence.pattern_builder.PatternBuilder(
5370			pattern=pattern,
5371			cycle=0,  # One-shot patterns don't rebuild, so cycle is always 0
5372			drum_note_map=drum_note_map,
5373			cc_name_map=cc_name_map,
5374			nrpn_name_map=nrpn_name_map,
5375			section=trigger_section,
5376			bar=self._builder_bar,
5377			conductor=self.conductor,
5378			rng=random.Random(),  # Fresh random state for each trigger
5379			tweaks={},
5380			default_grid=default_grid,
5381			data=self.data,
5382			# A one-shot resolves key-relative content against the same
5383			# effective key/scale as the section it fires into (previously
5384			# omitted entirely — degrees raised even in a keyed composition).
5385			key=trigger_key,
5386			scale=trigger_scale,
5387			time_signature=self.time_signature,
5388			held_notes=self._sequencer._held_notes,
5389			harmony=trigger_harmony,
5390			energy=self._current_energy(trigger_section)
5391		)
5392
5393		# Call the builder function
5394		try:
5395
5396			current_chord = self.current_chord() if chord else None
5397
5398			if current_chord is not None:
5399				injected = _InjectedChord(current_chord, None)  # No voice leading for one-shots
5400				fn(builder, injected)
5401
5402			else:
5403				fn(builder)
5404
5405		except Exception:
5406			logger.exception("Error in trigger builder — pattern will be silent")
5407			return
5408
5409		# Calculate the start pulse based on quantize
5410		current_pulse = self._sequencer.pulse_count
5411		pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE
5412
5413		if quantize == 0:
5414			# Immediate: use current pulse
5415			start_pulse = current_pulse
5416
5417		else:
5418			# Quantize to the next multiple of (quantize * pulses_per_beat)
5419			quantize_pulses = int(quantize * pulses_per_beat)
5420			start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses
5421
5422		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
5443	@property
5444	def is_clock_following (self) -> bool:
5445
5446		"""True if either the primary or any additional device is following external clock."""
5447
5448		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:
5451	def play (self) -> None:
5452
5453		"""
5454		Start the composition.
5455
5456		This call blocks until the program is interrupted (e.g., via Ctrl+C).
5457		It initializes the MIDI hardware, launches the background sequencer,
5458		and begins playback.
5459		"""
5460
5461		try:
5462			asyncio.run(self._run())
5463
5464		except KeyboardInterrupt:
5465			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:
5468	def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None:
5469
5470		"""Render the composition to a MIDI file without real-time playback.
5471
5472		Runs the sequencer as fast as possible (no timing delays) and stops
5473		when the first active limit is reached.  The result is saved as a
5474		standard MIDI file that can be imported into any DAW.
5475
5476		All patterns, scheduled callbacks, and harmony logic run exactly as
5477		they would during live playback — BPM transitions, generative fills,
5478		and probabilistic gates all work in render mode.  The only difference
5479		is that time is simulated rather than wall-clock driven.
5480
5481		Parameters:
5482			bars: Number of bars to render, or ``None`` for no bar limit
5483			      (default ``None``).  When both *bars* and *max_minutes* are
5484			      active, playback stops at whichever limit is reached first.
5485			filename: Output MIDI filename (default ``"render.mid"``).
5486			max_minutes: Safety cap on the length of rendered MIDI in minutes
5487			             (default ``60.0``).  Pass ``None`` to disable the time
5488			             cap — you must then provide an explicit *bars* value.
5489
5490		Raises:
5491			ValueError: If both *bars* and *max_minutes* are ``None``, which
5492			            would produce an infinite render.
5493
5494		Examples:
5495			```python
5496			# Default: renders up to 60 minutes of MIDI content.
5497			composition.render()
5498
5499			# Render exactly 64 bars (time cap still active as backstop).
5500			composition.render(bars=64, filename="demo.mid")
5501
5502			# Render up to 5 minutes of an infinite generative composition.
5503			composition.render(max_minutes=5, filename="five_min.mid")
5504
5505			# Remove the time cap — must supply bars instead.
5506			composition.render(bars=128, max_minutes=None, filename="long.mid")
5507			```
5508		"""
5509
5510		if bars is None and max_minutes is None:
5511			raise ValueError(
5512				"render() requires at least one limit: provide bars=, max_minutes=, or both. "
5513				"Passing both as None would produce an infinite render."
5514			)
5515
5516		self._sequencer.recording = True
5517		self._sequencer.record_filename = filename
5518		self._sequencer.render_mode = True
5519		self._sequencer.render_bars = bars if bars is not None else 0
5520		self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None
5521		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:
 404@dataclasses.dataclass(frozen=True)
 405class Motif:
 406
 407	"""
 408	An immutable musical figure: timed note events + control gestures + a length in beats.
 409
 410	Construct via the classmethods (:meth:`degrees`, :meth:`notes`,
 411	:meth:`hits`, :meth:`steps`, :meth:`euclidean`, the control-gesture
 412	constructors, or :meth:`from_events`) rather than positionally.
 413	``length`` is explicit — a trailing rest is meaningful.
 414	"""
 415
 416	events: typing.Tuple[MotifEvent, ...]
 417	length: float
 418	controls: typing.Tuple[ControlEvent, ...] = ()
 419	fit: typing.Optional[float] = None		# placement default for the fit dial; set by generate()
 420
 421	def __post_init__ (self) -> None:
 422
 423		"""Validate, and normalise both streams to canonical order."""
 424
 425		if self.length < 0:
 426			raise ValueError(f"Motif length must be non-negative — got {self.length}")
 427
 428		object.__setattr__(self, "events", tuple(sorted(self.events, key=MotifEvent._sort_key)))
 429		object.__setattr__(self, "controls", tuple(sorted(self.controls, key=ControlEvent._sort_key)))
 430
 431	# ── constructors ────────────────────────────────────────────────────
 432
 433	@classmethod
 434	def empty (cls) -> "Motif":
 435
 436		"""The empty motif (zero events, zero length) — the identity for ``then``."""
 437
 438		return cls(events=(), length=0.0)
 439
 440	@classmethod
 441	def from_events (
 442		cls,
 443		events: typing.Iterable[MotifEvent],
 444		length: typing.Optional[float] = None,
 445		controls: typing.Iterable[ControlEvent] = (),
 446	) -> "Motif":
 447
 448		"""Build a motif from explicit events (power use; length defaults to the next whole beat)."""
 449
 450		events = tuple(events)
 451		controls = tuple(controls)
 452
 453		return cls(
 454			events = events,
 455			length = _computed_length(events, controls) if length is None else length,
 456			controls = controls,
 457		)
 458
 459	@classmethod
 460	def _from_sequence (
 461		cls,
 462		pitches: typing.List[PitchSpec],
 463		beats: typing.Optional[typing.List[float]],
 464		velocities: typing.Any,
 465		durations: typing.Any,
 466		probabilities: typing.Any,
 467		length: typing.Optional[float],
 468	) -> "Motif":
 469
 470		"""Shared core: one event per element, None = rest (slot still advances)."""
 471
 472		n = len(pitches)
 473		onsets = list(beats) if beats is not None else [float(i) for i in range(n)]
 474
 475		if len(onsets) != n:
 476			raise ValueError(f"beats has {len(onsets)} onsets for {n} elements — parallel lists must match")
 477
 478		velocity_list = _expand("velocities", velocities, n)
 479		duration_list = _expand("durations", durations, n)
 480		probability_list = _expand("probabilities", probabilities, n)
 481
 482		events = tuple(
 483			MotifEvent(
 484				beat = float(onsets[i]),
 485				pitch = pitches[i],
 486				velocity = velocity_list[i],
 487				duration = float(duration_list[i]),
 488				probability = float(probability_list[i]),
 489			)
 490			for i in range(n)
 491			if pitches[i] is not None
 492		)
 493
 494		return cls(
 495			events = events,
 496			length = _computed_length(events, ()) if length is None else float(length),
 497		)
 498
 499	@classmethod
 500	def degrees (
 501		cls,
 502		degrees: typing.List[typing.Union[int, Degree, None]],
 503		beats: typing.Optional[typing.List[float]] = None,
 504		velocities: typing.Any = _DEFAULT_VELOCITY,
 505		durations: typing.Any = 1.0,
 506		probabilities: typing.Any = 1.0,
 507		length: typing.Optional[float] = None,
 508	) -> "Motif":
 509
 510		"""
 511		A melody written as 1-based scale degrees, one per beat by default.
 512
 513		Elements are ints (1 = tonic, 8 = tonic an octave up), ``None`` for a
 514		rest (the beat slot still advances), or :class:`Degree` for octave/
 515		chromatic detail.  Resolved against key + scale at placement.
 516		Durations default to a full beat (each note holds its slot).
 517		"""
 518
 519		converted: typing.List[PitchSpec] = []
 520
 521		for element in degrees:
 522			if isinstance(element, int):
 523				if element > _MAX_PLAUSIBLE_DEGREE:
 524					raise ValueError(
 525						f"Degree {element} is implausibly large — scale degrees are 1-based "
 526						f"(8 = tonic an octave up). For MIDI note numbers use Motif.notes()."
 527					)
 528				converted.append(Degree(element))
 529			elif isinstance(element, Degree) or element is None:
 530				converted.append(element)
 531			else:
 532				raise TypeError(f"Motif.degrees takes ints, Degree, or None — got {type(element).__name__}")
 533
 534		return cls._from_sequence(converted, beats, velocities, durations, probabilities, length)
 535
 536	@classmethod
 537	def notes (
 538		cls,
 539		notes: typing.List[typing.Union[int, None]],
 540		beats: typing.Optional[typing.List[float]] = None,
 541		velocities: typing.Any = _DEFAULT_VELOCITY,
 542		durations: typing.Any = 1.0,
 543		probabilities: typing.Any = 1.0,
 544		length: typing.Optional[float] = None,
 545	) -> "Motif":
 546
 547		"""A melody written as absolute MIDI note numbers (60 = middle C); ``None`` = rest."""
 548
 549		for element in notes:
 550			# bool is a subclass of int, but True/False are never MIDI notes.
 551			if isinstance(element, bool) or not (isinstance(element, int) or element is None):
 552				raise TypeError(f"Motif.notes takes MIDI ints or None — got {type(element).__name__}")
 553
 554		return cls._from_sequence(list(notes), beats, velocities, durations, probabilities, length)
 555
 556	@classmethod
 557	def hits (
 558		cls,
 559		pitch: typing.Union[int, str],
 560		beats: typing.List[float],
 561		length: typing.Optional[float] = None,
 562		velocities: typing.Any = _DEFAULT_VELOCITY,
 563		durations: typing.Any = 0.1,
 564		probabilities: typing.Any = 1.0,
 565	) -> "Motif":
 566
 567		"""One pitch (usually a drum name) at a list of beat positions — the ``hit()`` convention."""
 568
 569		return cls._from_sequence([pitch] * len(beats), list(beats), velocities, durations, probabilities, length)
 570
 571	@classmethod
 572	def steps (
 573		cls,
 574		steps: typing.List[int],
 575		pitches: typing.Any,
 576		velocities: typing.Any = _DEFAULT_VELOCITY,
 577		durations: typing.Any = 0.1,
 578		probabilities: typing.Any = 1.0,
 579		step_duration: float = 0.25,
 580		length: typing.Optional[float] = None,
 581	) -> "Motif":
 582
 583		"""
 584		Grid placement — the ``sequence()`` convention: ``steps`` are 0-based
 585		grid indices (sixteenths by default), ``pitches`` a scalar or
 586		parallel list of MIDI ints or drum names.
 587		"""
 588
 589		n = len(steps)
 590		pitch_list = _expand("pitches", pitches, n)
 591		onsets = [s * step_duration for s in steps]
 592
 593		if length is None and n:
 594			length = float(math.ceil((max(steps) + 1) * step_duration))
 595
 596		return cls._from_sequence(pitch_list, onsets, velocities, durations, probabilities, length)
 597
 598	@classmethod
 599	def euclidean (
 600		cls,
 601		pulses: int,
 602		steps: int,
 603		pitch: typing.Union[int, str],
 604		length: float = 4.0,
 605		velocities: typing.Any = _DEFAULT_VELOCITY,
 606		durations: typing.Any = 0.1,
 607		probabilities: typing.Any = 1.0,
 608	) -> "Motif":
 609
 610		"""A euclidean rhythm as a value: *pulses* spread evenly across *steps* over *length* beats."""
 611
 612		# bool is a subclass of int, but True/False are never MIDI notes.
 613		if isinstance(pitch, bool):
 614			raise TypeError(f"Motif.euclidean takes a MIDI int or drum name for pitch — got {pitch!r}")
 615
 616		# The kernel returns one 0/1 flag per grid step; onsets are the 1s.
 617		# It validates pulses first, so pulses > steps still raises clearly.
 618		flags = subsequence.sequence_utils.generate_euclidean_sequence(steps=steps, pulses=pulses)
 619
 620		if steps <= 0:
 621			# A grid of zero steps holds no onsets — an empty motif of the given
 622			# length, matching pulses=0 on a real grid (the empty-input policy).
 623			return cls._from_sequence([], [], velocities, durations, probabilities, length)
 624
 625		step_duration = length / steps
 626		onsets = [i * step_duration for i, flag in enumerate(flags) if flag]
 627
 628		return cls._from_sequence(
 629			[pitch] * len(onsets),
 630			onsets,
 631			velocities, durations, probabilities, length,
 632		)
 633
 634	@classmethod
 635	def preset (
 636		cls,
 637		name: str,
 638		pitch: typing.Optional[typing.Union[int, str]] = None,
 639		length: float = 4.0,
 640		velocities: typing.Any = _DEFAULT_VELOCITY,
 641		durations: typing.Any = 0.1,
 642		probabilities: typing.Any = 1.0,
 643	) -> "Motif":
 644
 645		"""A named world-rhythm timeline as a value — ``Motif.preset("son_clave_3_2")``.
 646
 647		Looks a curated timeline up in the world-rhythm table (clave family,
 648		West-African bell patterns, tresillo/cinquillo, samba) and lays its
 649		onsets across *length* beats.  Onset positions are exact pulse indices
 650		from Toussaint's "The Geometry of Musical Rhythm"; each preset declares
 651		its own grid (16 for the clave/4-4 timelines, 12 for the bell
 652		patterns) and a default drum voice.
 653
 654		Parameters:
 655			name: A preset name (``KeyError``-style ValueError lists them all).
 656			pitch: The voice — a drum name or MIDI int; defaults to the
 657				preset's General-MIDI voice (``"claves"``, ``"cowbell"``,
 658				``"side_stick"``, ``"low_conga"``), so it sounds against the
 659				standard GM drum map without a ``pitch=``.
 660			length: Total beats the cycle spans (4 = one common-time bar).
 661			velocities / durations / probabilities: The parallel-list params.
 662
 663		Returns:
 664			A drum/pitched :class:`Motif` of the timeline's onsets.
 665
 666		Raises:
 667			ValueError: If *name* is not a known preset.
 668
 669		Example:
 670			```python
 671			clave = subsequence.Motif.preset("son_clave_3_2")              # GM "claves"
 672			bell  = subsequence.Motif.preset("bembe", pitch="cowbell")     # 12-pulse
 673			```
 674		"""
 675
 676		if name not in _WORLD_RHYTHMS:
 677			known = ", ".join(sorted(_WORLD_RHYTHMS))
 678			raise ValueError(f"Unknown rhythm preset {name!r}. Known presets: {known}.")
 679
 680		steps, grid, voice = _WORLD_RHYTHMS[name]
 681
 682		return cls.steps(
 683			steps = list(steps),
 684			pitches = pitch if pitch is not None else voice,
 685			velocities = velocities,
 686			durations = durations,
 687			probabilities = probabilities,
 688			step_duration = length / grid,
 689			length = length,
 690		)
 691
 692	# ── control-gesture constructors (mirror the pattern_midi verbs) ────
 693
 694	@classmethod
 695	def _control_writes (
 696		cls,
 697		signal: ControlSignal,
 698		values: typing.List[float],
 699		beats: typing.List[float],
 700		length: typing.Optional[float],
 701		probabilities: typing.Any = 1.0,
 702	) -> "Motif":
 703
 704		"""Shared core for discrete control writes."""
 705
 706		if len(values) != len(beats):
 707			raise ValueError(f"values has {len(values)} entries for {len(beats)} beats — parallel lists must match")
 708
 709		probability_list = _expand("probabilities", probabilities, len(values))
 710
 711		controls = tuple(
 712			ControlEvent(beat=float(beats[i]), signal=signal, start=float(values[i]), probability=float(probability_list[i]))
 713			for i in range(len(values))
 714		)
 715
 716		return cls(
 717			events = (),
 718			length = _computed_length((), controls) if length is None else float(length),
 719			controls = controls,
 720		)
 721
 722	@classmethod
 723	def _control_ramp (
 724		cls,
 725		signal: ControlSignal,
 726		start: float,
 727		end: float,
 728		beat_start: float,
 729		beat_end: typing.Optional[float],
 730		shape: typing.Union["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"],
 731		length: typing.Optional[float],
 732		probability: float = 1.0,
 733	) -> "Motif":
 734
 735		"""Shared core for shaped control ramps."""
 736
 737		if beat_end is None:
 738			if length is None:
 739				raise ValueError("A ramp needs beat_end= (or length=, which beat_end defaults to)")
 740			beat_end = float(length)
 741
 742		if beat_end <= beat_start:
 743			raise ValueError(f"beat_end ({beat_end}) must be after beat_start ({beat_start})")
 744
 745		controls = (
 746			ControlEvent(
 747				beat = float(beat_start),
 748				signal = signal,
 749				start = float(start),
 750				end = float(end),
 751				span = float(beat_end) - float(beat_start),
 752				shape = shape,
 753				probability = probability,
 754			),
 755		)
 756
 757		return cls(
 758			events = (),
 759			length = float(math.ceil(beat_end)) if length is None else float(length),
 760			controls = controls,
 761		)
 762
 763	@classmethod
 764	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":
 765
 766		"""Discrete CC writes at beat positions — mirrors ``p.cc()``; names resolve at placement."""
 767
 768		return cls._control_writes(CC(control), list(values), list(beats), length, probabilities)
 769
 770	@classmethod
 771	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["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 772
 773		"""A CC value swept ``start`` → ``end`` over a beat range — mirrors ``p.cc_ramp()``."""
 774
 775		return cls._control_ramp(CC(control), start, end, beat_start, beat_end, shape, length, probability)
 776
 777	@classmethod
 778	def pitch_bend (cls, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
 779
 780		"""Discrete pitch-bend writes (-1.0 to 1.0) at beat positions — mirrors ``p.pitch_bend()``."""
 781
 782		return cls._control_writes(PitchBend(), list(values), list(beats), length, probabilities)
 783
 784	@classmethod
 785	def pitch_bend_ramp (cls, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 786
 787		"""Pitch bend swept ``start`` → ``end`` (-1.0 to 1.0) over a beat range — mirrors ``p.pitch_bend_ramp()``."""
 788
 789		return cls._control_ramp(PitchBend(), start, end, beat_start, beat_end, shape, length, probability)
 790
 791	@classmethod
 792	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":
 793
 794		"""Discrete NRPN parameter writes at beat positions — mirrors ``p.nrpn()``."""
 795
 796		return cls._control_writes(NRPN(parameter, fine=fine, null_reset=null_reset), list(values), list(beats), length, probabilities)
 797
 798	@classmethod
 799	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["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 800
 801		"""An NRPN value swept over a beat range — mirrors ``p.nrpn_ramp()``."""
 802
 803		return cls._control_ramp(NRPN(parameter, fine=fine, null_reset=null_reset), start, end, beat_start, beat_end, shape, length, probability)
 804
 805	@classmethod
 806	def rpn (cls, parameter: typing.Union[int, "subsequence.declarations.RpnParameter"], 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":
 807
 808		"""Discrete RPN parameter writes at beat positions — mirrors ``p.rpn()``."""
 809
 810		return cls._control_writes(RPN(parameter, fine=fine, null_reset=null_reset), list(values), list(beats), length, probabilities)
 811
 812	@classmethod
 813	def rpn_ramp (cls, parameter: typing.Union[int, "subsequence.declarations.RpnParameter"], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 814
 815		"""An RPN value swept over a beat range — mirrors ``p.rpn_ramp()``."""
 816
 817		return cls._control_ramp(RPN(parameter, fine=fine, null_reset=null_reset), start, end, beat_start, beat_end, shape, length, probability)
 818
 819	@classmethod
 820	def osc (cls, address: str, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
 821
 822		"""Discrete OSC float sends at beat positions — mirrors ``p.osc()``."""
 823
 824		return cls._control_writes(OSC(address), list(values), list(beats), length, probabilities)
 825
 826	@classmethod
 827	def osc_ramp (cls, address: str, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
 828
 829		"""An OSC float swept over a beat range — mirrors ``p.osc_ramp()``."""
 830
 831		return cls._control_ramp(OSC(address), start, end, beat_start, beat_end, shape, length, probability)
 832
 833	# ── the algebra ─────────────────────────────────────────────────────
 834
 835	def then (self, other: "Motif") -> "Motif":
 836
 837		"""Closed sequential concat: glue *other* after this motif into ONE longer motif."""
 838
 839		if not isinstance(other, Motif):
 840			raise TypeError(f"then() takes a Motif — got {type(other).__name__}")
 841
 842		return Motif(
 843			events = self.events + tuple(dataclasses.replace(e, beat=e.beat + self.length) for e in other.events),
 844			length = self.length + other.length,
 845			controls = self.controls + tuple(dataclasses.replace(c, beat=c.beat + self.length) for c in other.controls),
 846			# fit is a dial, not content: keep ours, inherit the other's when
 847			# we have none — join()/tiling folds from empty() (fit=None), and
 848			# must not silently strip a generated motif's chord-snapping.
 849			fit = self.fit if self.fit is not None else other.fit,
 850		)
 851
 852	@classmethod
 853	def join (cls, motifs: typing.Iterable["Motif"]) -> "Motif":
 854
 855		"""Fold a list of motifs into one with ``then`` (empty list → ``Motif.empty()``)."""
 856
 857		result = cls.empty()
 858
 859		for m in motifs:
 860			result = result.then(m)
 861
 862		return result
 863
 864	@classmethod
 865	def generate (
 866		cls,
 867		rhythm: typing.Any,
 868		length: typing.Optional[float] = None,
 869		scale: typing.Optional[typing.Union[str, typing.Sequence[int]]] = None,
 870		contour: typing.Optional[str] = None,
 871		end_on: typing.Optional[typing.Union[int, Degree]] = None,
 872		cadence: typing.Optional[str] = None,
 873		pins: typing.Optional[typing.Dict[int, typing.Union[int, Degree]]] = None,
 874		max_pitches: typing.Optional[int] = None,
 875		velocities: typing.Any = _DEFAULT_VELOCITY,
 876		durations: typing.Any = 0.25,
 877		seed: typing.Optional[int] = None,
 878		rng: typing.Optional[random.Random] = None,
 879		state: typing.Optional[typing.Any] = None,
 880		nir_strength: float = 0.5,
 881		pitch_diversity: float = 0.6,
 882		tessitura_strength: float = 0.6,
 883	) -> "Motif":
 884
 885		"""Generate a melodic motif — rhythm first, pitches walked, a value out.
 886
 887		The melody engine emitting a value: you give the **rhythm** (an onset
 888		list in beats, or another motif whose rhythm to borrow — cross-pattern
 889		rhythm reuse is shared values); the engine walks pitches over it
 890		through the soft scoring factors (NIR expectation, contour envelope,
 891		tessitura regression, diversity), honouring any pins.
 892
 893		The result emits **scale degrees** (resolved at placement against the
 894		composition key/scale), so a generated hook transposes, varies, and
 895		develops like a hand-written one.  ``scale=`` constrains *candidate
 896		choice only*: a name or interval list masks which pitches the walk
 897		may use, spelled relative to its best-fit reference (major or minor)
 898		— bind it in a composition whose scale matches that family and
 899		resolution is exact.  An explicit MIDI pitch pool (a list of note
 900		numbers) switches to absolute output (the sieve/atonal path).
 901
 902		Parameters:
 903			rhythm: Onset beats (``[0, 1, 1.5, 1.75, 2.5]``) or a Motif
 904				(its onsets are borrowed).
 905			length: Motif length in beats; defaults to the onsets rounded
 906				up to a whole 4-beat bar.
 907			scale: A scale name, an interval list, or an explicit MIDI
 908				pitch pool.  ``None`` = the plain seven degrees.
 909			contour: Envelope shaping the line's height over its span —
 910				``"arch"``, ``"valley"``, ``"ascending"``, ``"descending"``.
 911			end_on: Degree the line must end on — sugar for ``pins={-1: ...}``.
 912				Degree semantics: raises with an explicit MIDI pool (pin the
 913				exact note instead).
 914			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
 915				``"fakeout"``) — the line closes on that cadence's melodic
 916				degree (1 for the full closes and the fakeout, 5 for the
 917				open half).  Sugar for ``end_on=``; conflicts with it, and
 918				raises with an explicit MIDI pool like ``end_on=``.
 919			pins: ``{position: degree}`` — 1-based note positions (``-1`` =
 920				the last, the Python idiom); the engine fills between.  With
 921				an explicit MIDI pool there are no degrees to read, so each
 922				pin is the exact MIDI note to play (``Degree`` pins raise).
 923			max_pitches: Cap on distinct pitches (a tight pool is a hook);
 924				keeps the most central candidates.
 925			velocities / durations: Scalar or per-note list (the parallel-
 926				list convention).
 927			seed: Seed for the walk (required or warned — module-level
 928				nondeterminism breaks live reload).
 929			rng: Explicit stream (overrides ``seed``).
 930			state: A ``MelodicState`` whose dials, scoring factors, and
 931				melodic history seed the walk.  It is **copied** — building
 932				a value never mutates a module-level live object.  The
 933				candidate pool is not carried over: it is always rebuilt
 934				from ``scale=`` (pass an explicit pool there instead),
 935				though the state's key still sets the tonic that the NIR
 936				closure rule lands on.
 937			nir_strength / pitch_diversity / tessitura_strength: The walk's
 938				dials when no ``state`` is given.
 939
 940		Example:
 941			```python
 942			hook = subsequence.Motif.generate(
 943				rhythm=[0, 1, 1.5, 1.75, 2.5], scale="minor_pentatonic",
 944				contour="arch", end_on=1, seed=7,
 945			)
 946			```
 947		"""
 948
 949		import subsequence.melodic_state
 950
 951		onsets = list(rhythm.onsets()) if hasattr(rhythm, "onsets") else [float(b) for b in rhythm]
 952
 953		if cadence is not None:
 954			if end_on is not None:
 955				raise ValueError("cadence= already names the close degree — it conflicts with end_on=")
 956			end_on = subsequence.cadences.cadence_formula(cadence).close_degree
 957
 958		if not onsets:
 959			raise ValueError("generate() needs at least one onset — the rhythm comes first")
 960		if sorted(onsets) != onsets:
 961			raise ValueError("rhythm onsets must ascend")
 962
 963		if length is None:
 964			length = max(4.0, math.ceil((onsets[-1] + 1e-9) / 4.0) * 4.0)
 965		if onsets[-1] >= length:
 966			raise ValueError(f"the last onset ({onsets[-1]:g}) falls outside length={length:g}")
 967
 968		if rng is None:
 969			if seed is None:
 970				warnings.warn(
 971					"generate() without seed= is nondeterministic — pass seed= so the "
 972					"value survives live reload",
 973					stacklevel = 2,
 974				)
 975				rng = random.Random()
 976			else:
 977				rng = random.Random(seed)
 978
 979		# --- The candidate pool ------------------------------------------------
 980		absolute_pool: typing.Optional[typing.List[int]] = None
 981		intervals: typing.List[int]
 982
 983		if scale is None:
 984			intervals = list(subsequence.intervals.scale_pitch_classes(0, "ionian"))
 985		elif isinstance(scale, str):
 986			intervals = list(subsequence.intervals.scale_pitch_classes(0, scale))
 987		else:
 988			values = [int(v) for v in scale]
 989			if values and (min(values) != 0 or max(values) > 11):
 990				absolute_pool = sorted(values)		# an explicit MIDI pool: absolute output
 991				intervals = []
 992			else:
 993				intervals = sorted(set(values))
 994
 995		# Best-fit reference scale for degree spelling: whichever of major/
 996		# minor contains more of the pool (ties to major).  Bound under a
 997		# matching composition scale, resolution is exact.
 998		if absolute_pool is None:
 999			ionian = set(subsequence.intervals.scale_pitch_classes(0, "ionian"))
1000			aeolian = set(subsequence.intervals.scale_pitch_classes(0, "minor"))
1001			reference_name = "minor" if sum(i in aeolian for i in intervals) > sum(i in ionian for i in intervals) else "ionian"
1002			reference = list(subsequence.intervals.scale_pitch_classes(0, reference_name))
1003
1004		# --- The walking state (copied, never mutated in place) ----------------
1005		if state is not None:
1006			walker = state.clone()
1007			walker.rest_probability = 0.0		# generate is rhythm-first: every onset gets a
1008												# note, so the walker never rests (and never falls
1009												# back to a stuck repeat) — rests come from the rhythm
1010		else:
1011			walker = subsequence.melodic_state.MelodicState(
1012				nir_strength = nir_strength,
1013				pitch_diversity = pitch_diversity,
1014				tessitura_strength = tessitura_strength,
1015				chord_weight = 0.0,		# values have no chord context; fit applies at placement
1016			)
1017
1018		if absolute_pool is not None:
1019			walker.set_pool(absolute_pool)
1020		else:
1021			# Offsets over ~1.5 octaves anchored at 60 — register is decided
1022			# at placement (root=), so the anchor is arbitrary and erased.
1023			walker.set_pool([60 + octave * 12 + interval for octave in (0, 1) for interval in intervals if octave * 12 + interval <= 19])
1024
1025		if max_pitches is not None:
1026			if max_pitches < 1:
1027				raise ValueError("max_pitches must be at least 1")
1028			pool = sorted(walker._pitch_pool)
1029			centre = pool[len(pool) // 2]
1030			walker.set_pool(sorted(sorted(pool, key = lambda p: (abs(p - centre), p))[:max_pitches]))
1031
1032		# --- Pins ---------------------------------------------------------------
1033		resolved_pins: typing.Dict[int, int] = {}
1034		combined = dict(pins or {})
1035
1036		# cadence=/end_on= name scale DEGREES — meaningless against an explicit
1037		# MIDI pool, where they would silently land as raw (sub-audio) note
1038		# numbers.
1039		if absolute_pool is not None and end_on is not None:
1040			raise ValueError(
1041				"cadence=/end_on= name scale degrees, but this motif uses an "
1042				"explicit MIDI pool — pin the exact closing note instead: "
1043				"pins={-1: <midi note>}"
1044			)
1045
1046		if end_on is not None:
1047			if -1 in combined or len(onsets) in combined:
1048				raise ValueError("end_on conflicts with a pin on the last note — they name the same position")
1049			combined[-1] = end_on
1050
1051		for pin_position, pin_spec in combined.items():
1052			if not isinstance(pin_position, int) or isinstance(pin_position, bool):
1053				raise ValueError(f"pin positions are 1-based ints (or -1 for last), got {pin_position!r}")
1054			index = pin_position - 1 if pin_position >= 1 else len(onsets) + pin_position
1055			if not 0 <= index < len(onsets):
1056				raise ValueError(f"pin position {pin_position} is outside the {len(onsets)}-note rhythm")
1057			if absolute_pool is not None:
1058				# A raw int pins the exact MIDI note; a Degree has no meaning
1059				# here (the pool defines no scale to read it against).
1060				if not isinstance(pin_spec, int) or isinstance(pin_spec, bool):
1061					raise ValueError(
1062						f"pin {pin_spec!r} is a scale degree, but this motif uses an "
1063						"explicit MIDI pool — pin the exact MIDI note instead "
1064						"(e.g. pins={-1: 52})"
1065					)
1066				resolved_pins[index] = int(pin_spec)
1067			else:
1068				degree = pin_spec if isinstance(pin_spec, Degree) else Degree(int(pin_spec))
1069				step_index = (degree.step - 1) % len(reference)
1070				carry = (degree.step - 1) // len(reference)
1071				resolved_pins[index] = 60 + reference[step_index] + 12 * (carry + degree.octave) + degree.chroma
1072
1073		# --- The walk -----------------------------------------------------------
1074		envelopes: typing.Dict[str, typing.Callable[[float], float]] = {
1075			"arch": lambda pos: 0.15 + 0.8 * math.sin(math.pi * pos),
1076			"valley": lambda pos: 0.95 - 0.8 * math.sin(math.pi * pos),
1077			"ascending": lambda pos: 0.1 + 0.85 * pos,
1078			"descending": lambda pos: 0.95 - 0.85 * pos,
1079		}
1080
1081		if contour is not None and contour not in envelopes:
1082			known = ", ".join(sorted(envelopes))
1083			raise ValueError(f"unknown contour {contour!r} — expected one of: {known}")
1084
1085		chosen_pitches: typing.List[int] = []
1086
1087		for index, onset in enumerate(onsets):
1088
1089			if index in resolved_pins:
1090				pitch = resolved_pins[index]
1091				walker.record(pitch)	# pins enter the NIR context like chosen notes
1092			else:
1093				span_position = index / (len(onsets) - 1) if len(onsets) > 1 else 0.0
1094				target = envelopes[contour](span_position) if contour is not None else None
1095				picked = walker.choose_next(None, rng, beat = onset, position = span_position, contour_target = target)
1096				pitch = picked if picked is not None else walker._pitch_pool[0]
1097
1098			chosen_pitches.append(pitch)
1099
1100		# --- Emission ------------------------------------------------------------
1101		velocity_values = _expand("velocities", velocities, len(onsets))
1102		duration_values = _expand("durations", durations, len(onsets))
1103
1104		events = []
1105
1106		for index, (onset, pitch) in enumerate(zip(onsets, chosen_pitches)):
1107
1108			spec: PitchSpec
1109
1110			if absolute_pool is not None:
1111				spec = pitch
1112			else:
1113				offset = pitch - 60
1114				octave, pc = divmod(offset, 12)
1115				if pc in reference:
1116					spec = Degree(reference.index(pc) + 1, octave = octave)
1117				elif (pc + 1) % 12 in reference and pc + 1 <= 11:
1118					spec = Degree(reference.index(pc + 1) + 1, octave = octave, chroma = -1)
1119				else:
1120					spec = Degree(reference.index(pc - 1) + 1, octave = octave, chroma = 1)
1121
1122			events.append(MotifEvent(
1123				beat = onset,
1124				pitch = spec,
1125				velocity = velocity_values[index],
1126				duration = float(duration_values[index]),
1127			))
1128
1129		return cls(events = tuple(events), length = float(length), fit = 0.7)
1130
1131	def stack (self, other: typing.Union["Motif", "Phrase"]) -> "Motif":
1132
1133		"""
1134		Parallel merge (the spelled form of ``&``): event union, length = max.
1135
1136		No implicit tiling — a short gesture stacked under a long figure
1137		plays once.  Phrase operands flatten first.
1138		"""
1139
1140		if isinstance(other, Phrase):
1141			merged = other.flatten()
1142		elif isinstance(other, Motif):
1143			merged = other
1144		else:
1145			raise TypeError(f"stack() takes a Motif or Phrase — got {type(other).__name__}")
1146
1147		return Motif(
1148			events = self.events + merged.events,
1149			length = max(self.length, merged.length),
1150			controls = self.controls + merged.controls,
1151			fit = self.fit,
1152		)
1153
1154	def slice (self, start: float, end: float) -> "Motif":
1155
1156		"""
1157		A window onto the motif, on its own authority: events starting outside
1158		are dropped; durations and ramp spans truncate at the cut (a truncated
1159		ramp ends at its interpolated cut value).  Beats shift so the window
1160		starts at 0.
1161		"""
1162
1163		if end <= start:
1164			raise ValueError(f"slice end ({end}) must be after start ({start})")
1165
1166		events = tuple(
1167			dataclasses.replace(e, beat=e.beat - start, duration=min(e.duration, end - e.beat))
1168			for e in self.events
1169			if start <= e.beat < end
1170		)
1171
1172		controls = []
1173
1174		for c in self.controls:
1175			if not (start <= c.beat < end):
1176				continue
1177			if c.end is not None and c.beat + c.span > end:
1178				kept = end - c.beat
1179				controls.append(dataclasses.replace(
1180					c, beat=c.beat - start, span=kept, end=c._value_at(kept / c.span),
1181				))
1182			else:
1183				controls.append(dataclasses.replace(c, beat=c.beat - start))
1184
1185		return Motif(events=events, length=end - start, controls=tuple(controls), fit=self.fit)
1186
1187	def __add__ (self, other: typing.Any) -> "Phrase":
1188
1189		"""``a + b`` — sequential: a two-segment Phrase (segmentation preserved)."""
1190
1191		if isinstance(other, Motif):
1192			return Phrase((self, other))
1193
1194		return NotImplemented
1195
1196	def __mul__ (self, count: int) -> typing.Union["Motif", "Phrase"]:
1197
1198		"""``m * n`` — repetition: a Phrase of n segments; ``m * 1`` is ``m``; ``m * 0`` is empty."""
1199
1200		if not isinstance(count, int):
1201			return NotImplemented
1202		if count < 0:
1203			raise ValueError(f"Repetition count must be non-negative — got {count}")
1204		if count == 0:
1205			return Motif.empty()
1206		if count == 1:
1207			return self
1208
1209		return Phrase((self,) * count)
1210
1211	__rmul__ = __mul__
1212
1213	def __and__ (self, other: typing.Any) -> "Motif":
1214
1215		"""``a & b`` — parallel merge; the spelled form is :meth:`stack`."""
1216
1217		if isinstance(other, (Motif, Phrase)):
1218			return self.stack(other)
1219
1220		return NotImplemented
1221
1222	# ── transforms (pure; control gestures ride time, ignore pitch) ─────
1223
1224	def reverse (self) -> "Motif":
1225
1226		"""Mirror the figure in time; ramps swap direction (a rising sweep falls)."""
1227
1228		events = tuple(
1229			dataclasses.replace(e, beat=max(0.0, self.length - e.beat - e.duration))
1230			for e in self.events
1231		)
1232		controls = tuple(
1233			dataclasses.replace(
1234				c,
1235				beat = max(0.0, self.length - c.beat - c.span),
1236				start = c.start if c.end is None else c.end,
1237				end = c.end if c.end is None else c.start,
1238			)
1239			for c in self.controls
1240		)
1241
1242		return Motif(events=events, length=self.length, controls=controls, fit=self.fit)
1243
1244	def rotate (self, beats: float) -> "Motif":
1245
1246		"""Shift every onset by *beats*, wrapping modulo the length (spans ride along)."""
1247
1248		if self.length == 0:
1249			return self
1250
1251		events = tuple(dataclasses.replace(e, beat=(e.beat + beats) % self.length) for e in self.events)
1252		controls = tuple(dataclasses.replace(c, beat=(c.beat + beats) % self.length) for c in self.controls)
1253
1254		return Motif(events=events, length=self.length, controls=controls, fit=self.fit)
1255
1256	def stretch (self, factor: float) -> "Motif":
1257
1258		"""Scale time by *factor* (2.0 = half-time feel): beats, durations, spans, and length."""
1259
1260		if factor <= 0:
1261			raise ValueError(f"Stretch factor must be positive — got {factor}")
1262
1263		events = tuple(
1264			dataclasses.replace(e, beat=e.beat * factor, duration=e.duration * factor)
1265			for e in self.events
1266		)
1267		controls = tuple(
1268			dataclasses.replace(c, beat=c.beat * factor, span=c.span * factor)
1269			for c in self.controls
1270		)
1271
1272		return Motif(events=events, length=self.length * factor, controls=controls, fit=self.fit)
1273
1274	def quantize (self, grid: float) -> "Motif":
1275
1276		"""Snap note onsets to the nearest multiple of *grid* beats (control gestures untouched).
1277
1278		An onset exactly midway between grid lines snaps LATER (round half
1279		up) — every midpoint moves the same way, the predictable behaviour
1280		for a musician.  (Python's own ``round()`` is half-to-even, which
1281		made exact midpoints snap in alternating directions.)
1282		"""
1283
1284		if grid <= 0:
1285			raise ValueError(f"Quantize grid must be positive — got {grid}")
1286
1287		events = tuple(
1288			dataclasses.replace(e, beat=math.floor(e.beat / grid + 0.5) * grid)
1289			for e in self.events
1290		)
1291
1292		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1293
1294	def accent (self, beat: float, amount: int = 20) -> "Motif":
1295
1296		"""Add *amount* velocity to every note at the given beat position (0-based beats)."""
1297
1298		def boost (velocity: subsequence.declarations.VelocityValue) -> subsequence.declarations.VelocityValue:
1299			# Clamp both ends: a negative amount (a de-accent) must not store
1300			# a velocity below 1, which MIDI cannot play.
1301			if isinstance(velocity, (tuple, list)):
1302				return (max(1, min(127, velocity[0] + amount)), max(1, min(127, velocity[1] + amount)))
1303			return max(1, min(127, velocity + amount))
1304
1305		events = tuple(
1306			dataclasses.replace(e, velocity=boost(e.velocity)) if abs(e.beat - beat) < 1e-9 else e
1307			for e in self.events
1308		)
1309
1310		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1311
1312	def with_velocity (self, velocity: subsequence.declarations.VelocityValue) -> "Motif":
1313
1314		"""Replace every note's velocity (an int, or a ``(low, high)`` random range)."""
1315
1316		events = tuple(dataclasses.replace(e, velocity=velocity) for e in self.events)
1317
1318		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1319
1320	def _nudged_pitch (self, pitch: PitchSpec, rng: random.Random, origin: typing.Optional[str]) -> PitchSpec:
1321
1322		"""One varied pitch: a small melodic nudge that always changes the note.
1323
1324		Degrees move by scale steps, MIDI ints by semitones, chord tones by
1325		index; an Approach's target is nudged.  Drum names raise — a varied
1326		drum is a different instrument, not a variation — and so does a
1327		captured drum, which arrives as a number carrying its ``origin``.
1328		"""
1329
1330		_refuse_captured_drum(origin, "vary()", "varied")
1331
1332		if isinstance(pitch, Degree):
1333			steps = [pitch.step + delta for delta in (-2, -1, 1, 2) if pitch.step + delta >= 1]
1334			return dataclasses.replace(pitch, step = rng.choice(steps))
1335		if isinstance(pitch, ChordTone):
1336			indices = [pitch.index + delta for delta in (-1, 1) if pitch.index + delta >= 1]
1337			return ChordTone(rng.choice(indices), octave = pitch.octave)
1338		if isinstance(pitch, Approach):
1339			nudged = self._nudged_pitch(pitch.target, rng, None)	# an Approach is authored, never captured
1340			if not isinstance(nudged, (int, Degree, ChordTone)):
1341				raise TypeError(f"cannot vary an Approach aimed at {type(nudged).__name__} content")
1342			return Approach(nudged)
1343		if isinstance(pitch, int):
1344			return pitch + rng.choice((-2, -1, 1, 2))
1345
1346		raise TypeError(
1347			f"vary() moves pitches — {type(pitch).__name__} content cannot vary "
1348			"(a varied drum is a different instrument)"
1349		)
1350
1351	def vary (
1352		self,
1353		notes: int = 1,
1354		position: str = "end",
1355		seed: typing.Optional[int] = None,
1356		rng: typing.Optional[random.Random] = None,
1357		keep_contour: bool = False,
1358	) -> "Motif":
1359
1360		"""Replace a few pitches, preserving the rhythm — the smallest variation.
1361
1362		Rhythm, velocities, durations, rests, and control gestures are
1363		untouched; only the chosen notes' pitches move (by a small melodic
1364		nudge: scale steps for degrees, semitones for MIDI ints).
1365
1366		Parameters:
1367			notes: How many pitched notes to vary (clamped to what exists).
1368			position: Which notes — ``"end"`` (the tail, the default),
1369				``"start"``, or ``"anywhere"`` (drawn from the stream).
1370			seed: Seed for the variation.  A standalone vary without a seed
1371				warns — module-level nondeterminism breaks live reload.
1372			rng: An explicit random stream (overrides ``seed``; used by
1373				recipe machinery).
1374			keep_contour: When True, the variation preserves the line's
1375				CSEG — every varied note keeps its rank relations with
1376				every other note, so the melodic shape is identical (the
1377				motif-identity guard).  Where no nudge can preserve the
1378				contour, that note stays unchanged — shape wins over
1379				motion.
1380
1381		Example:
1382			```python
1383			answer = call.vary(notes=1, seed=4)     # same figure, new tail note
1384			```
1385		"""
1386
1387		if notes < 0:
1388			raise ValueError(f"notes must be at least 0, got {notes}")
1389		if position not in ("end", "start", "anywhere"):
1390			raise ValueError(f'position must be "end", "start", or "anywhere" — got {position!r}')
1391
1392		if rng is None:
1393			if seed is None:
1394				warnings.warn(
1395					"vary() without seed= is nondeterministic — pass seed= so the "
1396					"value survives live reload",
1397					stacklevel = 2,
1398				)
1399				rng = random.Random()
1400			else:
1401				rng = random.Random(seed)
1402
1403		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1404		count = min(notes, len(pitched_indices))
1405
1406		if count == 0:
1407			return self
1408
1409		if position == "end":
1410			chosen = pitched_indices[-count:]
1411		elif position == "start":
1412			chosen = pitched_indices[:count]
1413		else:
1414			chosen = sorted(rng.sample(pitched_indices, count))
1415
1416		events = list(self.events)
1417
1418		for index in chosen:
1419			if keep_contour:
1420				replacement = self._contour_safe_nudge(events, index, pitched_indices, rng)
1421				if replacement is not None:
1422					events[index] = dataclasses.replace(events[index], pitch = replacement)
1423			else:
1424				events[index] = dataclasses.replace(events[index], pitch = self._nudged_pitch(events[index].pitch, rng, events[index].origin))
1425
1426		return Motif(events = tuple(events), length = self.length, controls = self.controls, fit = self.fit)
1427
1428	@staticmethod
1429	def _rank_value (pitch: PitchSpec) -> float:
1430
1431		"""A comparable height for contour ranking (uniform content only)."""
1432
1433		if isinstance(pitch, Degree):
1434			return pitch.octave * 7 + pitch.step + 0.4 * pitch.chroma
1435		if isinstance(pitch, ChordTone):
1436			return pitch.octave * 4 + pitch.index
1437		if isinstance(pitch, int):
1438			return float(pitch)
1439
1440		raise TypeError(f"keep_contour needs rankable pitches — {type(pitch).__name__} content has no height")
1441
1442	def _contour_safe_nudge (
1443		self,
1444		events: typing.List[MotifEvent],
1445		index: int,
1446		pitched_indices: typing.List[int],
1447		rng: random.Random,
1448	) -> typing.Optional[PitchSpec]:
1449
1450		"""A nudge for events[index] that preserves its CSEG rank relations.
1451
1452		Candidates are the usual small nudges, filtered to those keeping the
1453		note's above/below/equal relation to every other pitched note.  One
1454		rng draw happens regardless (stream stability); ``None`` means no
1455		candidate preserves the shape — leave the note alone.
1456		"""
1457
1458		_refuse_captured_drum(events[index].origin, "vary()", "varied")
1459
1460		pitch = events[index].pitch
1461
1462		if isinstance(pitch, Degree):
1463			candidates: typing.List[PitchSpec] = [
1464				dataclasses.replace(pitch, step = pitch.step + delta)
1465				for delta in (-2, -1, 1, 2) if pitch.step + delta >= 1
1466			]
1467		elif isinstance(pitch, int):
1468			candidates = [pitch + delta for delta in (-2, -1, 1, 2)]
1469		else:
1470			raise TypeError(f"keep_contour cannot vary {type(pitch).__name__} content")
1471
1472		original = self._rank_value(pitch)
1473		others = [
1474			(self._rank_value(events[other].pitch), other)
1475			for other in pitched_indices if other != index
1476		]
1477
1478		def preserves (candidate: PitchSpec) -> bool:
1479			height = self._rank_value(candidate)
1480			for other_height, _ in others:
1481				before = (original > other_height) - (original < other_height)
1482				after = (height > other_height) - (height < other_height)
1483				if before != after:
1484					return False
1485			return True
1486
1487		surviving = [candidate for candidate in candidates if preserves(candidate)]
1488
1489		# One draw either way, so adding keep_contour never shifts the stream
1490		# consumed by the notes around this one.
1491		draw = rng.random()
1492
1493		if not surviving:
1494			return None
1495
1496		return surviving[int(draw * len(surviving)) % len(surviving)]
1497
1498	def answer (self, to: typing.Union[int, Degree] = 1) -> "Motif":
1499
1500		"""Call → response: re-aim the tail to a stable degree.
1501
1502		The classic consequent move — the figure repeats but its last pitched
1503		note lands home (degree 1 by default; pass ``to=5`` for a half-close,
1504		or a full ``Degree`` for register control).  Everything else —
1505		rhythm, the other pitches, velocities, controls — is untouched.
1506
1507		Degree content only: absolute MIDI has no degrees to re-aim (build
1508		the call with ``motif([...])``), and drums raise.
1509		"""
1510
1511		target = to if isinstance(to, Degree) else Degree(int(to))
1512
1513		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1514
1515		if not pitched_indices:
1516			return self
1517
1518		last = self.events[pitched_indices[-1]]
1519
1520		if not isinstance(last.pitch, Degree):
1521			raise TypeError(
1522				f"answer() re-aims scale degrees — the tail is {type(last.pitch).__name__} "
1523				"content (build the call with motif([...]) for degree content)"
1524			)
1525
1526		if isinstance(to, int):
1527			# Keep the call's register: only the step is re-aimed.
1528			target = dataclasses.replace(last.pitch, step = int(to), chroma = 0)
1529
1530		events = list(self.events)
1531		events[pitched_indices[-1]] = dataclasses.replace(last, pitch = target)
1532
1533		return Motif(events = tuple(events), length = self.length, controls = self.controls, fit = self.fit)
1534
1535	def pitched (self, spec: PitchSpec) -> "Motif":
1536
1537		"""
1538		Replace every pitch with one spec — a kick rhythm becomes a bass line.
1539
1540		``"root"`` / ``"third"`` / ``"fifth"`` / ``"seventh"`` become chord
1541		tones; any other string is a drum name; ints are MIDI; Degree /
1542		ChordTone / Approach pass through.
1543		"""
1544
1545		if isinstance(spec, str) and spec in _CHORD_TONE_NAMES:
1546			spec = ChordTone(spec)
1547
1548		# The new spec replaces whatever a capture resolved, so its origin goes too.
1549		events = tuple(dataclasses.replace(e, pitch=spec, origin=None) for e in self.events)
1550
1551		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1552
1553	def rhythm (self) -> "Motif":
1554
1555		"""
1556		Strip pitches (and control gestures): a reusable rhythmic skeleton.
1557
1558		Timing, velocities, durations, and probabilities survive; re-pitch
1559		with :meth:`pitched` before placement (placing a skeleton raises).
1560		"""
1561
1562		events = tuple(dataclasses.replace(e, pitch=None, origin=None) for e in self.events)
1563
1564		return Motif(events=events, length=self.length)
1565
1566	def onsets (self) -> typing.List[float]:
1567
1568		"""The note onset beats, in order — ready for rhythm-first generation."""
1569
1570		return [e.beat for e in self.events]
1571
1572	def transpose (self, steps: typing.Optional[int] = None, semitones: typing.Optional[int] = None) -> "Motif":
1573
1574		"""
1575		Transpose pitched content; the keyword names the unit.
1576
1577		``steps=`` moves scale degrees diatonically (the sequencing move) and
1578		raises on absolute-MIDI or drum content; ``semitones=`` is the
1579		literal chromatic form for MIDI ints and degrees.  Drum motifs raise
1580		on both — a transposed drum name is a different instrument, not a
1581		transposition — and a captured drum raises too, because its number
1582		still remembers which instrument it came from.
1583		"""
1584
1585		if (steps is None) == (semitones is None):
1586			raise ValueError("transpose() takes exactly one of steps= or semitones=")
1587
1588		def move (pitch: PitchSpec, origin: typing.Optional[str]) -> PitchSpec:
1589
1590			_refuse_captured_drum(origin, "transpose()", "transposed")
1591
1592			if pitch is None:
1593				return None
1594
1595			if isinstance(pitch, Approach):
1596				moved = move(pitch.target, None)
1597				if not isinstance(moved, (int, Degree, ChordTone)):
1598					raise TypeError(f"transpose cannot aim an Approach at {type(moved).__name__} content")
1599				return Approach(moved)
1600
1601			if steps is not None:
1602				if isinstance(pitch, Degree):
1603					return dataclasses.replace(pitch, step=pitch.step + steps)
1604				raise TypeError(
1605					f"transpose(steps=) moves scale degrees — {type(pitch).__name__} content "
1606					f"has no degrees (use semitones= for MIDI ints)"
1607				)
1608
1609			assert semitones is not None	# exactly one of steps/semitones is set (validated above)
1610
1611			if isinstance(pitch, int):
1612				return pitch + semitones
1613			if isinstance(pitch, Degree):
1614				return dataclasses.replace(pitch, chroma=pitch.chroma + semitones)
1615			raise TypeError(f"transpose(semitones=) cannot move {type(pitch).__name__} content")
1616
1617		events = tuple(dataclasses.replace(e, pitch=move(e.pitch, e.origin)) for e in self.events)
1618
1619		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1620
1621	def invert (self, pivot: typing.Optional[int] = None) -> "Motif":
1622
1623		"""
1624		Mirror pitches around a pivot: MIDI content around a MIDI pivot,
1625		degree content around a degree pivot (default: the first note's pitch).
1626		Drum motifs raise, captured ones included.
1627		"""
1628
1629		pitched_events = [e for e in self.events if e.pitch is not None]
1630
1631		if not pitched_events:
1632			return self
1633
1634		first = pitched_events[0].pitch
1635
1636		if pivot is None:
1637			if isinstance(first, int):
1638				pivot = first
1639			elif isinstance(first, Degree):
1640				pivot = first.step
1641			else:
1642				raise TypeError(f"invert() cannot derive a pivot from {type(first).__name__} content")
1643
1644		def mirror (pitch: PitchSpec, origin: typing.Optional[str]) -> PitchSpec:
1645
1646			_refuse_captured_drum(origin, "invert()", "mirrored")
1647
1648			if pitch is None:
1649				return None
1650			if isinstance(pitch, int):
1651				return 2 * pivot - pitch
1652			if isinstance(pitch, Degree):
1653				mirrored = 2 * pivot - pitch.step
1654				if mirrored < 1:
1655					raise ValueError(
1656						f"invert() around degree {pivot} sends degree {pitch.step} below the tonic — "
1657						f"raise the pivot or use Degree octaves"
1658					)
1659				# Reflection around the pivot (read at octave 0) is an isometry, so a
1660				# note's register flips too: a degree an octave above the pivot lands an
1661				# octave below it.  Negating octave needs no scale length and leaves
1662				# octave-0 content unchanged.
1663				return dataclasses.replace(pitch, step=mirrored, octave=-pitch.octave, chroma=-pitch.chroma)
1664			raise TypeError(f"invert() cannot mirror {type(pitch).__name__} content")
1665
1666		events = tuple(dataclasses.replace(e, pitch=mirror(e.pitch, e.origin)) for e in self.events)
1667
1668		return Motif(events=events, length=self.length, controls=self.controls, fit=self.fit)
1669
1670	# ── description ─────────────────────────────────────────────────────
1671
1672	def describe (self) -> str:
1673
1674		"""A readable one-line summary: length, notes (pitch@beat), and control gestures."""
1675
1676		notes = ", ".join(f"{_event_label(e)}@{e.beat:g}" for e in self.events)
1677		parts = [f"Motif {self.length:g} beats", f"[{notes}]" if notes else "[no notes]"]
1678
1679		if self.controls:
1680			gestures = ", ".join(_control_label(c) for c in self.controls)
1681			parts.append(f"controls [{gestures}]")
1682
1683		return " ".join(parts)
1684
1685	def __str__ (self) -> str:
1686
1687		"""Printable form (same as :meth:`describe`)."""
1688
1689		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:
433	@classmethod
434	def empty (cls) -> "Motif":
435
436		"""The empty motif (zero events, zero length) — the identity for ``then``."""
437
438		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:
440	@classmethod
441	def from_events (
442		cls,
443		events: typing.Iterable[MotifEvent],
444		length: typing.Optional[float] = None,
445		controls: typing.Iterable[ControlEvent] = (),
446	) -> "Motif":
447
448		"""Build a motif from explicit events (power use; length defaults to the next whole beat)."""
449
450		events = tuple(events)
451		controls = tuple(controls)
452
453		return cls(
454			events = events,
455			length = _computed_length(events, controls) if length is None else length,
456			controls = controls,
457		)

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:
499	@classmethod
500	def degrees (
501		cls,
502		degrees: typing.List[typing.Union[int, Degree, None]],
503		beats: typing.Optional[typing.List[float]] = None,
504		velocities: typing.Any = _DEFAULT_VELOCITY,
505		durations: typing.Any = 1.0,
506		probabilities: typing.Any = 1.0,
507		length: typing.Optional[float] = None,
508	) -> "Motif":
509
510		"""
511		A melody written as 1-based scale degrees, one per beat by default.
512
513		Elements are ints (1 = tonic, 8 = tonic an octave up), ``None`` for a
514		rest (the beat slot still advances), or :class:`Degree` for octave/
515		chromatic detail.  Resolved against key + scale at placement.
516		Durations default to a full beat (each note holds its slot).
517		"""
518
519		converted: typing.List[PitchSpec] = []
520
521		for element in degrees:
522			if isinstance(element, int):
523				if element > _MAX_PLAUSIBLE_DEGREE:
524					raise ValueError(
525						f"Degree {element} is implausibly large — scale degrees are 1-based "
526						f"(8 = tonic an octave up). For MIDI note numbers use Motif.notes()."
527					)
528				converted.append(Degree(element))
529			elif isinstance(element, Degree) or element is None:
530				converted.append(element)
531			else:
532				raise TypeError(f"Motif.degrees takes ints, Degree, or None — got {type(element).__name__}")
533
534		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:
536	@classmethod
537	def notes (
538		cls,
539		notes: typing.List[typing.Union[int, None]],
540		beats: typing.Optional[typing.List[float]] = None,
541		velocities: typing.Any = _DEFAULT_VELOCITY,
542		durations: typing.Any = 1.0,
543		probabilities: typing.Any = 1.0,
544		length: typing.Optional[float] = None,
545	) -> "Motif":
546
547		"""A melody written as absolute MIDI note numbers (60 = middle C); ``None`` = rest."""
548
549		for element in notes:
550			# bool is a subclass of int, but True/False are never MIDI notes.
551			if isinstance(element, bool) or not (isinstance(element, int) or element is None):
552				raise TypeError(f"Motif.notes takes MIDI ints or None — got {type(element).__name__}")
553
554		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:
556	@classmethod
557	def hits (
558		cls,
559		pitch: typing.Union[int, str],
560		beats: typing.List[float],
561		length: typing.Optional[float] = None,
562		velocities: typing.Any = _DEFAULT_VELOCITY,
563		durations: typing.Any = 0.1,
564		probabilities: typing.Any = 1.0,
565	) -> "Motif":
566
567		"""One pitch (usually a drum name) at a list of beat positions — the ``hit()`` convention."""
568
569		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:
571	@classmethod
572	def steps (
573		cls,
574		steps: typing.List[int],
575		pitches: typing.Any,
576		velocities: typing.Any = _DEFAULT_VELOCITY,
577		durations: typing.Any = 0.1,
578		probabilities: typing.Any = 1.0,
579		step_duration: float = 0.25,
580		length: typing.Optional[float] = None,
581	) -> "Motif":
582
583		"""
584		Grid placement — the ``sequence()`` convention: ``steps`` are 0-based
585		grid indices (sixteenths by default), ``pitches`` a scalar or
586		parallel list of MIDI ints or drum names.
587		"""
588
589		n = len(steps)
590		pitch_list = _expand("pitches", pitches, n)
591		onsets = [s * step_duration for s in steps]
592
593		if length is None and n:
594			length = float(math.ceil((max(steps) + 1) * step_duration))
595
596		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:
598	@classmethod
599	def euclidean (
600		cls,
601		pulses: int,
602		steps: int,
603		pitch: typing.Union[int, str],
604		length: float = 4.0,
605		velocities: typing.Any = _DEFAULT_VELOCITY,
606		durations: typing.Any = 0.1,
607		probabilities: typing.Any = 1.0,
608	) -> "Motif":
609
610		"""A euclidean rhythm as a value: *pulses* spread evenly across *steps* over *length* beats."""
611
612		# bool is a subclass of int, but True/False are never MIDI notes.
613		if isinstance(pitch, bool):
614			raise TypeError(f"Motif.euclidean takes a MIDI int or drum name for pitch — got {pitch!r}")
615
616		# The kernel returns one 0/1 flag per grid step; onsets are the 1s.
617		# It validates pulses first, so pulses > steps still raises clearly.
618		flags = subsequence.sequence_utils.generate_euclidean_sequence(steps=steps, pulses=pulses)
619
620		if steps <= 0:
621			# A grid of zero steps holds no onsets — an empty motif of the given
622			# length, matching pulses=0 on a real grid (the empty-input policy).
623			return cls._from_sequence([], [], velocities, durations, probabilities, length)
624
625		step_duration = length / steps
626		onsets = [i * step_duration for i, flag in enumerate(flags) if flag]
627
628		return cls._from_sequence(
629			[pitch] * len(onsets),
630			onsets,
631			velocities, durations, probabilities, length,
632		)

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:
634	@classmethod
635	def preset (
636		cls,
637		name: str,
638		pitch: typing.Optional[typing.Union[int, str]] = None,
639		length: float = 4.0,
640		velocities: typing.Any = _DEFAULT_VELOCITY,
641		durations: typing.Any = 0.1,
642		probabilities: typing.Any = 1.0,
643	) -> "Motif":
644
645		"""A named world-rhythm timeline as a value — ``Motif.preset("son_clave_3_2")``.
646
647		Looks a curated timeline up in the world-rhythm table (clave family,
648		West-African bell patterns, tresillo/cinquillo, samba) and lays its
649		onsets across *length* beats.  Onset positions are exact pulse indices
650		from Toussaint's "The Geometry of Musical Rhythm"; each preset declares
651		its own grid (16 for the clave/4-4 timelines, 12 for the bell
652		patterns) and a default drum voice.
653
654		Parameters:
655			name: A preset name (``KeyError``-style ValueError lists them all).
656			pitch: The voice — a drum name or MIDI int; defaults to the
657				preset's General-MIDI voice (``"claves"``, ``"cowbell"``,
658				``"side_stick"``, ``"low_conga"``), so it sounds against the
659				standard GM drum map without a ``pitch=``.
660			length: Total beats the cycle spans (4 = one common-time bar).
661			velocities / durations / probabilities: The parallel-list params.
662
663		Returns:
664			A drum/pitched :class:`Motif` of the timeline's onsets.
665
666		Raises:
667			ValueError: If *name* is not a known preset.
668
669		Example:
670			```python
671			clave = subsequence.Motif.preset("son_clave_3_2")              # GM "claves"
672			bell  = subsequence.Motif.preset("bembe", pitch="cowbell")     # 12-pulse
673			```
674		"""
675
676		if name not in _WORLD_RHYTHMS:
677			known = ", ".join(sorted(_WORLD_RHYTHMS))
678			raise ValueError(f"Unknown rhythm preset {name!r}. Known presets: {known}.")
679
680		steps, grid, voice = _WORLD_RHYTHMS[name]
681
682		return cls.steps(
683			steps = list(steps),
684			pitches = pitch if pitch is not None else voice,
685			velocities = velocities,
686			durations = durations,
687			probabilities = probabilities,
688			step_duration = length / grid,
689			length = length,
690		)

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:
763	@classmethod
764	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":
765
766		"""Discrete CC writes at beat positions — mirrors ``p.cc()``; names resolve at placement."""
767
768		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[Literal['linear', 'ease_in', 'ease_out', 'ease_in_out', 'exponential', 'logarithmic', 's_curve'], Callable[[float], float]] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif:
770	@classmethod
771	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["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
772
773		"""A CC value swept ``start`` → ``end`` over a beat range — mirrors ``p.cc_ramp()``."""
774
775		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:
777	@classmethod
778	def pitch_bend (cls, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
779
780		"""Discrete pitch-bend writes (-1.0 to 1.0) at beat positions — mirrors ``p.pitch_bend()``."""
781
782		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[Literal['linear', 'ease_in', 'ease_out', 'ease_in_out', 'exponential', 'logarithmic', 's_curve'], Callable[[float], float]] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif:
784	@classmethod
785	def pitch_bend_ramp (cls, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
786
787		"""Pitch bend swept ``start`` → ``end`` (-1.0 to 1.0) over a beat range — mirrors ``p.pitch_bend_ramp()``."""
788
789		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:
791	@classmethod
792	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":
793
794		"""Discrete NRPN parameter writes at beat positions — mirrors ``p.nrpn()``."""
795
796		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[Literal['linear', 'ease_in', 'ease_out', 'ease_in_out', 'exponential', 'logarithmic', 's_curve'], Callable[[float], float]] = 'linear', fine: bool = True, null_reset: bool = True, length: Optional[float] = None, probability: float = 1.0) -> Motif:
798	@classmethod
799	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["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
800
801		"""An NRPN value swept over a beat range — mirrors ``p.nrpn_ramp()``."""
802
803		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, Literal['channel_coarse_tuning', 'channel_fine_tuning', 'modulation_depth_range', 'null_parameter', 'pitch_bend_sensitivity', 'tuning_bank_select', 'tuning_program_select']], values: List[int], beats: List[float], fine: bool = False, null_reset: bool = True, length: Optional[float] = None, probabilities: Any = 1.0) -> Motif:
805	@classmethod
806	def rpn (cls, parameter: typing.Union[int, "subsequence.declarations.RpnParameter"], 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":
807
808		"""Discrete RPN parameter writes at beat positions — mirrors ``p.rpn()``."""
809
810		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, Literal['channel_coarse_tuning', 'channel_fine_tuning', 'modulation_depth_range', 'null_parameter', 'pitch_bend_sensitivity', 'tuning_bank_select', 'tuning_program_select']], start: int, end: int, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[Literal['linear', 'ease_in', 'ease_out', 'ease_in_out', 'exponential', 'logarithmic', 's_curve'], Callable[[float], float]] = 'linear', fine: bool = True, null_reset: bool = True, length: Optional[float] = None, probability: float = 1.0) -> Motif:
812	@classmethod
813	def rpn_ramp (cls, parameter: typing.Union[int, "subsequence.declarations.RpnParameter"], start: int, end: int, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", fine: bool = True, null_reset: bool = True, length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
814
815		"""An RPN value swept over a beat range — mirrors ``p.rpn_ramp()``."""
816
817		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:
819	@classmethod
820	def osc (cls, address: str, values: typing.List[float], beats: typing.List[float], length: typing.Optional[float] = None, probabilities: typing.Any = 1.0) -> "Motif":
821
822		"""Discrete OSC float sends at beat positions — mirrors ``p.osc()``."""
823
824		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[Literal['linear', 'ease_in', 'ease_out', 'ease_in_out', 'exponential', 'logarithmic', 's_curve'], Callable[[float], float]] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif:
826	@classmethod
827	def osc_ramp (cls, address: str, start: float, end: float, beat_start: float = 0.0, beat_end: typing.Optional[float] = None, shape: typing.Union["subsequence.declarations.EasingCurve", "subsequence.easing.EasingFn"] = "linear", length: typing.Optional[float] = None, probability: float = 1.0) -> "Motif":
828
829		"""An OSC float swept over a beat range — mirrors ``p.osc_ramp()``."""
830
831		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:
835	def then (self, other: "Motif") -> "Motif":
836
837		"""Closed sequential concat: glue *other* after this motif into ONE longer motif."""
838
839		if not isinstance(other, Motif):
840			raise TypeError(f"then() takes a Motif — got {type(other).__name__}")
841
842		return Motif(
843			events = self.events + tuple(dataclasses.replace(e, beat=e.beat + self.length) for e in other.events),
844			length = self.length + other.length,
845			controls = self.controls + tuple(dataclasses.replace(c, beat=c.beat + self.length) for c in other.controls),
846			# fit is a dial, not content: keep ours, inherit the other's when
847			# we have none — join()/tiling folds from empty() (fit=None), and
848			# must not silently strip a generated motif's chord-snapping.
849			fit = self.fit if self.fit is not None else other.fit,
850		)

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

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

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:
1154	def slice (self, start: float, end: float) -> "Motif":
1155
1156		"""
1157		A window onto the motif, on its own authority: events starting outside
1158		are dropped; durations and ramp spans truncate at the cut (a truncated
1159		ramp ends at its interpolated cut value).  Beats shift so the window
1160		starts at 0.
1161		"""
1162
1163		if end <= start:
1164			raise ValueError(f"slice end ({end}) must be after start ({start})")
1165
1166		events = tuple(
1167			dataclasses.replace(e, beat=e.beat - start, duration=min(e.duration, end - e.beat))
1168			for e in self.events
1169			if start <= e.beat < end
1170		)
1171
1172		controls = []
1173
1174		for c in self.controls:
1175			if not (start <= c.beat < end):
1176				continue
1177			if c.end is not None and c.beat + c.span > end:
1178				kept = end - c.beat
1179				controls.append(dataclasses.replace(
1180					c, beat=c.beat - start, span=kept, end=c._value_at(kept / c.span),
1181				))
1182			else:
1183				controls.append(dataclasses.replace(c, beat=c.beat - start))
1184
1185		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:
1224	def reverse (self) -> "Motif":
1225
1226		"""Mirror the figure in time; ramps swap direction (a rising sweep falls)."""
1227
1228		events = tuple(
1229			dataclasses.replace(e, beat=max(0.0, self.length - e.beat - e.duration))
1230			for e in self.events
1231		)
1232		controls = tuple(
1233			dataclasses.replace(
1234				c,
1235				beat = max(0.0, self.length - c.beat - c.span),
1236				start = c.start if c.end is None else c.end,
1237				end = c.end if c.end is None else c.start,
1238			)
1239			for c in self.controls
1240		)
1241
1242		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:
1244	def rotate (self, beats: float) -> "Motif":
1245
1246		"""Shift every onset by *beats*, wrapping modulo the length (spans ride along)."""
1247
1248		if self.length == 0:
1249			return self
1250
1251		events = tuple(dataclasses.replace(e, beat=(e.beat + beats) % self.length) for e in self.events)
1252		controls = tuple(dataclasses.replace(c, beat=(c.beat + beats) % self.length) for c in self.controls)
1253
1254		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:
1256	def stretch (self, factor: float) -> "Motif":
1257
1258		"""Scale time by *factor* (2.0 = half-time feel): beats, durations, spans, and length."""
1259
1260		if factor <= 0:
1261			raise ValueError(f"Stretch factor must be positive — got {factor}")
1262
1263		events = tuple(
1264			dataclasses.replace(e, beat=e.beat * factor, duration=e.duration * factor)
1265			for e in self.events
1266		)
1267		controls = tuple(
1268			dataclasses.replace(c, beat=c.beat * factor, span=c.span * factor)
1269			for c in self.controls
1270		)
1271
1272		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:
1274	def quantize (self, grid: float) -> "Motif":
1275
1276		"""Snap note onsets to the nearest multiple of *grid* beats (control gestures untouched).
1277
1278		An onset exactly midway between grid lines snaps LATER (round half
1279		up) — every midpoint moves the same way, the predictable behaviour
1280		for a musician.  (Python's own ``round()`` is half-to-even, which
1281		made exact midpoints snap in alternating directions.)
1282		"""
1283
1284		if grid <= 0:
1285			raise ValueError(f"Quantize grid must be positive — got {grid}")
1286
1287		events = tuple(
1288			dataclasses.replace(e, beat=math.floor(e.beat / grid + 0.5) * grid)
1289			for e in self.events
1290		)
1291
1292		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:
1294	def accent (self, beat: float, amount: int = 20) -> "Motif":
1295
1296		"""Add *amount* velocity to every note at the given beat position (0-based beats)."""
1297
1298		def boost (velocity: subsequence.declarations.VelocityValue) -> subsequence.declarations.VelocityValue:
1299			# Clamp both ends: a negative amount (a de-accent) must not store
1300			# a velocity below 1, which MIDI cannot play.
1301			if isinstance(velocity, (tuple, list)):
1302				return (max(1, min(127, velocity[0] + amount)), max(1, min(127, velocity[1] + amount)))
1303			return max(1, min(127, velocity + amount))
1304
1305		events = tuple(
1306			dataclasses.replace(e, velocity=boost(e.velocity)) if abs(e.beat - beat) < 1e-9 else e
1307			for e in self.events
1308		)
1309
1310		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], List[int]]) -> Motif:
1312	def with_velocity (self, velocity: subsequence.declarations.VelocityValue) -> "Motif":
1313
1314		"""Replace every note's velocity (an int, or a ``(low, high)`` random range)."""
1315
1316		events = tuple(dataclasses.replace(e, velocity=velocity) for e in self.events)
1317
1318		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:
1351	def vary (
1352		self,
1353		notes: int = 1,
1354		position: str = "end",
1355		seed: typing.Optional[int] = None,
1356		rng: typing.Optional[random.Random] = None,
1357		keep_contour: bool = False,
1358	) -> "Motif":
1359
1360		"""Replace a few pitches, preserving the rhythm — the smallest variation.
1361
1362		Rhythm, velocities, durations, rests, and control gestures are
1363		untouched; only the chosen notes' pitches move (by a small melodic
1364		nudge: scale steps for degrees, semitones for MIDI ints).
1365
1366		Parameters:
1367			notes: How many pitched notes to vary (clamped to what exists).
1368			position: Which notes — ``"end"`` (the tail, the default),
1369				``"start"``, or ``"anywhere"`` (drawn from the stream).
1370			seed: Seed for the variation.  A standalone vary without a seed
1371				warns — module-level nondeterminism breaks live reload.
1372			rng: An explicit random stream (overrides ``seed``; used by
1373				recipe machinery).
1374			keep_contour: When True, the variation preserves the line's
1375				CSEG — every varied note keeps its rank relations with
1376				every other note, so the melodic shape is identical (the
1377				motif-identity guard).  Where no nudge can preserve the
1378				contour, that note stays unchanged — shape wins over
1379				motion.
1380
1381		Example:
1382			```python
1383			answer = call.vary(notes=1, seed=4)     # same figure, new tail note
1384			```
1385		"""
1386
1387		if notes < 0:
1388			raise ValueError(f"notes must be at least 0, got {notes}")
1389		if position not in ("end", "start", "anywhere"):
1390			raise ValueError(f'position must be "end", "start", or "anywhere" — got {position!r}')
1391
1392		if rng is None:
1393			if seed is None:
1394				warnings.warn(
1395					"vary() without seed= is nondeterministic — pass seed= so the "
1396					"value survives live reload",
1397					stacklevel = 2,
1398				)
1399				rng = random.Random()
1400			else:
1401				rng = random.Random(seed)
1402
1403		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1404		count = min(notes, len(pitched_indices))
1405
1406		if count == 0:
1407			return self
1408
1409		if position == "end":
1410			chosen = pitched_indices[-count:]
1411		elif position == "start":
1412			chosen = pitched_indices[:count]
1413		else:
1414			chosen = sorted(rng.sample(pitched_indices, count))
1415
1416		events = list(self.events)
1417
1418		for index in chosen:
1419			if keep_contour:
1420				replacement = self._contour_safe_nudge(events, index, pitched_indices, rng)
1421				if replacement is not None:
1422					events[index] = dataclasses.replace(events[index], pitch = replacement)
1423			else:
1424				events[index] = dataclasses.replace(events[index], pitch = self._nudged_pitch(events[index].pitch, rng, events[index].origin))
1425
1426		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:
1498	def answer (self, to: typing.Union[int, Degree] = 1) -> "Motif":
1499
1500		"""Call → response: re-aim the tail to a stable degree.
1501
1502		The classic consequent move — the figure repeats but its last pitched
1503		note lands home (degree 1 by default; pass ``to=5`` for a half-close,
1504		or a full ``Degree`` for register control).  Everything else —
1505		rhythm, the other pitches, velocities, controls — is untouched.
1506
1507		Degree content only: absolute MIDI has no degrees to re-aim (build
1508		the call with ``motif([...])``), and drums raise.
1509		"""
1510
1511		target = to if isinstance(to, Degree) else Degree(int(to))
1512
1513		pitched_indices = [index for index, event in enumerate(self.events) if event.pitch is not None]
1514
1515		if not pitched_indices:
1516			return self
1517
1518		last = self.events[pitched_indices[-1]]
1519
1520		if not isinstance(last.pitch, Degree):
1521			raise TypeError(
1522				f"answer() re-aims scale degrees — the tail is {type(last.pitch).__name__} "
1523				"content (build the call with motif([...]) for degree content)"
1524			)
1525
1526		if isinstance(to, int):
1527			# Keep the call's register: only the step is re-aimed.
1528			target = dataclasses.replace(last.pitch, step = int(to), chroma = 0)
1529
1530		events = list(self.events)
1531		events[pitched_indices[-1]] = dataclasses.replace(last, pitch = target)
1532
1533		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:
1535	def pitched (self, spec: PitchSpec) -> "Motif":
1536
1537		"""
1538		Replace every pitch with one spec — a kick rhythm becomes a bass line.
1539
1540		``"root"`` / ``"third"`` / ``"fifth"`` / ``"seventh"`` become chord
1541		tones; any other string is a drum name; ints are MIDI; Degree /
1542		ChordTone / Approach pass through.
1543		"""
1544
1545		if isinstance(spec, str) and spec in _CHORD_TONE_NAMES:
1546			spec = ChordTone(spec)
1547
1548		# The new spec replaces whatever a capture resolved, so its origin goes too.
1549		events = tuple(dataclasses.replace(e, pitch=spec, origin=None) for e in self.events)
1550
1551		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:
1553	def rhythm (self) -> "Motif":
1554
1555		"""
1556		Strip pitches (and control gestures): a reusable rhythmic skeleton.
1557
1558		Timing, velocities, durations, and probabilities survive; re-pitch
1559		with :meth:`pitched` before placement (placing a skeleton raises).
1560		"""
1561
1562		events = tuple(dataclasses.replace(e, pitch=None, origin=None) for e in self.events)
1563
1564		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]:
1566	def onsets (self) -> typing.List[float]:
1567
1568		"""The note onset beats, in order — ready for rhythm-first generation."""
1569
1570		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:
1572	def transpose (self, steps: typing.Optional[int] = None, semitones: typing.Optional[int] = None) -> "Motif":
1573
1574		"""
1575		Transpose pitched content; the keyword names the unit.
1576
1577		``steps=`` moves scale degrees diatonically (the sequencing move) and
1578		raises on absolute-MIDI or drum content; ``semitones=`` is the
1579		literal chromatic form for MIDI ints and degrees.  Drum motifs raise
1580		on both — a transposed drum name is a different instrument, not a
1581		transposition — and a captured drum raises too, because its number
1582		still remembers which instrument it came from.
1583		"""
1584
1585		if (steps is None) == (semitones is None):
1586			raise ValueError("transpose() takes exactly one of steps= or semitones=")
1587
1588		def move (pitch: PitchSpec, origin: typing.Optional[str]) -> PitchSpec:
1589
1590			_refuse_captured_drum(origin, "transpose()", "transposed")
1591
1592			if pitch is None:
1593				return None
1594
1595			if isinstance(pitch, Approach):
1596				moved = move(pitch.target, None)
1597				if not isinstance(moved, (int, Degree, ChordTone)):
1598					raise TypeError(f"transpose cannot aim an Approach at {type(moved).__name__} content")
1599				return Approach(moved)
1600
1601			if steps is not None:
1602				if isinstance(pitch, Degree):
1603					return dataclasses.replace(pitch, step=pitch.step + steps)
1604				raise TypeError(
1605					f"transpose(steps=) moves scale degrees — {type(pitch).__name__} content "
1606					f"has no degrees (use semitones= for MIDI ints)"
1607				)
1608
1609			assert semitones is not None	# exactly one of steps/semitones is set (validated above)
1610
1611			if isinstance(pitch, int):
1612				return pitch + semitones
1613			if isinstance(pitch, Degree):
1614				return dataclasses.replace(pitch, chroma=pitch.chroma + semitones)
1615			raise TypeError(f"transpose(semitones=) cannot move {type(pitch).__name__} content")
1616
1617		events = tuple(dataclasses.replace(e, pitch=move(e.pitch, e.origin)) for e in self.events)
1618
1619		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 — and a captured drum raises too, because its number still remembers which instrument it came from.

def invert(self, pivot: Optional[int] = None) -> Motif:
1621	def invert (self, pivot: typing.Optional[int] = None) -> "Motif":
1622
1623		"""
1624		Mirror pitches around a pivot: MIDI content around a MIDI pivot,
1625		degree content around a degree pivot (default: the first note's pitch).
1626		Drum motifs raise, captured ones included.
1627		"""
1628
1629		pitched_events = [e for e in self.events if e.pitch is not None]
1630
1631		if not pitched_events:
1632			return self
1633
1634		first = pitched_events[0].pitch
1635
1636		if pivot is None:
1637			if isinstance(first, int):
1638				pivot = first
1639			elif isinstance(first, Degree):
1640				pivot = first.step
1641			else:
1642				raise TypeError(f"invert() cannot derive a pivot from {type(first).__name__} content")
1643
1644		def mirror (pitch: PitchSpec, origin: typing.Optional[str]) -> PitchSpec:
1645
1646			_refuse_captured_drum(origin, "invert()", "mirrored")
1647
1648			if pitch is None:
1649				return None
1650			if isinstance(pitch, int):
1651				return 2 * pivot - pitch
1652			if isinstance(pitch, Degree):
1653				mirrored = 2 * pivot - pitch.step
1654				if mirrored < 1:
1655					raise ValueError(
1656						f"invert() around degree {pivot} sends degree {pitch.step} below the tonic — "
1657						f"raise the pivot or use Degree octaves"
1658					)
1659				# Reflection around the pivot (read at octave 0) is an isometry, so a
1660				# note's register flips too: a degree an octave above the pivot lands an
1661				# octave below it.  Negating octave needs no scale length and leaves
1662				# octave-0 content unchanged.
1663				return dataclasses.replace(pitch, step=mirrored, octave=-pitch.octave, chroma=-pitch.chroma)
1664			raise TypeError(f"invert() cannot mirror {type(pitch).__name__} content")
1665
1666		events = tuple(dataclasses.replace(e, pitch=mirror(e.pitch, e.origin)) for e in self.events)
1667
1668		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, captured ones included.

def describe(self) -> str:
1672	def describe (self) -> str:
1673
1674		"""A readable one-line summary: length, notes (pitch@beat), and control gestures."""
1675
1676		notes = ", ".join(f"{_event_label(e)}@{e.beat:g}" for e in self.events)
1677		parts = [f"Motif {self.length:g} beats", f"[{notes}]" if notes else "[no notes]"]
1678
1679		if self.controls:
1680			gestures = ", ".join(_control_label(c) for c in self.controls)
1681			parts.append(f"controls [{gestures}]")
1682
1683		return " ".join(parts)

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

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

Coerce any iterable of Motifs.

segments: Tuple[Motif, ...]
recipe: Optional[subsequence.motifs._PhraseRecipe]
length: float
1865	@property
1866	def length (self) -> float:
1867
1868		"""Total length in beats (sum of segment lengths)."""
1869
1870		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:
1872	@classmethod
1873	def develop (
1874		cls,
1875		motif: Motif,
1876		bars: int = 8,
1877		plan: typing.Optional[typing.Union[typing.Sequence[str], str]] = None,
1878		seed: typing.Optional[int] = None,
1879		beats_per_bar: float = 4.0,
1880	) -> "Phrase":
1881
1882		"""Grow a motif into a phrase by a plan — the phrase generator.
1883
1884		``plan`` follows the standard form.  The literal form is a **list of
1885		unit labels** — ``plan=["a", "a", "a", "b"]``, equivalently
1886		``["a"] * 3 + ["b"]``: the first label is the given motif, each new
1887		label is a generated contrast unit (the source's rhythm, freshly
1888		re-pitched), a repeated label is a restatement, and *bars* spreads
1889		evenly across the units.  A bare string is a **recipe name** from
1890		the curated table — ``plan="call_response"`` (call, answer, call,
1891		varied answer) — reserved for plans whose semantics exceed a label
1892		skeleton.  A letter string is not a plan: a sequence of labels is a
1893		sequence, so it is a list.
1894
1895		The result carries its recipe, so :meth:`reroll` can regenerate a
1896		region later.
1897
1898		Parameters:
1899			motif: The source unit (its length must be ``bars / len(units)``
1900				bars — the plan's units tile the phrase exactly).
1901			bars: Phrase length in bars (must divide evenly by the unit
1902				count).
1903			plan: A list of unit labels, or a recipe name.
1904			seed: Seed for the generated units.  Without one, develop()
1905				warns — module-level nondeterminism breaks live reload.
1906			beats_per_bar: Bar size in beats (the value is context-free;
1907				4 is the common-time default).
1908
1909		Example:
1910			```python
1911			call = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
1912			lead = subsequence.Phrase.develop(call, bars=8, plan="call_response", seed=11)
1913			```
1914		"""
1915
1916		if plan is None:
1917			raise ValueError(
1918				'develop() needs a plan= — a list of unit labels (plan=["a", "a", "a", "b"]) '
1919				'or a recipe name (plan="call_response")'
1920			)
1921
1922		if seed is None:
1923			warnings.warn(
1924				"develop() without seed= is nondeterministic — pass seed= so the "
1925				"value survives live reload",
1926				stacklevel = 2,
1927			)
1928
1929		# How many units the plan asks for — known before any unit is built,
1930		# so a short motif can tile up to the unit size first.
1931		if isinstance(plan, str):
1932			if plan not in _PHRASE_RECIPES:
1933				known = ", ".join(sorted(_PHRASE_RECIPES))
1934				hint = ""
1935				if plan.isalpha() and plan == plan.lower() and len(set(plan)) < len(plan):
1936					spelled = ", ".join(repr(c) for c in plan)
1937					hint = f" A letter string is not a plan — a sequence of labels is a list: plan=[{spelled}]."
1938				raise ValueError(f"Unknown phrase recipe {plan!r}. Known recipes: {known}.{hint}")
1939			unit_count = _PHRASE_RECIPES[plan][0]
1940		else:
1941			labels = list(plan)
1942			if not labels or not all(isinstance(label, str) and label for label in labels):
1943				raise ValueError("plan labels must be non-empty strings, e.g. plan=['a', 'a', 'b']")
1944			unit_count = len(labels)
1945
1946		source = _tile_source(motif, bars, unit_count, beats_per_bar)
1947
1948		# An unseeded call draws a fresh salt so repeated calls genuinely
1949		# differ, as the warning above promises — interpolating None gave the
1950		# FIXED seed "None:..." and silently returned the same phrase every
1951		# time.
1952		salt = seed if seed is not None else random.randrange(2 ** 32)
1953
1954		if isinstance(plan, str):
1955			units = _PHRASE_RECIPES[plan][1](source, salt)
1956			stored_plan: typing.Union[typing.Tuple[str, ...], str] = plan
1957		else:
1958			generated: typing.Dict[str, Motif] = {labels[0]: source}
1959			for label in labels:
1960				if label not in generated:
1961					generated[label] = _contrast_unit(source, random.Random(f"{salt}:unit:{label}"))
1962			units = [generated[label] for label in labels]
1963			stored_plan = tuple(labels)
1964
1965		return cls(units, recipe = _PhraseRecipe(
1966			source = motif,
1967			plan = stored_plan,
1968			bars = bars,
1969			seed = seed,
1970			beats_per_bar = beats_per_bar,
1971		))

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:
1973	def reroll (
1974		self,
1975		bar: typing.Optional[int] = None,
1976		bars: typing.Optional[typing.Sequence[int]] = None,
1977		seed: typing.Optional[int] = None,
1978	) -> "Phrase":
1979
1980		"""Regenerate only the named bars — rhythm and boundary pitches kept.
1981
1982		Within each named bar, the first and last pitched notes stay (the
1983		boundary pins) and the interior pitches re-roll from a fresh per-bar
1984		stream salted by ``seed=`` (an unseeded call draws a fresh salt, so
1985		each call genuinely differs); onsets, durations, velocities, rests,
1986		drums, and control gestures are untouched.  Segmentation and the
1987		recipe survive, so rerolls compose.
1988
1989		Only a phrase that carries a recipe can reroll — a hand-written or
1990		transformed phrase raises loudly (its notes no longer come from a
1991		generator, so regenerating them would invent music).
1992
1993		Parameters:
1994			bar: A single 1-based bar to reroll.
1995			bars: A list of 1-based bars (the paired plural spelling).
1996			seed: Seed for the new pitches (salted per bar).  Without one,
1997				reroll() warns.
1998
1999		Example:
2000			```python
2001			lead = lead.reroll(bar=7, seed=4)    # only bar 7; rhythm + boundaries kept
2002			```
2003		"""
2004
2005		if self.recipe is None:
2006			raise ValueError(
2007				"this phrase carries no recipe (it was written by hand, or transformed "
2008				"since generation) — reroll() regenerates from a recipe; edit segments "
2009				"with replace(), or rebuild with Phrase.develop()"
2010			)
2011
2012		if (bar is None) == (bars is None):
2013			raise ValueError("reroll() takes exactly one of bar= (an int) or bars= (a list)")
2014
2015		region = [bar] if bar is not None else list(bars or [])
2016		beats_per_bar = self.recipe.beats_per_bar
2017		total_bars = int(round(self.length / beats_per_bar))
2018
2019		for number in region:
2020			if not isinstance(number, int) or isinstance(number, bool) or not 1 <= number <= total_bars:
2021				raise ValueError(f"bar {number!r} is outside this phrase (1–{total_bars})")
2022
2023		if seed is None:
2024			warnings.warn(
2025				"reroll() without seed= is nondeterministic — pass seed= so the "
2026				"value survives live reload",
2027				stacklevel = 2,
2028			)
2029
2030		# Unseeded rerolls draw a fresh salt — a fixed "None:..." seed would
2031		# "re-roll" to the identical pitches every time.
2032		salt = seed if seed is not None else random.randrange(2 ** 32)
2033
2034		windows = [
2035			((number - 1) * beats_per_bar, number * beats_per_bar, random.Random(f"{salt}:reroll:{number}"))
2036			for number in sorted(set(region))
2037		]
2038
2039		new_segments: typing.List[Motif] = []
2040		offset = 0.0
2041
2042		for segment in self.segments:
2043
2044			events = list(segment.events)
2045
2046			for window_start, window_end, rng in windows:
2047
2048				inside = [
2049					index for index, event in enumerate(events)
2050					if window_start <= offset + event.beat < window_end
2051					and event.pitch is not None and not isinstance(event.pitch, str)
2052				]
2053
2054				# Boundary pins: the first and last pitched notes of the bar
2055				# stay; only the interior re-rolls.
2056				for index in inside[1:-1]:
2057					events[index] = dataclasses.replace(
2058						events[index],
2059						pitch = segment._nudged_pitch(events[index].pitch, rng, events[index].origin),
2060					)
2061
2062			new_segments.append(Motif(events = tuple(events), length = segment.length, controls = segment.controls))
2063			offset += segment.length
2064
2065		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:
2067	def flatten (self) -> Motif:
2068
2069		"""Erase segmentation: one long Motif (the monoid homomorphism onto ``then``)."""
2070
2071		return Motif.join(self.segments)

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

def stack( self, other: Union[Motif, Phrase]) -> Motif:
2117	def stack (self, other: typing.Union[Motif, "Phrase"]) -> Motif:
2118
2119		"""The spelled form of ``&`` — flattens, then merges."""
2120
2121		return self.flatten().stack(other)

The spelled form of & — flattens, then merges.

def slice(self, start: float, end: float) -> Phrase:
2123	def slice (self, start: float, end: float) -> "Phrase":
2124
2125		"""A window; re-segments at the cut points (partial segments are sliced)."""
2126
2127		segments = []
2128		offset = 0.0
2129
2130		for segment in self.segments:
2131			seg_start, seg_end = offset, offset + segment.length
2132			lo, hi = max(start, seg_start), min(end, seg_end)
2133			if lo < hi:
2134				segments.append(segment.slice(lo - seg_start, hi - seg_start))
2135			offset = seg_end
2136
2137		return Phrase(segments)

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

def replace( self, position: int, motif: Motif) -> Phrase:
2139	def replace (self, position: int, motif: Motif) -> "Phrase":
2140
2141		"""Replace the segment at a 1-based position (musicians count from one)."""
2142
2143		if not 1 <= position <= len(self.segments):
2144			raise IndexError(f"Phrase has {len(self.segments)} segments — position {position} is out of range (1-based)")
2145
2146		segments = list(self.segments)
2147		segments[position - 1] = motif
2148
2149		return Phrase(segments)

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

def reverse(self) -> Phrase:
2153	def reverse (self) -> "Phrase":
2154
2155		"""Reverse the whole timeline: segments reverse order AND each reverses internally."""
2156
2157		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:
2159	def rotate (self, beats: float) -> "Phrase":
2160
2161		"""Rotate the whole timeline modulo the total length, then re-segment at the original boundaries."""
2162
2163		flat = self.flatten().rotate(beats)
2164		segments = []
2165		offset = 0.0
2166
2167		# Re-segment by onset (events keep their full durations — a note may
2168		# ring past its new segment, exactly as it does on the flat timeline).
2169		for segment in self.segments:
2170			lo, hi = offset, offset + segment.length
2171			segments.append(Motif(
2172				events = tuple(
2173					dataclasses.replace(e, beat=e.beat - lo)
2174					for e in flat.events if lo <= e.beat < hi
2175				),
2176				length = segment.length,
2177				controls = tuple(
2178					dataclasses.replace(c, beat=c.beat - lo)
2179					for c in flat.controls if lo <= c.beat < hi
2180				),
2181			))
2182			offset = hi
2183
2184		return Phrase(segments)

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

def stretch(self, factor: float) -> Phrase:
2192	def stretch (self, factor: float) -> "Phrase":
2193
2194		"""Scale time in every segment (lengths scale with them)."""
2195
2196		return self._lift("stretch", factor)

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

def quantize(self, grid: float) -> Phrase:
2198	def quantize (self, grid: float) -> "Phrase":
2199
2200		"""Snap note onsets segment-wise."""
2201
2202		return self._lift("quantize", grid)

Snap note onsets segment-wise.

def with_velocity( self, velocity: Union[int, Tuple[int, int], List[int]]) -> Phrase:
2204	def with_velocity (self, velocity: subsequence.declarations.VelocityValue) -> "Phrase":
2205
2206		"""Replace every note's velocity, segment-wise."""
2207
2208		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:
2210	def pitched (self, spec: PitchSpec) -> "Phrase":
2211
2212		"""Replace every pitch, segment-wise."""
2213
2214		return self._lift("pitched", spec)

Replace every pitch, segment-wise.

def rhythm(self) -> Phrase:
2216	def rhythm (self) -> "Phrase":
2217
2218		"""Strip pitches segment-wise: a phrase-shaped skeleton."""
2219
2220		return self._lift("rhythm")

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

def transpose( self, steps: Optional[int] = None, semitones: Optional[int] = None) -> Phrase:
2222	def transpose (self, steps: typing.Optional[int] = None, semitones: typing.Optional[int] = None) -> "Phrase":
2223
2224		"""Transpose every segment (see :meth:`Motif.transpose`)."""
2225
2226		return self._lift("transpose", steps=steps, semitones=semitones)

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

def invert(self, pivot: Optional[int] = None) -> Phrase:
2228	def invert (self, pivot: typing.Optional[int] = None) -> "Phrase":
2229
2230		"""Mirror pitches in every segment around one pivot (see :meth:`Motif.invert`)."""
2231
2232		if pivot is None:
2233			for segment in self.segments:
2234				for event in segment.events:
2235					if event.pitch is not None:
2236						if isinstance(event.pitch, int):
2237							pivot = event.pitch
2238						elif isinstance(event.pitch, Degree):
2239							pivot = event.pitch.step
2240						break
2241				if pivot is not None:
2242					break
2243
2244		return self._lift("invert", pivot=pivot)

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

def describe(self) -> str:
2246	def describe (self) -> str:
2247
2248		"""A readable summary: total length and each segment on its own line."""
2249
2250		header = f"Phrase {self.length:g} beats, {len(self.segments)} segments"
2251		lines = [f"  {i + 1}. {segment.describe()}" for i, segment in enumerate(self.segments)]
2252
2253		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:
2262def motif (
2263	degrees: typing.List[typing.Union[int, Degree, None]],
2264	beats: typing.Optional[typing.List[float]] = None,
2265	velocities: typing.Any = _DEFAULT_VELOCITY,
2266	durations: typing.Any = 1.0,
2267	probabilities: typing.Any = 1.0,
2268	length: typing.Optional[float] = None,
2269) -> Motif:
2270
2271	"""
2272	The lowercase shortcut: a melody as 1-based scale degrees.
2273
2274	``subsequence.motif([5, 6, 5, 3])`` is ``Motif.degrees([5, 6, 5, 3])`` —
2275	relative pitch is the primary form.  For absolute MIDI note numbers use
2276	``Motif.notes([64, 65, 64, 60])``; implausibly large ints here raise so
2277	a pasted MIDI list fails loud instead of squealing octaves up.
2278	"""
2279
2280	return Motif.degrees(
2281		degrees,
2282		beats = beats,
2283		velocities = velocities,
2284		durations = durations,
2285		probabilities = probabilities,
2286		length = length,
2287	)

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:
2290def sentence (
2291	motif: Motif,
2292	bars: int = 8,
2293	cadence: str = "strong",
2294	seed: typing.Optional[int] = None,
2295	beats_per_bar: float = 4.0,
2296) -> Phrase:
2297
2298	"""The classical sentence, as a thin combinator — idea, idea, drive, close.
2299
2300	Four units: the basic idea stated twice (the presentation), a generated
2301	contrast unit (the continuation — the source's rhythm, freshly
2302	re-pitched), and a second contrast unit whose tail lands on the
2303	cadence's close degree (the cadential close).  An 8-bar sentence from a
2304	2-bar idea is the textbook proportion; a shorter idea tiles up to the
2305	unit size first.
2306
2307	The melodic side of a cadence only — pair it with the harmonic side
2308	(``prog.cadence()``, ``Progression.generate(cadence=)``, or
2309	``request_cadence()``) and the two arrive together.
2310
2311	Parameters:
2312		motif: The basic idea (degree content — the close re-aims a degree).
2313		bars: Sentence length (must divide evenly across the 4 units).
2314		cadence: The close — ``"strong"`` lands on 1, ``"open"`` on 5,
2315			``"soft"``/``"fakeout"`` on 1 (theory aliases accepted).
2316		seed: Seed for the generated continuation units (seed-or-warn).
2317		beats_per_bar: Bar size in beats (context-free; 4 is the default).
2318
2319	Example:
2320		```python
2321		idea = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
2322		verse_lead = subsequence.sentence(idea, bars=8, cadence="open", seed=11)
2323		```
2324	"""
2325
2326	spec = subsequence.cadences.cadence_formula(cadence)
2327
2328	if seed is None:
2329		warnings.warn(
2330			"sentence() without seed= is nondeterministic — pass seed= so the "
2331			"value survives live reload",
2332			stacklevel = 2,
2333		)
2334
2335	source = _tile_source(motif, bars, 4, beats_per_bar)
2336
2337	# Unseeded calls draw a fresh salt (a fixed "None:..." seed would return
2338	# the same sentence every time, belying the warning above).
2339	salt = seed if seed is not None else random.randrange(2 ** 32)
2340
2341	continuation = _contrast_unit(source, random.Random(f"{salt}:sentence:continuation"))
2342	cadential = _contrast_unit(source, random.Random(f"{salt}:sentence:cadential")).answer(to = spec.close_degree)
2343
2344	return Phrase([source, source, continuation, cadential], recipe = _PhraseRecipe(
2345		source = motif,
2346		plan = "sentence",
2347		bars = bars,
2348		seed = seed,
2349		beats_per_bar = beats_per_bar,
2350		cadence = spec.name,
2351	))

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:
2354def period (
2355	antecedent: typing.Union[Motif, Phrase],
2356	cadence: str = "strong",
2357	beats_per_bar: float = 4.0,
2358) -> Phrase:
2359
2360	"""The classical period, as a thin combinator — question, then answer.
2361
2362	Two halves: the antecedent with its tail re-aimed to the open half-close
2363	(degree 5 — the question), then the same material restated with its tail
2364	on the cadence's close degree (the answer).  The two halves differ
2365	exactly at their closes — the open/closed contrast *is* the period.
2366
2367	Deterministic: no notes are generated, only the two tail notes re-aim
2368	(so there is no seed).  Vary the consequent yourself for a looser
2369	restatement: ``period(a).reroll(bar=7, seed=4)``.
2370
2371	Parameters:
2372		antecedent: The first half — a Motif, or a Phrase whose segmentation
2373			is kept (only its last segment's tail re-aims).
2374		cadence: The consequent's close — ``"strong"`` lands on 1 (theory
2375			aliases accepted).
2376		beats_per_bar: Bar size in beats, recorded for ``reroll()`` windows.
2377
2378	Example:
2379		```python
2380		idea = subsequence.motif([3, 4, 5, 1, None, 6, 5, 4], length=8)
2381		lead = subsequence.period(idea)        # 16 beats: half-close, then home
2382		```
2383	"""
2384
2385	spec = subsequence.cadences.cadence_formula(cadence)
2386	open_degree = subsequence.cadences.cadence_formula("open").close_degree
2387
2388	units = list(antecedent.segments) if isinstance(antecedent, Phrase) else [antecedent]
2389
2390	if not units or sum(unit.length for unit in units) <= 0:
2391		raise ValueError("cannot build a period from an empty antecedent")
2392
2393	tail = units[-1]
2394
2395	antecedent_units = units[:-1] + [tail.answer(to = open_degree)]
2396	consequent_units = units[:-1] + [tail.answer(to = spec.close_degree)]
2397
2398	source = antecedent.flatten() if isinstance(antecedent, Phrase) else antecedent
2399	total_beats = 2 * sum(unit.length for unit in units)
2400
2401	return Phrase(antecedent_units + consequent_units, recipe = _PhraseRecipe(
2402		source = source,
2403		plan = "period",
2404		bars = int(round(total_beats / beats_per_bar)),
2405		seed = None,
2406		beats_per_bar = beats_per_bar,
2407		cadence = spec.name,
2408	))

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:
 97@dataclasses.dataclass(frozen=True)
 98class Degree:
 99
100	"""
101	A scale degree — 1-based, resolved against key + scale at placement.
102
103	Degree 1 is the tonic; 8 is the tonic an octave up (steps may exceed the
104	scale length and resolve into higher octaves).  ``octave`` shifts whole
105	octaves; ``chroma`` is a chromatic offset in semitones (+1 = sharpened).
106	"""
107
108	step: int
109	octave: int = 0
110	chroma: int = 0
111
112	def __post_init__ (self) -> None:
113
114		"""Validate that the degree is 1-based and plausibly a degree."""
115
116		if self.step < 1:
117			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:
120@dataclasses.dataclass(frozen=True)
121class ChordTone:
122
123	"""
124	An index into the current chord's tones — 1-based, resolved at placement.
125
126	Accepts an int (1 = root, 2 = third, ...) or one of the names
127	``"root"`` / ``"third"`` / ``"fifth"`` / ``"seventh"``.  ``octave``
128	shifts whole octaves.
129	"""
130
131	index: int
132	octave: int = 0
133
134	def __init__ (self, index_or_name: typing.Union[int, str], octave: int = 0) -> None:
135
136		"""Normalize a tone name to its 1-based index."""
137
138		if isinstance(index_or_name, str):
139			if index_or_name not in _CHORD_TONE_NAMES:
140				raise ValueError(
141					f"Unknown chord tone name '{index_or_name}' — "
142					f"use one of {sorted(_CHORD_TONE_NAMES)} or a 1-based index"
143				)
144			index = _CHORD_TONE_NAMES[index_or_name]
145		else:
146			index = index_or_name
147
148		if index < 1:
149			raise ValueError(f"Chord tone indices are 1-based (1 = root) — got {index}")
150
151		object.__setattr__(self, "index", index)
152		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)
134	def __init__ (self, index_or_name: typing.Union[int, str], octave: int = 0) -> None:
135
136		"""Normalize a tone name to its 1-based index."""
137
138		if isinstance(index_or_name, str):
139			if index_or_name not in _CHORD_TONE_NAMES:
140				raise ValueError(
141					f"Unknown chord tone name '{index_or_name}' — "
142					f"use one of {sorted(_CHORD_TONE_NAMES)} or a 1-based index"
143				)
144			index = _CHORD_TONE_NAMES[index_or_name]
145		else:
146			index = index_or_name
147
148		if index < 1:
149			raise ValueError(f"Chord tone indices are 1-based (1 = root) — got {index}")
150
151		object.__setattr__(self, "index", index)
152		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:
155@dataclasses.dataclass(frozen=True)
156class Approach:
157
158	"""
159	A half-step approach into a target pitch at the next chord boundary.
160
161	Resolves at placement, one semitone below its target (the leading-tone
162	approach); a ``ChordTone`` target reads the NEXT chord through the
163	harmony window, so the approach lands as the harmony arrives.
164	"""
165
166	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:
288@dataclasses.dataclass(frozen=True)
289class MotifEvent:
290
291	"""
292	One timed note event inside a Motif.
293
294	``pitch`` is a specification: an absolute MIDI int, a drum name string,
295	a :class:`Degree`, :class:`ChordTone`, or :class:`Approach` — or None
296	for a pitch-stripped skeleton event (see :meth:`Motif.rhythm`), which
297	must be re-pitched via :meth:`Motif.pitched` before placement.
298	``velocity`` is an int or a ``(low, high)`` random-range tuple.
299
300	``origin`` names the drum a pitch was resolved *from*.  Only
301	:meth:`~subsequence.pattern_builder.PatternBuilder.capture` sets it —
302	capture reads notes back as absolute MIDI, so ``"kick"`` arrives here as
303	``36`` — and it is what lets the pitch-moving methods go on refusing a
304	drum they can no longer see.
305	"""
306
307	beat: float
308	pitch: PitchSpec
309	velocity: subsequence.declarations.VelocityValue = _DEFAULT_VELOCITY
310	duration: float = 0.25
311	probability: float = 1.0
312	origin: typing.Optional[str] = None		# Drum name a captured pitch was resolved from; None for anything authored directly.
313
314	def __post_init__ (self) -> None:
315
316		"""Validate ranges that are wrong at any placement."""
317
318		if self.duration <= 0:
319			raise ValueError(f"Event duration must be positive — got {self.duration}")
320		if not 0.0 <= self.probability <= 1.0:
321			raise ValueError(f"Event probability must be 0.0–1.0 — got {self.probability}")
322
323	def _sort_key (self) -> tuple:
324
325		"""Canonical ordering key — makes parallel merge order-independent."""
326
327		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.

origin names the drum a pitch was resolved from. Only ~subsequence.pattern_builder.PatternBuilder.capture() sets it — capture reads notes back as absolute MIDI, so "kick" arrives here as 36 — and it is what lets the pitch-moving methods go on refusing a drum they can no longer see.

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

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

is_concrete: bool
1137	@property
1138	def is_concrete (self) -> bool:
1139
1140		"""True when every span is key-independent (no romans/degrees)."""
1141
1142		return all(span.is_concrete for span in self.spans)

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

chords: Tuple[Any, ...]
1144	@property
1145	def chords (self) -> typing.Tuple[typing.Any, ...]:
1146
1147		"""The bare chords, one per span (concrete progressions only)."""
1148
1149		self._require_concrete("read .chords")
1150
1151		return tuple(span.chord for span in self.spans)

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

loops_on_exhaustion: bool
1153	@property
1154	def loops_on_exhaustion (self) -> bool:
1155
1156		"""True when the clock must loop rather than fall through to live stepping."""
1157
1158		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, ...]:
1190	def events (self) -> typing.Tuple[ChordEvent, ...]:
1191
1192		"""The realised timeline as a tuple (iteration, materialised)."""
1193
1194		return tuple(self)

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

def span_at( self, beat: float) -> Tuple[ChordSpan, float, float]:
1196	def span_at (self, beat: float) -> typing.Tuple[ChordSpan, float, float]:
1197
1198		"""Return ``(span, start, end)`` for the span sounding at *beat*.
1199
1200		*beat* wraps modulo the progression length, so the lookup also
1201		serves looped playback.
1202		"""
1203
1204		position = beat % self.length
1205		cursor = 0.0
1206
1207		for span in self.spans:
1208			if cursor <= position < cursor + span.beats:
1209				return span, cursor, cursor + span.beats
1210			cursor += span.beats
1211
1212		final = self.spans[-1]
1213		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:
1215	def resolve (self, key: typing.Union[str, int], scale: str = "ionian") -> "Progression":
1216
1217		"""Resolve every key-relative span against a key (name or pitch class)."""
1218
1219		key_pc = key if isinstance(key, int) else subsequence.chords.key_name_to_pc(key)
1220
1221		return dataclasses.replace(
1222			self,
1223			spans = tuple(span.resolve(key_pc, scale) for span in self.spans),
1224		)

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:
1226	@classmethod
1227	def generate (
1228		cls,
1229		style: typing.Union[str, typing.Any] = "functional_major",
1230		bars: int = 8,
1231		beats: typing.Union[float, typing.List[float]] = DEFAULT_SPAN_BEATS,
1232		*,
1233		key: typing.Optional[str] = None,
1234		scale: typing.Optional[str] = None,
1235		seed: typing.Optional[int] = None,
1236		rng: typing.Optional[random.Random] = None,
1237		pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
1238		end: typing.Optional[typing.Any] = None,
1239		avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
1240		cadence: typing.Optional[str] = None,
1241		dominant_7th: bool = True,
1242		gravity: float = 1.0,
1243		nir_strength: float = 0.5,
1244		minor_turnaround_weight: float = 0.0,
1245		root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1246	) -> "Progression":
1247
1248		"""Generate a progression from a chord-graph walk — the hybrid generator.
1249
1250		Full parameter pass-through to the engine (no more throwaway default
1251		engines), plus the hybrid constraints: ``pins`` fix chords at 1-based
1252		bars, ``end`` fixes the last bar, ``avoid`` excludes chords
1253		everywhere.  Constraints compile into the walk — a backward
1254		feasibility pass guarantees satisfiability before any chord is
1255		drawn (unsatisfiable constraints raise immediately), then a forward
1256		walk samples through the engine's real history-dependent weights
1257		(NIR, gravity, diversity keep their character).
1258
1259		**Without** ``key=`` the result is key-relative — the walk runs
1260		against a reference tonic and the spans store scale-proof
1261		major-relative romans, so the value prints meaningfully unbound and
1262		resolves wherever it is bound (the walk itself is key-invariant).
1263		**With** ``key=`` the result is concrete.
1264
1265		Parameters:
1266			style: A chord-graph style name (or ``ChordGraph`` instance).
1267			bars: How many chords to generate.
1268			beats: Span length per chord — a scalar, or a list cycled.
1269			key: Key for a concrete result; omit for a key-relative value.
1270			scale: Scale for int constraints' quality inference (e.g.
1271				``end=1``).  Defaults from the style (aeolian_minor →
1272				minor); explicit strings (``"V"``, ``"bVII7"``) never
1273				need it.
1274			seed: Seed for the walk.  A standalone generated value without
1275				a seed warns — module-level nondeterminism breaks live
1276				reload.
1277			rng: An explicit random stream (overrides ``seed``).
1278			pins: ``{bar: chord}`` — 1-based; values parse like progression
1279				elements (ints, romans, names, ``Chord``).
1280			end: The chord at the final bar — ``end="V"`` is the cadential
1281				major dominant in minor (a string because it is chromatic;
1282				no int can ask for it).
1283			avoid: Chords excluded from the walk.  Naming a chord outside
1284				the style's vocabulary is allowed (trivially satisfied).
1285			cadence: A cadence name (``"strong"``/``"soft"``/``"open"``/
1286				``"fakeout"``, theory aliases accepted) — its formula
1287				becomes pins on the final bars, so the walk *approaches*
1288				the close.  Conflicts with ``end=`` or pins on those bars.
1289			dominant_7th / gravity / nir_strength / minor_turnaround_weight /
1290				root_diversity: The engine parameters, exactly as
1291				:meth:`Composition.harmony` takes them.
1292
1293		Example:
1294			```python
1295			chorus = subsequence.Progression.generate(
1296				style="aeolian_minor", bars=4, end="V", seed=7,
1297			)
1298			print(chorus)        # romans until bound
1299			```
1300		"""
1301
1302		if bars < 1:
1303			raise ValueError("bars must be at least 1")
1304
1305		if cadence is not None:
1306			pins = cadence_pins(cadence, bars, pins, end)
1307			end = None
1308
1309		if rng is None:
1310			if seed is None:
1311				warnings.warn(
1312					"Progression.generate without seed= is nondeterministic — "
1313					"pass seed= so the value survives live reload",
1314					stacklevel = 2,
1315				)
1316				rng = random.Random()
1317			else:
1318				rng = random.Random(seed)
1319
1320		resolved_scale = scale if scale is not None else _STYLE_SCALES.get(style if isinstance(style, str) else "", "ionian")
1321		relative = key is None
1322		reference = key if key is not None else "C"
1323
1324		state = subsequence.harmonic_state.HarmonicState(
1325			key_name = reference,
1326			graph_style = style,
1327			include_dominant_7th = dominant_7th,
1328			key_gravity_blend = gravity,
1329			nir_strength = nir_strength,
1330			minor_turnaround_weight = minor_turnaround_weight,
1331			root_diversity = root_diversity,
1332			rng = rng,
1333		)
1334
1335		resolved_pins = {
1336			position: resolve_constraint(spec, state.key_root_pc, resolved_scale, f"pins[{position}]")
1337			for position, spec in (pins or {}).items()
1338		}
1339		resolved_end = resolve_constraint(end, state.key_root_pc, resolved_scale, "end") if end is not None else None
1340		resolved_avoid = [resolve_constraint(spec, state.key_root_pc, resolved_scale, "avoid") for spec in (avoid or [])]
1341
1342		if 1 in resolved_pins:
1343			if resolved_pins[1] not in state.graph.nodes():
1344				raise ValueError(
1345					f"pins[1]={resolved_pins[1].name()} is not in style {style!r}'s vocabulary"
1346				)
1347			state.current_chord = resolved_pins[1]
1348
1349		def commit (chosen: subsequence.chords.Chord) -> None:
1350			state.current_chord = chosen
1351
1352		walked = subsequence.sequence_utils.constrained_walk(
1353			state.graph,
1354			state.current_chord,
1355			bars,
1356			rng = state.rng,
1357			pins = resolved_pins,
1358			end = resolved_end,
1359			avoid = resolved_avoid,
1360			weight_modifier = state._transition_weight,
1361			before_choice = state._record_transition_source,
1362			after_choice = commit,
1363		)
1364
1365		lengths = _span_lengths(beats, bars)
1366
1367		if relative:
1368			return cls(spans = tuple(
1369				ChordSpan(chord = _roman_from_chord(chord, state.key_root_pc), beats = lengths[index])
1370				for index, chord in enumerate(walked)
1371			))
1372
1373		return cls(spans = tuple(
1374			ChordSpan(chord = chord, beats = lengths[index])
1375			for index, chord in enumerate(walked)
1376		))

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:
1411	def extend (self, *extensions: typing.Any, only: typing.Optional[typing.List[int]] = None) -> "Progression":
1412
1413		"""Add chord extensions (``7``/``9``/``11``/``13``/``"sus4"``/...) to every span.
1414
1415		``only=`` restricts the spice to the given 1-based chord slots.
1416		"""
1417
1418		slots = set(range(len(self.spans))) if only is None else {_check_slot(s, len(self.spans)) for s in only}
1419
1420		spans = tuple(
1421			dataclasses.replace(span, extensions = tuple(dict.fromkeys(span.extensions + extensions)))
1422			if index in slots else span
1423			for index, span in enumerate(self.spans)
1424		)
1425
1426		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:
1428	def inversions (self, spec: typing.Union[int, typing.List[int]]) -> "Progression":
1429
1430		"""Set chord inversions — a single int for all spans, or a list cycled per span."""
1431
1432		values = [spec] if isinstance(spec, int) else list(spec)
1433
1434		if not values:
1435			raise ValueError("inversions list is empty — pass at least one inversion")
1436
1437		spans = tuple(
1438			dataclasses.replace(span, inversion = int(values[index % len(values)]))
1439			for index, span in enumerate(self.spans)
1440		)
1441
1442		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:
1444	def spread (self, style: str) -> "Progression":
1445
1446		"""Set the voicing spread: ``"close"``, ``"open"`` (drop-2), or ``"wide"``."""
1447
1448		spans = tuple(dataclasses.replace(span, spread = None if style == "close" else style) for span in self.spans)
1449
1450		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:
1452	def over (self, bass: typing.Union[int, str], only: typing.Optional[typing.List[int]] = None) -> "Progression":
1453
1454		"""Put the progression over a slash/pedal bass — *the* trance/techno move.
1455
1456		*bass* is a pitch class int, a note name (``"G"``), or ``"tonic"``.  A
1457		note name is key-independent, so it resolves to its pitch class right
1458		here; ``"tonic"`` follows the key and stays relative until the
1459		progression is resolved.  ``only=`` restricts it to the given 1-based
1460		slots (slash chords rather than a full pedal).
1461		"""
1462
1463		if isinstance(bass, str) and bass != "tonic":
1464			bass = subsequence.chords.key_name_to_pc(bass)	# note names are key-independent — resolve now
1465		elif isinstance(bass, int) and not 0 <= bass <= 11:
1466			raise ValueError(f"a bass pitch class must be 0–11, got {bass}")
1467
1468		slots = set(range(len(self.spans))) if only is None else {_check_slot(s, len(self.spans)) for s in only}
1469
1470		spans = tuple(
1471			dataclasses.replace(span, bass=bass) if index in slots else span
1472			for index, span in enumerate(self.spans)
1473		)
1474
1475		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:
1477	def borrow (self, slot: typing.Union[int, typing.List[int]]) -> "Progression":
1478
1479		"""Borrow the chord(s) at the given 1-based slot(s) from the parallel scale.
1480
1481		Modal interchange for key-relative content: the degree re-resolves
1482		against the parallel mode (minor under a major scale and vice
1483		versa).  Concrete chords raise — there is nothing relative to borrow.
1484		"""
1485
1486		slots = {_check_slot(s, len(self.spans)) for s in ([slot] if isinstance(slot, int) else slot)}
1487
1488		spans = list(self.spans)
1489
1490		for index in slots:
1491			chord = spans[index].chord
1492			if not isinstance(chord, RomanChord):
1493				raise ValueError(
1494					f"slot {index + 1} holds a concrete chord ({spans[index].label()}) — "
1495					"borrow() needs key-relative content (an int degree or roman)"
1496				)
1497			spans[index] = dataclasses.replace(spans[index], chord = dataclasses.replace(chord, borrowed = not chord.borrowed))
1498
1499		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:
1501	def replace (self, slot: int, chord: typing.Any) -> "Progression":
1502
1503		"""Replace the chord at a 1-based slot (the span keeps its beats)."""
1504
1505		index = _check_slot(slot, len(self.spans))
1506		parsed = parse_element(chord, beats = self.spans[index].beats)
1507
1508		spans = self.spans[:index] + (parsed,) + self.spans[index + 1:]
1509
1510		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:
1512	def cadence (self, name: str = "strong") -> "Progression":
1513
1514		"""Substitute a cadence formula into the tail — the close, named.
1515
1516		The final spans take the formula's chords (``"strong"`` is V→I,
1517		``"soft"`` IV→I, ``"open"`` IV→V, ``"fakeout"`` V→vi; theory names —
1518		authentic, plagal, half, deceptive — work as aliases).  Each replaced
1519		span keeps its beats; its old chord and decorations go.  Formula
1520		chords are key-relative (ints follow the bound scale's qualities,
1521		``"V"`` is the major dominant by convention), so the tail resolves
1522		wherever the progression is bound — a concrete progression becomes
1523		mixed and resolves its tail at bind time, like any roman content.
1524
1525		Example::
1526
1527			verse = subsequence.progression(["Am", "F", "C", "G"]).cadence("open")
1528			# Bound in A minor: Am F Dm E — the half close, hanging on the dominant
1529
1530		Raises:
1531			ValueError: If the cadence name is unknown, or the progression
1532				has fewer spans than the formula.
1533		"""
1534
1535		spec = subsequence.cadences.cadence_formula(name)
1536		count = len(spec.formula)
1537
1538		if len(self.spans) < count:
1539			raise ValueError(
1540				f"cadence({name!r}) substitutes the last {count} chords, but this "
1541				f"progression has only {len(self.spans)}"
1542			)
1543
1544		tail = tuple(
1545			parse_element(element, beats = span.beats)
1546			for element, span in zip(spec.formula, self.spans[-count:])
1547		)
1548
1549		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:
1551	def with_rhythm (self, beats: typing.Union[float, typing.List[float]]) -> "Progression":
1552
1553		"""Reshape the harmonic rhythm — a scalar for all spans, or a list cycled per span."""
1554
1555		if isinstance(beats, bool):
1556			raise TypeError(f"with_rhythm takes beats or a list of beats, got bool: {beats!r}")
1557
1558		values = [float(beats)] if isinstance(beats, (int, float)) else [float(b) for b in beats]
1559
1560		if not values:
1561			raise ValueError("with_rhythm list is empty — pass at least one length")
1562
1563		spans = tuple(
1564			dataclasses.replace(span, beats = float(values[index % len(values)]))
1565			for index, span in enumerate(self.spans)
1566		)
1567
1568		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:
1570	def elaborate (self, depth: int = 1, seed: typing.Optional[int] = None) -> "Progression":
1571
1572		"""Steedman-inspired chord elaboration — approach each chord by fifths.
1573
1574		Implements the heart of Mark Steedman's generative grammar for
1575		jazz/blues chord sequences: every chord is **approached** by a chain
1576		of secondary dominants propagated backward around the cycle of fifths
1577		(Rule 3, "the perfect cadence propagated backward"), carved out of that
1578		chord's own span (Rule 1, metric subdivision).  ``depth`` is literally
1579		how many fifth-steps back the chain extends:
1580
1581		- ``depth=0`` — identity (the bare progression).
1582		- ``depth=1`` — a secondary dominant before each chord: ``[X]`` →
1583		  ``[V7/X, X]`` (e.g. a bar of C becomes G7 C).
1584		- ``depth=2`` — a secondary ii–V: ``[ii/X, V7/X, X]`` (Dm7 G7 C).
1585		- ``depth≥3`` — the chain extends (…V7/V7/X), the furthest-back chord
1586		  is made minor — the ``ii`` of *its own local dominant* (the next
1587		  link in the chain), forming a ii–V into that link, not the
1588		  target's own ii — and dominants are recoloured by **tritone
1589		  substitution** with even odds (Rule 4) for chromatic descents.
1590		  This tritone choice is the only nondeterministic part, so ``seed``
1591		  is taken (or warned) at depth ≥ 3.
1592
1593		Its flagship is the 12-bar blues with depth-per-chorus — elaborate a
1594		``"twelve_bar_blues"`` more each chorus and the ii–V turnarounds and
1595		tritone subs accumulate.
1596
1597		The progression must be **concrete** (resolved to rooted chords);
1598		the inserted dominants are computed by pitch-class arithmetic.  Each
1599		chord keeps its decorations on the final (resolved) sub-span; the
1600		inserted approach chords are bare dominant/minor sevenths.  Note that
1601		each span is divided into ``depth + 1`` equal sub-spans, so deep
1602		elaboration of a short harmonic rhythm can drop sub-spans below the
1603		harmony clock's lookahead floor — which raises at ``play()``/
1604		``render()`` if the result is bound to the global clock (it is free
1605		of that floor at the part level, ``p.progression()``).
1606
1607		Parameters:
1608			depth: Elaboration depth (≥ 0).
1609			seed: Seed for the depth-≥3 tritone-substitution choices.
1610
1611		Returns:
1612			A new :class:`Progression` with the approach chords inserted.
1613
1614		Raises:
1615			ValueError: If *depth* is negative, the progression is
1616				key-relative, or any span is a rootless
1617				:class:`PitchSet`.
1618
1619		Example:
1620			```python
1621			blues = subsequence.progression("twelve_bar_blues").resolve("C")
1622			chorus2 = blues.elaborate(2, seed=4)      # ii–V turnarounds throughout
1623			```
1624		"""
1625
1626		if depth < 0:
1627			raise ValueError("elaborate depth must be at least 0")
1628
1629		self._require_concrete("elaborate")
1630
1631		if depth == 0:
1632			return self
1633
1634		for span in self.spans:
1635			if isinstance(span.chord, PitchSet):
1636				raise ValueError("elaborate needs rooted chords — a PitchSet has no root to approach by fifths")
1637
1638		if depth >= 3 and seed is None:
1639			warnings.warn(
1640				"elaborate(depth>=3) makes tritone-substitution choices — pass seed= so the "
1641				"result survives live reload",
1642				stacklevel = 2,
1643			)
1644
1645		rng = random.Random(seed)
1646		new_spans: typing.List[ChordSpan] = []
1647
1648		for span in self.spans:
1649
1650			target_root = span.chord.root_pc
1651			sub_beats = span.beats / (depth + 1)
1652
1653			# The backward cycle-of-fifths chain, furthest-back first: chord j
1654			# sits a fifth above chord j-1's target, i.e. root = X + 7·j.  The
1655			# furthest-back (j == depth) is made minor — the ii of its OWN
1656			# local dominant (the next link), forming a ii–V into that link —
1657			# once the chain is long enough (depth >= 2) to spell one.
1658			for j in range(depth, 0, -1):
1659				root = (target_root + 7 * j) % 12
1660				quality = "minor_7th" if (j == depth and depth >= 2) else "dominant_7th"
1661
1662				# Tritone substitution recolours a dominant to the dom7 a
1663				# tritone away (same guide tones, chromatic resolution).
1664				if quality == "dominant_7th" and depth >= 3 and rng.random() < 0.5:
1665					root = (root + 6) % 12
1666
1667				new_spans.append(ChordSpan(chord = subsequence.chords.Chord(root_pc = root, quality = quality), beats = sub_beats))
1668
1669			# The target keeps its own chord and decorations, on its sub-span.
1670			new_spans.append(dataclasses.replace(span, beats = sub_beats))
1671
1672		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:
1676	def describe (self, key: typing.Optional[typing.Union[str, int]] = None, scale: str = "ionian") -> str:
1677
1678		"""A readable, one-chord-per-line summary.
1679
1680		Key-relative spans print as written (romans/degrees) when unbound,
1681		and as concrete chord names under a *key*.
1682		"""
1683
1684		key_pc = None if key is None else (key if isinstance(key, int) else subsequence.chords.key_name_to_pc(key))
1685
1686		lines = [f"Progression — {len(self.spans)} chords over {self.length:g} beats"]
1687		cursor = 0.0
1688
1689		for span in self.spans:
1690			lines.append(
1691				f"  {cursor:6.2f}{cursor + span.beats:6.2f}   "
1692				f"{span.label(key_pc, scale):<8} ({span.beats:g} beats)"
1693			)
1694			cursor += span.beats
1695
1696		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:
495@dataclasses.dataclass(frozen=True)
496class ChordSpan:
497
498	"""One chord with a duration and its decoration — the unit of harmonic time.
499
500	Decoration (extensions, slash bass, inversion, spread) lives HERE, never
501	on :class:`~subsequence.chords.Chord`: the engine's graph identity stays
502	the bare triad, and the decorated voicing is what patterns hear.
503
504	Attributes:
505		chord: A concrete ``Chord``, a key-relative :class:`RomanChord`, or a
506			:class:`PitchSet`.
507		beats: Span length in beats.
508		extensions: Extension markers — ints (``7``, ``9``, ``11``, ``13``)
509			or names (``"sus2"``, ``"sus4"``, ``"add9"``, ``"6"``).
510		bass: Slash/pedal bass — a pitch class int, a note name, or
511			``"tonic"`` (resolved against the key at query time).
512		inversion: Chord inversion for the voicing (0 = root position).
513		spread: Voicing spread — ``"close"`` (default), ``"open"`` (drop-2),
514			or ``"wide"`` (drop-2-and-4).
515		extension_intervals: Pre-computed semitone offsets for the
516			extensions, set by :meth:`Progression.resolve` for diatonic
517			degrees.  ``None`` means "derive from the chord's own colour".
518	"""
519
520	chord: typing.Any
521	beats: float
522	extensions: typing.Tuple[typing.Any, ...] = ()
523	bass: typing.Optional[typing.Union[int, str]] = None
524	inversion: int = 0
525	spread: typing.Optional[str] = None
526	extension_intervals: typing.Optional[typing.Tuple[int, ...]] = None
527
528	def __post_init__ (self) -> None:
529
530		"""Validate beats, extensions, and spread."""
531
532		if self.beats <= 0:
533			raise ValueError(f"a chord span must last at least one beat-fraction, got {self.beats:g}")
534
535		for extension in self.extensions:
536			if isinstance(extension, bool) or not (
537				(isinstance(extension, int) and extension in _NUMERIC_EXTENSIONS)
538				or (isinstance(extension, str) and extension in _EXTENSION_NAMES)
539			):
540				known = ", ".join(["7", "9", "11", "13"] + sorted(_EXTENSION_NAMES))
541				raise ValueError(f"unknown extension {extension!r} — expected one of: {known}")
542
543		if self.spread is not None and self.spread not in _SPREAD_STYLES:
544			raise ValueError(f"unknown spread {self.spread!r} — expected one of: " + ", ".join(sorted(_SPREAD_STYLES)))
545
546	@property
547	def is_concrete (self) -> bool:
548
549		"""True when the chord (and any pedal bass) needs no key context to sound.
550
551		A ``"tonic"`` pedal bass is key-relative, so a span carrying one is not
552		concrete until :meth:`resolve` pins it to a key.  Note-name basses are
553		resolved to a pitch class eagerly in :meth:`Progression.over`, so they
554		never linger here as strings.
555		"""
556
557		return not isinstance(self.chord, RomanChord) and not isinstance(self.bass, str)
558
559	@property
560	def is_decorated (self) -> bool:
561
562		"""True when the span carries any decoration beyond the bare chord."""
563
564		return bool(self.extensions) or self.bass is not None or self.inversion != 0 or self.spread is not None
565
566	def resolve (self, key_pc: int, scale: str = "ionian") -> "ChordSpan":
567
568		"""Return a concrete span: romans resolved, bass resolved to a pitch class."""
569
570		chord = self.chord
571		extension_intervals = self.extension_intervals
572
573		if isinstance(chord, RomanChord):
574			if chord.quality is None and any(isinstance(e, int) for e in self.extensions):
575				extension_intervals = chord.diatonic_extension_intervals(key_pc, scale, self.extensions)
576			chord = chord.resolve(key_pc, scale)
577
578		bass: typing.Optional[typing.Union[int, str]] = self.bass
579
580		if isinstance(bass, str):
581			if bass == "tonic":
582				bass = key_pc
583			else:
584				bass = subsequence.chords.key_name_to_pc(bass)
585
586		return dataclasses.replace(
587			self,
588			chord = chord,
589			bass = bass,
590			extension_intervals = extension_intervals,
591		)
592
593	def label (self, key_pc: typing.Optional[int] = None, scale: str = "ionian") -> str:
594
595		"""A printable chord label: roman text when relative, decorated name when concrete."""
596
597		if isinstance(self.chord, RomanChord):
598			if key_pc is None:
599				text = self.chord.label()
600				return text + self._decoration_suffix(resolved=False)
601			return self.resolve(key_pc, scale).label()
602
603		stacked_name = self._stacked_chord_name()
604
605		if stacked_name is not None:
606			return stacked_name + self._decoration_suffix(resolved=True, stacked=False)
607
608		base = str(self.chord.name())
609		return base + self._decoration_suffix(resolved=True)
610
611	def _stacked_chord_name (self) -> typing.Optional[str]:
612
613		"""The chord's printed name when a stacked extension changes which chord it is.
614
615		``extend(7)`` deepens a chord in its own colour, so C major gains a
616		*major* seventh — and has to print ``Cmaj7``, because ``C7`` names a
617		dominant seventh, a different chord.  Naming the result rather than
618		gluing the number onto the triad also keeps the leading-tone chord
619		honest: its diatonic seventh is half-diminished (``Bm7b5``), not the
620		fully diminished ``Bdim7``.
621
622		Returns ``None`` for shapes this cannot identify — a registered custom
623		quality, a pitch set — so the caller falls back to the plain suffix
624		rather than inventing a name.
625		"""
626
627		if not isinstance(self.chord, subsequence.chords.Chord):
628			return None
629
630		stacked = [e for e in self.extensions if isinstance(e, int) and e in _NUMERIC_EXTENSIONS]
631
632		if not stacked:
633			return None
634
635		intervals = list(self.chord.intervals())
636
637		if len(intervals) < 3:
638			return None
639
640		# Mirror decorated_intervals(): a sus extension replaces the third, so
641		# the shape has to be read after that substitution, not before.
642		sus = [e for e in self.extensions if e in ("sus2", "sus4")]
643
644		if sus:
645			intervals[1] = 2 if sus[0] == "sus2" else 5
646
647		shape = _TRIAD_SHAPES.get((intervals[1], intervals[2]))
648
649		if shape is None:
650			return None
651
652		# 9 implies 7 (and so on up): the highest stacked extension names the chord.
653		top = max(stacked)
654		seventh = next((i for i in self.decorated_intervals() if i in (9, 10, 11)), None)
655		root_name = subsequence.chords.PC_TO_NOTE_NAME[self.chord.root_pc % 12]
656
657		if shape == "diminished":
658			tail = f"m{top}b5" if seventh == 10 else f"dim{top}"
659
660		elif shape == "minor":
661			tail = f"mMaj{top}" if seventh == 11 else f"m{top}"
662
663		elif shape == "augmented":
664			tail = f"+maj{top}" if seventh == 11 else f"+{top}"
665
666		elif shape == "major":
667			tail = f"maj{top}" if seventh == 11 else str(top)
668
669		else:
670			# Suspensions follow the number (C7sus4).  One that arrived as an
671			# extension is printed by the extension loop below, so only a
672			# suspended *quality* spells itself here.
673			tail = str(top) if sus else f"{top}{shape}"
674
675		return root_name + tail
676
677	def _decoration_suffix (self, resolved: bool, stacked: bool = True) -> str:
678
679		"""The printable decoration tail (extensions and slash bass).
680
681		``stacked=False`` leaves the numeric extension out, for when
682		:meth:`_stacked_chord_name` has already spelled it into the name.
683		"""
684
685		parts = ""
686		numeric = sorted(e for e in self.extensions if isinstance(e, int))
687
688		# 9 implies 7 (and so on up): print only the highest stacked extension.
689		stacked_extensions = [e for e in numeric if e in _NUMERIC_EXTENSIONS]
690		if stacked and stacked_extensions:
691			parts += str(stacked_extensions[-1])
692
693		for name in (e for e in self.extensions if isinstance(e, str)):
694			parts += name
695
696		if self.bass is not None:
697			if isinstance(self.bass, int):
698				parts += "/" + subsequence.chords.PC_TO_NOTE_NAME[self.bass % 12]
699			else:
700				parts += "/" + str(self.bass)
701
702		return parts
703
704	def decorated_intervals (self) -> typing.List[int]:
705
706		"""Semitone offsets of the decorated voicing (before inversion/spread/bass).
707
708		Numeric extensions deepen the chord in its own colour — a minor third
709		gets a minor seventh, a major third a major seventh, a diminished
710		triad a diminished seventh.  Diatonic degrees extended with
711		``extend(...)`` carry pre-computed scale-true intervals instead (so V
712		gets its dominant seventh).  Write ``"G7"``/``"V7"`` when you want the
713		dominant colour on a concrete major chord.
714		"""
715
716		if isinstance(self.chord, RomanChord):
717			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
718
719		intervals = list(self.chord.intervals())
720
721		sus = [e for e in self.extensions if e in ("sus2", "sus4")]
722		if sus and len(intervals) >= 2:
723			intervals[1] = 2 if sus[0] == "sus2" else 5
724
725		numeric = sorted(e for e in self.extensions if isinstance(e, int))
726
727		if self.extension_intervals is not None:
728			added: typing.List[int] = list(self.extension_intervals)
729		else:
730			added = []
731			third = intervals[1] if len(intervals) >= 2 else None
732			has_seventh = any(i in (9, 10, 11) for i in intervals)
733			stacked = [e for e in numeric if e in _NUMERIC_EXTENSIONS]
734
735			if stacked and not has_seventh:
736				if third == 3 and len(intervals) >= 3 and intervals[2] == 6:
737					added.append(9)		# diminished colour
738				elif third == 3:
739					added.append(10)	# minor colour
740				elif third == 4:
741					added.append(11)	# major colour
742				else:
743					added.append(10)	# sus / no third: the dominant-leaning seventh
744
745			for extension in stacked:
746				if extension == 9:
747					added.append(14)
748				elif extension == 11:
749					added.append(17)
750				elif extension == 13:
751					added.append(21)
752
753		if "add9" in self.extensions:
754			added.append(14)
755		if "6" in self.extensions:
756			added.append(9)
757
758		return sorted(set(intervals) | set(added))
759
760	def tones (self, root: int = 60, count: typing.Optional[int] = None) -> typing.List[int]:
761
762		"""MIDI notes of the decorated voicing nearest *root* (concrete spans only).
763
764		Applies, in order: extensions, inversion, spread, then the slash/pedal
765		bass below the voicing.  ``PitchSet`` spans return their absolute
766		pitches (decoration other than ``count`` does not apply).
767		"""
768
769		if isinstance(self.chord, RomanChord):
770			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
771
772		if isinstance(self.chord, PitchSet):
773			return self.chord.tones(root, inversion=self.inversion, count=count)
774
775		intervals = self.decorated_intervals()
776
777		if self.inversion != 0:
778			intervals = subsequence.voicings.invert_chord(intervals, self.inversion)
779
780		if self.spread == "open" and len(intervals) >= 3:
781			intervals = sorted(intervals[:-2] + [intervals[-2] - 12] + intervals[-1:])
782		elif self.spread == "wide" and len(intervals) >= 3:
783			dropped = [i - 12 if position in (len(intervals) - 2, len(intervals) - 4) else i for position, i in enumerate(intervals)]
784			intervals = sorted(dropped)
785
786		offset = (self.chord.root_pc - root) % 12
787		if offset > 6:
788			offset -= 12
789		effective_root = root + offset
790
791		if count is not None:
792			n = len(intervals)
793			span_octave = max(12, ((max(intervals) // 12) + 1) * 12)
794			pitches = [effective_root + intervals[i % n] + span_octave * (i // n) for i in range(count)]
795		else:
796			pitches = [effective_root + interval for interval in intervals]
797
798		if self.bass is not None and isinstance(self.bass, int):
799			lowest = min(pitches)
800			bass_note = lowest - ((lowest - self.bass) % 12)
801			if bass_note == lowest:
802				bass_note -= 12
803			pitches = [bass_note] + pitches
804
805		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
546	@property
547	def is_concrete (self) -> bool:
548
549		"""True when the chord (and any pedal bass) needs no key context to sound.
550
551		A ``"tonic"`` pedal bass is key-relative, so a span carrying one is not
552		concrete until :meth:`resolve` pins it to a key.  Note-name basses are
553		resolved to a pitch class eagerly in :meth:`Progression.over`, so they
554		never linger here as strings.
555		"""
556
557		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
559	@property
560	def is_decorated (self) -> bool:
561
562		"""True when the span carries any decoration beyond the bare chord."""
563
564		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:
566	def resolve (self, key_pc: int, scale: str = "ionian") -> "ChordSpan":
567
568		"""Return a concrete span: romans resolved, bass resolved to a pitch class."""
569
570		chord = self.chord
571		extension_intervals = self.extension_intervals
572
573		if isinstance(chord, RomanChord):
574			if chord.quality is None and any(isinstance(e, int) for e in self.extensions):
575				extension_intervals = chord.diatonic_extension_intervals(key_pc, scale, self.extensions)
576			chord = chord.resolve(key_pc, scale)
577
578		bass: typing.Optional[typing.Union[int, str]] = self.bass
579
580		if isinstance(bass, str):
581			if bass == "tonic":
582				bass = key_pc
583			else:
584				bass = subsequence.chords.key_name_to_pc(bass)
585
586		return dataclasses.replace(
587			self,
588			chord = chord,
589			bass = bass,
590			extension_intervals = extension_intervals,
591		)

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

def label(self, key_pc: Optional[int] = None, scale: str = 'ionian') -> str:
593	def label (self, key_pc: typing.Optional[int] = None, scale: str = "ionian") -> str:
594
595		"""A printable chord label: roman text when relative, decorated name when concrete."""
596
597		if isinstance(self.chord, RomanChord):
598			if key_pc is None:
599				text = self.chord.label()
600				return text + self._decoration_suffix(resolved=False)
601			return self.resolve(key_pc, scale).label()
602
603		stacked_name = self._stacked_chord_name()
604
605		if stacked_name is not None:
606			return stacked_name + self._decoration_suffix(resolved=True, stacked=False)
607
608		base = str(self.chord.name())
609		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]:
704	def decorated_intervals (self) -> typing.List[int]:
705
706		"""Semitone offsets of the decorated voicing (before inversion/spread/bass).
707
708		Numeric extensions deepen the chord in its own colour — a minor third
709		gets a minor seventh, a major third a major seventh, a diminished
710		triad a diminished seventh.  Diatonic degrees extended with
711		``extend(...)`` carry pre-computed scale-true intervals instead (so V
712		gets its dominant seventh).  Write ``"G7"``/``"V7"`` when you want the
713		dominant colour on a concrete major chord.
714		"""
715
716		if isinstance(self.chord, RomanChord):
717			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
718
719		intervals = list(self.chord.intervals())
720
721		sus = [e for e in self.extensions if e in ("sus2", "sus4")]
722		if sus and len(intervals) >= 2:
723			intervals[1] = 2 if sus[0] == "sus2" else 5
724
725		numeric = sorted(e for e in self.extensions if isinstance(e, int))
726
727		if self.extension_intervals is not None:
728			added: typing.List[int] = list(self.extension_intervals)
729		else:
730			added = []
731			third = intervals[1] if len(intervals) >= 2 else None
732			has_seventh = any(i in (9, 10, 11) for i in intervals)
733			stacked = [e for e in numeric if e in _NUMERIC_EXTENSIONS]
734
735			if stacked and not has_seventh:
736				if third == 3 and len(intervals) >= 3 and intervals[2] == 6:
737					added.append(9)		# diminished colour
738				elif third == 3:
739					added.append(10)	# minor colour
740				elif third == 4:
741					added.append(11)	# major colour
742				else:
743					added.append(10)	# sus / no third: the dominant-leaning seventh
744
745			for extension in stacked:
746				if extension == 9:
747					added.append(14)
748				elif extension == 11:
749					added.append(17)
750				elif extension == 13:
751					added.append(21)
752
753		if "add9" in self.extensions:
754			added.append(14)
755		if "6" in self.extensions:
756			added.append(9)
757
758		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]:
760	def tones (self, root: int = 60, count: typing.Optional[int] = None) -> typing.List[int]:
761
762		"""MIDI notes of the decorated voicing nearest *root* (concrete spans only).
763
764		Applies, in order: extensions, inversion, spread, then the slash/pedal
765		bass below the voicing.  ``PitchSet`` spans return their absolute
766		pitches (decoration other than ``count`` does not apply).
767		"""
768
769		if isinstance(self.chord, RomanChord):
770			raise ValueError("cannot voice a key-relative span — resolve(key=...) it first")
771
772		if isinstance(self.chord, PitchSet):
773			return self.chord.tones(root, inversion=self.inversion, count=count)
774
775		intervals = self.decorated_intervals()
776
777		if self.inversion != 0:
778			intervals = subsequence.voicings.invert_chord(intervals, self.inversion)
779
780		if self.spread == "open" and len(intervals) >= 3:
781			intervals = sorted(intervals[:-2] + [intervals[-2] - 12] + intervals[-1:])
782		elif self.spread == "wide" and len(intervals) >= 3:
783			dropped = [i - 12 if position in (len(intervals) - 2, len(intervals) - 4) else i for position, i in enumerate(intervals)]
784			intervals = sorted(dropped)
785
786		offset = (self.chord.root_pc - root) % 12
787		if offset > 6:
788			offset -= 12
789		effective_root = root + offset
790
791		if count is not None:
792			n = len(intervals)
793			span_octave = max(12, ((max(intervals) // 12) + 1) * 12)
794			pitches = [effective_root + intervals[i % n] + span_octave * (i // n) for i in range(count)]
795		else:
796			pitches = [effective_root + interval for interval in intervals]
797
798		if self.bass is not None and isinstance(self.bass, int):
799			lowest = min(pitches)
800			bass_note = lowest - ((lowest - self.bass) % 12)
801			if bass_note == lowest:
802				bass_note -= 12
803			pitches = [bass_note] + pitches
804
805		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:
1728def progression (
1729	source: typing.Optional[typing.Any] = None,
1730	beats: typing.Union[float, typing.List[float]] = DEFAULT_SPAN_BEATS,
1731	*,
1732	style: typing.Optional[str] = None,
1733	bars: int = 8,
1734	key: typing.Optional[str] = None,
1735	scale: typing.Optional[str] = None,
1736	seed: typing.Optional[int] = None,
1737	rng: typing.Optional[random.Random] = None,
1738	pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
1739	end: typing.Optional[typing.Any] = None,
1740	avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
1741	cadence: typing.Optional[str] = None,
1742	dominant_7th: bool = True,
1743	gravity: float = 1.0,
1744	nir_strength: float = 0.5,
1745	minor_turnaround_weight: float = 0.0,
1746	root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1747) -> Progression:
1748
1749	"""Build a :class:`Progression` — the lowercase factory.
1750
1751	Dispatch by argument type: a **list** parses per element (ints where
1752	diatonic, name/roman strings where nominal/chromatic,
1753	``(element, beats)`` tuples for per-chord durations); a bare **string** names a
1754	preset from the curated table; ``style=`` generates *bars* chords from a
1755	chord-graph walk (requires ``key=``).
1756
1757	Parameters:
1758		source: The element list, preset name, or an existing Progression
1759			(returned unchanged).
1760		beats: Span length per chord — a scalar, or a list cycled per chord
1761			(``beats=[4, 4, 2, 6]`` shapes the harmonic rhythm).
1762		style: A chord-graph style name to generate from (e.g.
1763			``"aeolian_minor"``).
1764		bars: How many chords to generate (style mode only).
1765		key: Key for style generation.
1766		seed: Seed for style generation.  A standalone generated value
1767			without a seed warns — module-level nondeterminism breaks live
1768			reload.
1769		rng: An explicit random stream (overrides ``seed``; used by
1770			engine-mediated calls).
1771		dominant_7th / gravity / nir_strength: Graph-walk parameters,
1772			matching :meth:`Composition.harmony` (style mode only; full
1773			pass-through arrives with ``Progression.generate``).
1774
1775	Example:
1776		```python
1777		verse = subsequence.progression([1, 6, 3, 7])           # i–VI–III–VII in A minor
1778		blues = subsequence.progression(["I7"] * 4 + ["IV7", "IV7", "I7", "I7", "V7", "IV7", "I7", "I7"])
1779		walk  = subsequence.progression(style="aeolian_minor", key="A", bars=8, seed=3)
1780		```
1781	"""
1782
1783	if style is not None:
1784		if source is not None:
1785			raise ValueError("pass either source or style=, not both")
1786		return Progression.generate(
1787			style = style,
1788			bars = bars,
1789			beats = beats,
1790			key = key,
1791			scale = scale,
1792			seed = seed,
1793			rng = rng,
1794			pins = pins,
1795			end = end,
1796			avoid = avoid,
1797			cadence = cadence,
1798			dominant_7th = dominant_7th,
1799			gravity = gravity,
1800			nir_strength = nir_strength,
1801			minor_turnaround_weight = minor_turnaround_weight,
1802			root_diversity = root_diversity,
1803		)
1804
1805	# Generation-only knobs are meaningless for a concrete source — reject
1806	# them so a musician asking for cadence= or key= on a list gets a usable
1807	# error instead of a silent no-op.
1808	generation_only = {
1809		"bars": bars != 8,
1810		"key": key is not None,
1811		"scale": scale is not None,
1812		"seed": seed is not None,
1813		"rng": rng is not None,
1814		"pins": pins is not None,
1815		"end": end is not None,
1816		"avoid": avoid is not None,
1817		"cadence": cadence is not None,
1818		"dominant_7th": dominant_7th is not True,
1819		"gravity": gravity != 1.0,
1820		"nir_strength": nir_strength != 0.5,
1821		"minor_turnaround_weight": minor_turnaround_weight != 0.0,
1822		"root_diversity": root_diversity != subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY,
1823	}
1824	passed = [name for name, was_set in generation_only.items() if was_set]
1825
1826	if passed:
1827		raise ValueError(
1828			f"{', '.join(sorted(passed))} only apply when generating with style=. "
1829			"A concrete progression takes these as methods instead — e.g. "
1830			".cadence('strong') for the close, and the key binds at "
1831			"composition.harmony() / resolve() time."
1832		)
1833
1834	if isinstance(source, Progression):
1835		return source
1836
1837	if isinstance(source, str):
1838		if source in _PRESETS:
1839			return progression(_PRESETS[source], beats=beats)
1840		known = ", ".join(sorted(_PRESETS))
1841		raise ValueError(
1842			f"Unknown progression preset {source!r}. Known presets: {known}. "
1843			"Or pass a list — progression([1, 6, 3, 7]) / progression(['Am', 'F', 'C', 'G'])."
1844		)
1845
1846	if source is None:
1847		raise ValueError("progression() needs a source list (or style=...)")
1848
1849	elements = list(source)
1850
1851	if not elements:
1852		raise ValueError("progression list is empty — pass at least one chord")
1853
1854	lengths = _span_lengths(beats, len(elements))
1855
1856	return Progression(spans = tuple(
1857		parse_element(element, beats=lengths[index])
1858		for index, element in enumerate(elements)
1859	))

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]:
3130def sieve (
3131	classes: typing.Sequence[typing.Tuple[int, int]],
3132	hi: int,
3133	lo: int = 0,
3134) -> typing.List[int]:
3135
3136	"""Xenakis sieve: the sorted integers in ``[lo, hi)`` in any of the classes.
3137
3138	A sieve (Xenakis's *crible*) is a logical formula over **residual
3139	classes** that denotes a subset of the integers.  This primary form takes
3140	a list of ``(modulus, residue)`` pairs and returns their **union** over a
3141	bounded range — every ``x`` in ``[lo, hi)`` with ``x % modulus == residue``
3142	for at least one class.  The integers index *any* ordered parameter, so
3143	one kernel builds custom scales (over 0–11 semitones), non-octave pitch
3144	pools, rhythm grids, and bar-selection masks.
3145
3146	For intersection and complement, compose :func:`residual_class` objects
3147	with ``&``, ``|``, ``~`` and evaluate the result (see :class:`Sieve`).
3148
3149	Parameters:
3150		classes: ``(modulus, residue)`` pairs.  ``modulus`` must be ≥ 1; the
3151			residue is taken modulo the modulus.
3152		hi: Exclusive upper bound.
3153		lo: Inclusive lower bound (default 0).
3154
3155	Returns:
3156		The sorted, de-duplicated integers in range that satisfy any class.
3157
3158	Raises:
3159		ValueError: If a modulus is below 1.
3160
3161	Example:
3162		```python
3163		sieve([(12, 0), (12, 2), (12, 4), (12, 5), (12, 7), (12, 9), (12, 11)], hi=12)
3164		# → [0, 2, 4, 5, 7, 9, 11]  — the major scale as a sieve
3165		sieve([(2, 0)], hi=12)          # → [0, 2, 4, 6, 8, 10]  — whole-tone
3166		sieve([(5, 0), (7, 1)], lo=60, hi=96)   # a non-octave pitch pool
3167		```
3168	"""
3169
3170	for modulus, _residue in classes:
3171		if modulus < 1:
3172			raise ValueError(f"sieve modulus must be at least 1 — got {modulus}")
3173
3174	hits = {
3175		x
3176		for x in range(lo, hi)
3177		for modulus, residue in classes
3178		if x % modulus == residue % modulus
3179	}
3180
3181	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:
3241def residual_class (modulus: int, residue: int) -> Sieve:
3242
3243	"""A single residual class ``{x : x % modulus == residue}`` as a :class:`Sieve`.
3244
3245	The atom of sieve algebra (Xenakis's notation ``modulus @ residue``).
3246	Combine with ``&`` ``|`` ``~`` and call :meth:`Sieve.evaluate`.
3247	"""
3248
3249	if modulus < 1:
3250		raise ValueError(f"residual-class modulus must be at least 1 — got {modulus}")
3251
3252	reduced = residue % modulus
3253
3254	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().

@dataclasses.dataclass(frozen=True)
class PlacedNote:
100@dataclasses.dataclass (frozen=True)
101class PlacedNote:
102
103	"""
104	One note read back off a pattern being built — see ``PatternBuilder.placed()``.
105
106	A read-only copy rather than a view: a consumer diffing what a generator
107	added must not be able to reach through the answer and edit the pattern.
108	Frozen also makes it hashable, so ``set(after) - set(before)`` works.
109
110	Positions and durations are in **pulses**, the unit the pattern stores and
111	the one every grid classification already uses (``PatternBuilder.thin()``
112	documents that zone arithmetic).  Handing back beats would mean a float
113	divide and a rounding rule that could disagree with the caller's on a note
114	groove has nudged; a caller wanting beats divides by a constant and loses
115	nothing.
116
117	``duration`` is None for a drone, which has no end until a later
118	``drone_off()`` places one.
119
120	``index`` exists so two notes that are otherwise identical stay distinct:
121	nothing stops a hand-placed kick and a generated one landing on the same
122	pulse, and without it a set difference would report the second as already
123	present.  It is an identity token, not a count — treat it as opaque.
124	"""
125
126	position: int						# Pulse position within the pattern
127	pitch: int							# Resolved MIDI note number
128	origin: typing.Optional[str]		# Original drum-name string (same contract as Note.origin), None for numeric pitches
129	index: int							# Distinguishes notes sharing a position and pitch; opaque, stable only within one build
130	velocity: int
131	duration: typing.Optional[int]		# Pulses, or None for a drone (a raw Note On with no end)
132	primary_unmapped: bool = False		# True when this pitch is a placeholder that the primary device will not sound (see Note.primary_unmapped)

One note read back off a pattern being built — see PatternBuilder.placed().

A read-only copy rather than a view: a consumer diffing what a generator added must not be able to reach through the answer and edit the pattern. Frozen also makes it hashable, so set(after) - set(before) works.

Positions and durations are in pulses, the unit the pattern stores and the one every grid classification already uses (PatternBuilder.thin() documents that zone arithmetic). Handing back beats would mean a float divide and a rounding rule that could disagree with the caller's on a note groove has nudged; a caller wanting beats divides by a constant and loses nothing.

duration is None for a drone, which has no end until a later drone_off() places one.

index exists so two notes that are otherwise identical stay distinct: nothing stops a hand-placed kick and a generated one landing on the same pulse, and without it a set difference would report the second as already present. It is an identity token, not a count — treat it as opaque.

PlacedNote( position: int, pitch: int, origin: Optional[str], index: int, velocity: int, duration: Optional[int], primary_unmapped: bool = False)
position: int
pitch: int
origin: Optional[str]
index: int
velocity: int
duration: Optional[int]
primary_unmapped: bool = False
def generators() -> List[Dict[str, Any]]:
555def generators () -> typing.List[typing.Dict[str, typing.Any]]:
556
557	"""Describe every generator Subsequence offers, as plain data.
558
559	The whole catalogue in one call, so a caller never has to hold its own list
560	of what exists.  Each entry is exactly what :func:`describe_generator`
561	returns for that name.
562
563	Example:
564		```python
565		import subsequence
566
567		for shape in subsequence.generators():
568			print(shape["name"], len(shape["parameters"]))
569		```
570	"""
571
572	return [describe_generator(name) for name in GENERATORS]

Describe every generator Subsequence offers, as plain data.

The whole catalogue in one call, so a caller never has to hold its own list of what exists. Each entry is exactly what describe_generator() returns for that name.

Example:
import subsequence

for shape in subsequence.generators():
        print(shape["name"], len(shape["parameters"]))
def describe_generator(name: str) -> Dict[str, Any]:
471def describe_generator (name: str) -> typing.Dict[str, typing.Any]:
472
473	"""Describe one generator's parameters as plain data.
474
475	Parameters:
476		name: The generator's method name on ``PatternBuilder``, e.g.
477			``"ghost_fill"``.
478
479	Returns:
480		A dict with ``name``, ``summary``, ``partial``, ``parameters`` and
481		``dropped`` — see this module's contract.  Each parameter says whether
482		it is ``required`` and what it defaults to.
483
484	Raises:
485		ValueError: if *name* is not a declared generator.  A transform is
486			named as such rather than reported missing, since the two
487			catalogues are easy to confuse and the answer is one call away.
488
489	Example:
490		```python
491		import subsequence
492
493		shape = subsequence.describe_generator("euclidean")
494		shape["parameters"][0]["kind"]     # 'pitch'
495		```
496	"""
497
498	if name not in GENERATORS:
499
500		if name in TRANSFORMS:
501			raise ValueError(
502				f"{name!r} is a transform, not a generator — "
503				f"use subsequence.describe_transform({name!r})."
504			)
505
506		raise ValueError(
507			f"{name!r} is not a declared generator. "
508			f"Use subsequence.generators() to see the {len(GENERATORS)} available."
509		)
510
511	return _describe(name)

Describe one generator's parameters as plain data.

Arguments:
  • name: The generator's method name on PatternBuilder, e.g. "ghost_fill".
Returns:

A dict with name, summary, partial, parameters and dropped — see this module's contract. Each parameter says whether it is required and what it defaults to.

Raises:
  • ValueError: if name is not a declared generator. A transform is named as such rather than reported missing, since the two catalogues are easy to confuse and the answer is one call away.
Example:
import subsequence

shape = subsequence.describe_generator("euclidean")
shape["parameters"][0]["kind"]     # 'pitch'
def transforms() -> List[Dict[str, Any]]:
575def transforms () -> typing.List[typing.Dict[str, typing.Any]]:
576
577	"""Describe every transform Subsequence offers, as plain data.
578
579	The companion to :func:`generators`: those *place* notes, these *reshape*
580	notes already placed.  A surface that wants to roll a rhythm off the
581	downbeat asks here rather than holding its own list of method names.
582
583	Example:
584		```python
585		import subsequence
586
587		for shape in subsequence.transforms():
588			print(shape["name"], len(shape["parameters"]))
589		```
590	"""
591
592	return [_describe(name) for name in TRANSFORMS]

Describe every transform Subsequence offers, as plain data.

The companion to generators(): those place notes, these reshape notes already placed. A surface that wants to roll a rhythm off the downbeat asks here rather than holding its own list of method names.

Example:
import subsequence

for shape in subsequence.transforms():
        print(shape["name"], len(shape["parameters"]))
def describe_transform(name: str) -> Dict[str, Any]:
514def describe_transform (name: str) -> typing.Dict[str, typing.Any]:
515
516	"""Describe one transform's parameters as plain data.
517
518	Parameters:
519		name: The transform's method name on ``PatternBuilder``, e.g.
520			``"rotate"``.
521
522	Returns:
523		The same shape :func:`describe_generator` returns.  A transform is
524		applied the same way a generator is, so a caller that can drive one
525		can drive the other without a second code path.
526
527	Raises:
528		ValueError: if *name* is not a declared transform.
529
530	Example:
531		```python
532		import subsequence
533
534		shape = subsequence.describe_transform("rotate")
535		shape["parameters"][0]["name"]     # 'steps'
536		```
537	"""
538
539	if name not in TRANSFORMS:
540
541		if name in GENERATORS:
542			raise ValueError(
543				f"{name!r} is a generator, not a transform — "
544				f"use subsequence.describe_generator({name!r})."
545			)
546
547		raise ValueError(
548			f"{name!r} is not a declared transform. "
549			f"Use subsequence.transforms() to see the {len(TRANSFORMS)} available."
550		)
551
552	return _describe(name)

Describe one transform's parameters as plain data.

Arguments:
  • name: The transform's method name on PatternBuilder, e.g. "rotate".
Returns:

The same shape describe_generator() returns. A transform is applied the same way a generator is, so a caller that can drive one can drive the other without a second code path.

Raises:
  • ValueError: if name is not a declared transform.
Example:
import subsequence

shape = subsequence.describe_transform("rotate")
shape["parameters"][0]["name"]     # 'steps'