subsequence 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. subsequence/__init__.py +231 -0
  2. subsequence/__main__.py +24 -0
  3. subsequence/assets/web/index.html +345 -0
  4. subsequence/cadences.py +113 -0
  5. subsequence/chord_graphs/__init__.py +100 -0
  6. subsequence/chord_graphs/aeolian_minor.py +158 -0
  7. subsequence/chord_graphs/chromatic_mediant.py +113 -0
  8. subsequence/chord_graphs/diminished.py +97 -0
  9. subsequence/chord_graphs/dorian_minor.py +127 -0
  10. subsequence/chord_graphs/functional_major.py +102 -0
  11. subsequence/chord_graphs/hooktheory_major.py +88 -0
  12. subsequence/chord_graphs/lydian_major.py +130 -0
  13. subsequence/chord_graphs/mixolydian.py +98 -0
  14. subsequence/chord_graphs/phrygian_minor.py +76 -0
  15. subsequence/chord_graphs/suspended.py +109 -0
  16. subsequence/chord_graphs/turnaround_global.py +157 -0
  17. subsequence/chord_graphs/whole_tone.py +77 -0
  18. subsequence/chords.py +419 -0
  19. subsequence/composition.py +6099 -0
  20. subsequence/conductor.py +238 -0
  21. subsequence/constants/__init__.py +24 -0
  22. subsequence/constants/durations.py +37 -0
  23. subsequence/constants/instruments/__init__.py +13 -0
  24. subsequence/constants/instruments/gm_cc.py +46 -0
  25. subsequence/constants/instruments/gm_drums.py +53 -0
  26. subsequence/constants/instruments/gm_instruments.py +32 -0
  27. subsequence/constants/instruments/roland_tr8s.py +320 -0
  28. subsequence/constants/instruments/vermona_drm1_drums.py +87 -0
  29. subsequence/constants/midi_notes.py +29 -0
  30. subsequence/constants/pulses.py +17 -0
  31. subsequence/constants/velocity.py +22 -0
  32. subsequence/definitions.py +232 -0
  33. subsequence/display.py +617 -0
  34. subsequence/easing.py +347 -0
  35. subsequence/event_emitter.py +109 -0
  36. subsequence/form_state.py +665 -0
  37. subsequence/forms.py +257 -0
  38. subsequence/groove.py +323 -0
  39. subsequence/harmonic_rhythm.py +83 -0
  40. subsequence/harmonic_state.py +352 -0
  41. subsequence/harmony.py +197 -0
  42. subsequence/held_notes.py +91 -0
  43. subsequence/helpers/__init__.py +0 -0
  44. subsequence/helpers/network.py +58 -0
  45. subsequence/helpers/wing.py +430 -0
  46. subsequence/intervals.py +436 -0
  47. subsequence/keystroke.py +249 -0
  48. subsequence/link_clock.py +128 -0
  49. subsequence/live_client.py +187 -0
  50. subsequence/live_reloader.py +298 -0
  51. subsequence/live_server.py +161 -0
  52. subsequence/melodic_state.py +483 -0
  53. subsequence/midi.py +97 -0
  54. subsequence/midi_utils.py +329 -0
  55. subsequence/mini_notation.py +164 -0
  56. subsequence/motifs.py +2356 -0
  57. subsequence/osc.py +194 -0
  58. subsequence/pattern.py +363 -0
  59. subsequence/pattern_algorithmic.py +2010 -0
  60. subsequence/pattern_builder.py +2589 -0
  61. subsequence/pattern_midi.py +1208 -0
  62. subsequence/progressions.py +1913 -0
  63. subsequence/py.typed +0 -0
  64. subsequence/roles.py +63 -0
  65. subsequence/sequence_utils.py +3123 -0
  66. subsequence/sequencer.py +2086 -0
  67. subsequence/tuning.py +453 -0
  68. subsequence/voicings.py +151 -0
  69. subsequence/web_ui.py +337 -0
  70. subsequence/weighted_graph.py +156 -0
  71. subsequence-0.6.4.dist-info/METADATA +208 -0
  72. subsequence-0.6.4.dist-info/RECORD +78 -0
  73. subsequence-0.6.4.dist-info/WHEEL +5 -0
  74. subsequence-0.6.4.dist-info/entry_points.txt +2 -0
  75. subsequence-0.6.4.dist-info/licenses/LICENSE +661 -0
  76. subsequence-0.6.4.dist-info/scm_file_list.json +193 -0
  77. subsequence-0.6.4.dist-info/scm_version.json +8 -0
  78. subsequence-0.6.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2589 @@
1
+ """``PatternBuilder`` (the ``p`` inside a pattern) — the note-placement surface.
2
+
3
+ This is the ``p`` handed to every ``@composition.pattern`` function: the verbs
4
+ for placing notes, drums, chords, motifs and phrases, plus articulation, the
5
+ transforms, and the algorithmic and MIDI mixins it inherits. It renders into
6
+ the plain data types in ``pattern``.
7
+ """
8
+
9
+ import dataclasses
10
+ import logging
11
+ import random
12
+ import time
13
+ import typing
14
+
15
+ import pymididefs.rpn
16
+ import subsequence.chords
17
+ import subsequence.constants
18
+ import subsequence.constants.velocity
19
+ import subsequence.easing
20
+ import subsequence.groove
21
+ import subsequence.held_notes
22
+ import subsequence.intervals
23
+ import subsequence.pattern
24
+ import subsequence.motifs
25
+ import subsequence.sequence_utils
26
+ import subsequence.mini_notation
27
+ import subsequence.conductor
28
+ import subsequence.pattern_algorithmic
29
+ import subsequence.pattern_midi
30
+ import subsequence.progressions
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ def _expand_sequence_param (name: str, value: typing.Any, n: int) -> list:
36
+
37
+ """Expand a scalar to a list of length n, or adjust a list to length n.
38
+
39
+ Parameters:
40
+ name: The name of the parameter being expanded (used for logging).
41
+ value: A scalar (e.g., int, float, str) or an iterable to expand.
42
+ n: The target length for the returned list.
43
+
44
+ Returns:
45
+ A list of length ``n``. If ``value`` is a scalar, returns ``[value] * n``.
46
+ If ``value`` is a list longer than ``n``, truncates it and logs a warning.
47
+ If ``value`` is a list shorter than ``n``, repeats the last value and logs a warning.
48
+ """
49
+
50
+ if isinstance(value, (int, float, str)):
51
+ return [value] * n
52
+
53
+ result = list(value)
54
+
55
+ if len(result) == 0:
56
+ raise ValueError(f"sequence(): {name} list cannot be empty")
57
+
58
+ if len(result) > n:
59
+ logger.warning("sequence(): %s has %d values but only %d steps - truncating", name, len(result), n)
60
+ return result[:n]
61
+
62
+ if len(result) < n:
63
+ logger.warning("sequence(): %s has %d values but %d steps - repeating last value", name, len(result), n)
64
+ return result + [result[-1]] * (n - len(result))
65
+
66
+ return result
67
+
68
+
69
+ class BarCycle:
70
+
71
+ """Position of the current bar within a repeating cycle of bars.
72
+
73
+ Returned by :meth:`PatternBuilder.bar_cycle`. Provides readable, musician-friendly
74
+ properties for bar-position logic without raw modulo arithmetic.
75
+
76
+ Attributes:
77
+ bar: Zero-indexed bar within the cycle (0 … length−1).
78
+ length: The cycle length in bars passed to :meth:`PatternBuilder.bar_cycle`.
79
+ """
80
+
81
+ __slots__ = ("bar", "length")
82
+
83
+ def __init__ (self, bar: int, length: int) -> None:
84
+ self.bar = bar
85
+ self.length = length
86
+
87
+ @property
88
+ def first (self) -> bool:
89
+ """True on the first bar of the cycle (``bar == 0``)."""
90
+ return self.bar == 0
91
+
92
+ @property
93
+ def last (self) -> bool:
94
+ """True on the last bar of the cycle (``bar == length − 1``)."""
95
+ return self.bar == self.length - 1
96
+
97
+ @property
98
+ def progress (self) -> float:
99
+ """Fractional progress through the cycle: 0.0 on bar 0, rising each bar.
100
+
101
+ For a 4-bar cycle: 0.0, 0.25, 0.5, 0.75.
102
+ Useful for gradual intensity curves or as a noise/LFO seed.
103
+ """
104
+ return self.bar / self.length
105
+
106
+
107
+ class PatternBuilder(
108
+ subsequence.pattern_algorithmic.PatternAlgorithmicMixin,
109
+ subsequence.pattern_midi.PatternMidiMixin,
110
+ ):
111
+
112
+ """
113
+ The musician's 'palette' for creating musical content.
114
+
115
+ A ``PatternBuilder`` instance (commonly named ``p``) is passed to every
116
+ pattern function. It provides methods for placing notes, generating rhythms,
117
+ and transforming the resulting sequence (e.g., swinging, reversing, or transposing).
118
+
119
+ Rhythm in Subsequence is typically expressed in **beats** (where 1.0 is a
120
+ quarter note) or **steps** (subdivisions of a pattern).
121
+ """
122
+
123
+ def __init__ (self, pattern: subsequence.pattern.Pattern, cycle: int, conductor: typing.Optional[subsequence.conductor.Conductor] = None, drum_note_map: typing.Optional[typing.Dict[str, int]] = None, cc_name_map: typing.Optional[typing.Dict[str, int]] = None, nrpn_name_map: typing.Optional[typing.Dict[str, int]] = None, section: typing.Any = None, bar: int = 0, rng: typing.Optional[random.Random] = None, tweaks: typing.Optional[typing.Dict[str, typing.Any]] = None, default_grid: int = 16, data: typing.Optional[typing.Dict[str, typing.Any]] = None, key: typing.Optional[str] = None, scale: typing.Optional[str] = None, time_signature: typing.Tuple[int, int] = (4, 4), held_notes: typing.Optional[subsequence.held_notes.HeldNotes] = None, harmony: typing.Optional[typing.Any] = None, section_motifs: typing.Optional[typing.Dict[typing.Tuple[str, typing.Optional[str]], typing.Any]] = None, energy: float = 0.5) -> None:
124
+
125
+ """Initialize the builder with pattern context, cycle count, and optional section info.
126
+
127
+ Parameters:
128
+ pattern: The ``Pattern`` instance this builder populates.
129
+ cycle: Zero-based rebuild counter.
130
+ conductor: Optional ``Conductor`` for time-varying signals.
131
+ drum_note_map: Optional mapping of drum names to MIDI notes.
132
+ cc_name_map: Optional mapping of CC names to MIDI CC numbers.
133
+ nrpn_name_map: Optional mapping of NRPN parameter names to 14-bit
134
+ parameter numbers (0–16383). Used by ``p.nrpn()`` and
135
+ ``p.nrpn_ramp()`` for symbolic access — typically a
136
+ device-specific dictionary (e.g. Sequential Take 5's
137
+ ``Osc1FreqFine`` → 9).
138
+ section: Current ``SectionInfo`` (or ``None``).
139
+ bar: Global bar count.
140
+ rng: Optional seeded ``Random`` for reproducibility.
141
+ tweaks: Per-pattern overrides set via ``composition.tweak()``.
142
+ default_grid: Number of grid slots used by ``hit_steps()``,
143
+ ``sequence()``, and ``rotate()`` when no explicit ``grid``
144
+ is passed. Normally set automatically from the decorator's
145
+ ``beats``/``bars``/``steps`` and ``step_duration`` parameters.
146
+ data: Shared state dict from the parent ``Composition``
147
+ (same object as ``composition.data``). Read and write
148
+ via ``p.data`` for cross-pattern communication and
149
+ external data access. Patterns rebuild in definition
150
+ order; when two patterns share the same ``length``,
151
+ a writer defined earlier in source is guaranteed to
152
+ run before a reader defined later in the same cycle.
153
+ key: The composition's key (e.g. ``"C"``), used by ``p.progression()``
154
+ to generate chords from a graph style and by ``p.motif()`` to
155
+ resolve scale degrees. ``None`` when the composition has no
156
+ key set.
157
+ scale: The composition's scale/mode name (e.g. ``"minor"``),
158
+ read via ``p.scale`` and used to resolve scale degrees in
159
+ ``p.motif()``. ``None`` means ionian/major.
160
+ time_signature: The composition's time signature, read via
161
+ ``p.time_signature``; powers the metric-weight table.
162
+ section_motifs: Optional reference to the composition's
163
+ section-motif registry, read by ``p.section_motif()``.
164
+ harmony: Optional read-only harmony window view for this cycle
165
+ (``p.harmony``) — ``p.harmony.chord``, ``chord_at(beat)``,
166
+ ``next_chord``, ``until_change``. ``None`` until the
167
+ harmonic clock has published a window.
168
+ held_notes: Optional live held-note tracker from ``composition.note_input()``.
169
+ Read via ``p.held_notes()``. ``None`` when no note input was declared
170
+ (and when rendering headlessly), so the accessor returns an empty list.
171
+ energy: The current section's energy level (0.0–1.0), read via
172
+ ``p.energy`` — the arranging dial. 0.5 when no energy source
173
+ is configured.
174
+ """
175
+
176
+ self._pattern = pattern
177
+ self.cycle = cycle
178
+ self.conductor = conductor
179
+ self._drum_note_map = drum_note_map
180
+ self._cc_name_map = cc_name_map
181
+ self._nrpn_name_map = nrpn_name_map
182
+ self.section = section
183
+ self.bar = bar
184
+ self.rng: random.Random = rng or random.Random()
185
+ self._tweaks: typing.Dict[str, typing.Any] = tweaks or {}
186
+ self._default_grid: int = default_grid
187
+ self.data: typing.Dict[str, typing.Any] = data if data is not None else {}
188
+ self.key: typing.Optional[str] = key # composition key, for p.progression() chord generation
189
+ self.scale: typing.Optional[str] = scale # composition scale/mode, for degree resolution
190
+ self.time_signature: typing.Tuple[int, int] = time_signature
191
+ self.harmony: typing.Optional[typing.Any] = harmony # HarmonyView for this cycle, or None
192
+ self.energy: float = energy # current section's energy (the arranging dial)
193
+ self._section_motifs: typing.Optional[typing.Dict[typing.Tuple[str, typing.Optional[str]], typing.Any]] = section_motifs
194
+ self._held_notes: typing.Optional[subsequence.held_notes.HeldNotes] = held_notes
195
+ self._tuning_applied: bool = False # set by apply_tuning() to prevent double-apply
196
+
197
+ @property
198
+ def grid (self) -> int:
199
+ """Number of grid slots in this pattern (e.g. 16 for a 4-beat sixteenth-note pattern)."""
200
+ return self._default_grid
201
+
202
+ def _has_pitch_at_beat (self, pitch: typing.Union[int, str], beat: float) -> bool:
203
+ """Helper to check if a pitch is already sounding at a specific beat.
204
+
205
+ Tolerant of unmappable drum names: a name absent from this pattern's
206
+ ``drum_note_map`` can't already be sounding here, so it returns False
207
+ (the placement itself handles the drop/warn) rather than raising."""
208
+ if isinstance(pitch, str):
209
+ if self._drum_note_map is None or pitch not in self._drum_note_map:
210
+ return False
211
+ midi_pitch = self._drum_note_map[pitch]
212
+ else:
213
+ midi_pitch = pitch
214
+ pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
215
+ if pulse in self._pattern.steps:
216
+ return any(n.pitch == midi_pitch for n in self._pattern.steps[pulse].notes)
217
+ return False
218
+
219
+
220
+ @property
221
+ def c (self) -> typing.Optional[subsequence.conductor.Conductor]:
222
+
223
+ """Alias for self.conductor."""
224
+
225
+ return self.conductor
226
+
227
+ def signal (self, name: str) -> float:
228
+
229
+ """Read a conductor signal at the current bar.
230
+
231
+ Shorthand for ``p.c.get(name, p.bar * beats_per_bar)``, where
232
+ ``beats_per_bar`` comes from the composition's time signature —
233
+ so the signal is read at the beat this bar actually starts on,
234
+ in any metre. Returns 0.0 if no conductor is attached or the
235
+ signal is not defined.
236
+ """
237
+
238
+ if self.conductor is None:
239
+ return 0.0
240
+
241
+ return self.conductor.get(name, self.bar * float(self.time_signature[0]))
242
+
243
+ def held_notes (self) -> typing.List[int]:
244
+
245
+ """Return the MIDI notes currently held on the ``note_input`` keyboard.
246
+
247
+ The notes are sorted ascending. Pass the result straight to
248
+ ``p.arpeggio()`` to arpeggiate whatever the player is holding —
249
+ ``p.arpeggio(p.held_notes())`` rests when no keys are down. Returns
250
+ an empty list when no ``note_input()`` source was declared and when
251
+ rendering headlessly (so seeded output stays deterministic).
252
+
253
+ The set is sampled once per rebuild; ``note_input(release_ms=…)``
254
+ smooths the gap during hand-position changes so the arp does not drop
255
+ out, and ``note_input(latch=True)`` holds the chord until you play a
256
+ new one.
257
+ """
258
+
259
+ if self._held_notes is None:
260
+ return []
261
+
262
+ return self._held_notes.snapshot(time.perf_counter())
263
+
264
+ def param (self, name: str, default: typing.Any = None) -> typing.Any:
265
+
266
+ """Read a tweakable parameter for this pattern.
267
+
268
+ Returns the value set via ``composition.tweak()`` if one
269
+ exists, otherwise returns ``default``.
270
+
271
+ Parameters:
272
+ name: The parameter name.
273
+ default: The value to return if no tweak is active.
274
+
275
+ Example::
276
+
277
+ @composition.pattern(channel=1, beats=4)
278
+ def bass (p):
279
+ pitches = p.param("pitches", [60, 64, 67, 72])
280
+ p.sequence(steps=[0, 4, 8, 12], pitches=pitches)
281
+ """
282
+
283
+ return self._tweaks.get(name, default)
284
+
285
+ def set_length (self, length: float) -> "PatternBuilder":
286
+
287
+ """
288
+ Dynamically change the length of the pattern.
289
+
290
+ The new length takes effect immediately for any subsequent notes
291
+ placed in the current builder call, and will be used by the
292
+ sequencer for next cycle's scheduling.
293
+
294
+ Parameters:
295
+ length: New pattern length in beats (e.g., 4.0 for a bar).
296
+
297
+ Returns ``self`` for fluent chaining.
298
+ """
299
+
300
+ if length <= 0:
301
+ raise ValueError("Pattern length must be positive")
302
+
303
+ self._pattern.length = length
304
+ return self
305
+
306
+ def _resolve_pitch (self, pitch: typing.Union[int, str]) -> int:
307
+
308
+ """
309
+ Resolve a pitch value to a MIDI note number (strict).
310
+
311
+ Raises on an unknown drum name — the strict counterpart of
312
+ :meth:`_resolve_pitch_lenient`. Note-placement and transform methods use
313
+ the lenient variant, so a device may legitimately lack a voice others
314
+ have; this strict primitive is retained for parity with the sibling
315
+ ``_resolve_cc`` / ``_resolve_nrpn`` / ``_resolve_rpn`` name resolvers,
316
+ where an unknown name is always a configuration error.
317
+ """
318
+
319
+ if isinstance(pitch, int):
320
+ return pitch
321
+
322
+ if self._drum_note_map is None:
323
+ raise ValueError(f"String pitch '{pitch}' requires a drum_note_map, but none was provided")
324
+
325
+ if pitch not in self._drum_note_map:
326
+ raise ValueError(f"Unknown drum name '{pitch}' - not found in drum_note_map")
327
+
328
+ return self._drum_note_map[pitch]
329
+
330
+ def _resolve_hit_pitch (self, pitch: typing.Union[int, str]) -> typing.Optional[typing.Tuple[int, typing.Optional[str], bool]]:
331
+
332
+ """Resolve a step-note pitch for placement, leniently for named drums.
333
+
334
+ Returns ``(midi_pitch, origin, primary_unmapped)``, or ``None`` to drop
335
+ the hit entirely.
336
+
337
+ Unlike :meth:`_resolve_pitch`, an unknown drum *name* does not raise:
338
+ faithful-core device maps legitimately lack voices other devices have,
339
+ so a name a device can't voice is dropped rather than crashing the
340
+ pattern. The cases:
341
+
342
+ - Integer pitch → ``(pitch, None, False)``.
343
+ - String in this pattern's ``drum_note_map`` → ``(note, name, False)``.
344
+ - String absent here but present in a mirror's map →
345
+ ``(placeholder, name, True)``: the primary can't voice it, but a
346
+ symbolic mirror can (the placeholder pitch is used only by transforms
347
+ and display, never for playback — see ``Note.primary_unmapped``).
348
+ - String absent everywhere → warn once and return ``None`` (drop).
349
+ - String with **no** ``drum_note_map`` at all → still a configuration
350
+ error; raises (you forgot the map, this is not a capability gap).
351
+ """
352
+
353
+ if isinstance(pitch, int):
354
+ return (pitch, None, False)
355
+
356
+ if self._drum_note_map is None:
357
+ raise ValueError(f"String pitch '{pitch}' requires a drum_note_map, but none was provided")
358
+
359
+ if pitch in self._drum_note_map:
360
+ return (self._drum_note_map[pitch], pitch, False)
361
+
362
+ mirror_pitch = self._first_mirror_pitch(pitch)
363
+ if mirror_pitch is not None:
364
+ return (mirror_pitch, pitch, True)
365
+
366
+ self._warn_unknown_drum(pitch)
367
+ return None
368
+
369
+ def _first_mirror_pitch (self, name: str) -> typing.Optional[int]:
370
+
371
+ """Return the first mirror ``drum_note_map`` value for *name*, or None.
372
+
373
+ Lets a named hit absent from the primary map still be placed (as a
374
+ ``primary_unmapped`` Note) when a symbolic (3-tuple) mirror can voice it.
375
+ """
376
+
377
+ for entry in getattr(self._pattern, 'mirrors', []):
378
+ if len(entry) == 3 and entry[2] is not None and name in entry[2]:
379
+ return typing.cast(int, entry[2][name])
380
+ return None
381
+
382
+ def _warn_unknown_drum (self, name: str, include_mirrors: bool = True) -> None:
383
+
384
+ """Warn once (per pattern, per name) that a drum name maps to nothing.
385
+
386
+ Deduplicated via the Pattern's ``_warned_drum_names`` set so the per-bar
387
+ rebuild does not spam; a hot-reload builds a fresh Pattern and
388
+ re-surfaces the warning.
389
+
390
+ ``include_mirrors`` tailors the wording: step-note placement checks the
391
+ mirror maps too (the name maps to *no* device), whereas the methods that
392
+ resolve against the primary map only — drones, ``arpeggio``, ``evolve``,
393
+ ``branch``, and the ``thin``/``ratchet`` pitch filter — report just this
394
+ device.
395
+ """
396
+
397
+ warned = getattr(self._pattern, '_warned_drum_names', None)
398
+ if warned is not None:
399
+ if name in warned:
400
+ return
401
+ warned.add(name)
402
+
403
+ fn = getattr(self._pattern, '_builder_fn', None)
404
+ label = getattr(fn, '__name__', None)
405
+ where = f"pattern '{label}'" if label else f"device {self._pattern.device} channel {self._pattern.channel}"
406
+ if include_mirrors:
407
+ scope = f"the drum_note_map for {where} or any of its mirror destinations"
408
+ reason = "no device maps this voice"
409
+ else:
410
+ scope = f"the drum_note_map for {where}"
411
+ reason = "this device has no such voice"
412
+ logger.warning(f"Drum name '{name}' is not in {scope} — the note is dropped ({reason}). Check the spelling, or add it to a map.")
413
+
414
+ def _resolve_pitch_lenient (self, pitch: typing.Union[int, str]) -> typing.Optional[int]:
415
+
416
+ """Resolve a pitch against this pattern's own ``drum_note_map``, leniently.
417
+
418
+ Like :meth:`_resolve_pitch`, but an unknown drum *name* (a map is present
419
+ yet lacks the voice) is **dropped** — warned once, returns ``None`` —
420
+ instead of raising, so a device may legitimately lack a voice that other
421
+ devices have. Used by the methods that do NOT carry the drum name to
422
+ mirror destinations (``note_on``/``note_off``/``drone``, ``arpeggio``,
423
+ ``evolve``, ``branch``, and the ``thin``/``ratchet`` pitch filter), so
424
+ resolution is against the primary map only. A string with **no**
425
+ ``drum_note_map`` at all is still a configuration error and raises.
426
+ """
427
+
428
+ if isinstance(pitch, int):
429
+ return pitch
430
+
431
+ if self._drum_note_map is None:
432
+ raise ValueError(f"String pitch '{pitch}' requires a drum_note_map, but none was provided")
433
+
434
+ if pitch in self._drum_note_map:
435
+ return self._drum_note_map[pitch]
436
+
437
+ self._warn_unknown_drum(pitch, include_mirrors=False)
438
+ return None
439
+
440
+ def _resolve_cc (self, control: typing.Union[int, str]) -> int:
441
+
442
+ """Resolve a CC name or number to a MIDI CC number."""
443
+
444
+ if isinstance(control, int):
445
+ return control
446
+
447
+ if self._cc_name_map is None:
448
+ raise ValueError(f"String CC name '{control}' requires a cc_name_map, but none was provided")
449
+
450
+ if control not in self._cc_name_map:
451
+ raise ValueError(f"Unknown CC name '{control}' - not found in cc_name_map")
452
+
453
+ return self._cc_name_map[control]
454
+
455
+ def _resolve_nrpn (self, parameter: typing.Union[int, str]) -> int:
456
+
457
+ """Resolve an NRPN parameter name or number to a 14-bit parameter number.
458
+
459
+ Strings require an ``nrpn_name_map`` on the pattern decorator —
460
+ NRPN parameter numbers are vendor-specific, so subsequence does not
461
+ ship a default mapping. Integer parameters must be in the 14-bit
462
+ range 0–16383.
463
+ """
464
+
465
+ if isinstance(parameter, int):
466
+ if not 0 <= parameter <= 16383:
467
+ raise ValueError(f"NRPN parameter number must be 0–16383, got {parameter}")
468
+ return parameter
469
+
470
+ if self._nrpn_name_map is None:
471
+ raise ValueError(f"String NRPN name '{parameter}' requires an nrpn_name_map, but none was provided")
472
+
473
+ if parameter not in self._nrpn_name_map:
474
+ raise ValueError(f"Unknown NRPN name '{parameter}' - not found in nrpn_name_map")
475
+
476
+ return self._nrpn_name_map[parameter]
477
+
478
+ def _resolve_rpn (self, parameter: typing.Union[int, str]) -> int:
479
+
480
+ """Resolve an RPN parameter name or number to a 14-bit parameter number.
481
+
482
+ Strings fall back to ``pymididefs.rpn.RPN_MAP`` — the standardised
483
+ set of MIDI Registered Parameter Numbers (``pitch_bend_sensitivity``,
484
+ ``channel_fine_tuning``, ...). No per-pattern map needed. Integer
485
+ parameters must be in the 14-bit range 0–16383.
486
+ """
487
+
488
+ if isinstance(parameter, int):
489
+ if not 0 <= parameter <= 16383:
490
+ raise ValueError(f"RPN parameter number must be 0–16383, got {parameter}")
491
+ return parameter
492
+
493
+ if parameter not in pymididefs.rpn.RPN_MAP:
494
+ raise ValueError(f"Unknown RPN name '{parameter}' - not a standard Registered Parameter Number")
495
+
496
+ return pymididefs.rpn.RPN_MAP[parameter]
497
+
498
+ def note (self, pitch: typing.Union[int, str], beat: float, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.25) -> "PatternBuilder":
499
+
500
+ """
501
+ Place a single MIDI note at a specific beat position.
502
+
503
+ A drum name is carried through to the mirror fan-out so each device can
504
+ re-resolve it through its own ``drum_note_map``. A name no destination
505
+ maps (not in the pattern's own map nor any mirror's) is dropped and
506
+ warned once — it does not raise — so device maps can legitimately lack
507
+ voices others have. (A string pitch with **no** ``drum_note_map`` at all
508
+ is still a configuration error and raises.)
509
+
510
+ Parameters:
511
+ pitch: MIDI note number (0-127) or a drum name string from
512
+ the pattern's ``drum_note_map``.
513
+ beat: The beat position (0.0 is the start). Negative values
514
+ wrap from the end (e.g., -1.0 is one beat before the end).
515
+ velocity: MIDI velocity (0-127, default 100), or a
516
+ ``(low, high)`` tuple for a single random draw.
517
+ duration: Note duration in beats (default 0.25).
518
+
519
+ Example:
520
+ ```python
521
+ p.note(60, beat=0, velocity=110) # Middle C on beat 1
522
+ p.note("kick", beat=1.0) # Kick on beat 2
523
+ p.note(67, beat=-0.5, duration=0.5) # G on the 'and' of the last beat
524
+ ```
525
+ """
526
+
527
+ # Resolve leniently: a named drum the target can't voice is dropped (and
528
+ # warned once) rather than raising, and the drum name is carried so each
529
+ # destination can re-resolve it through its own drum_note_map.
530
+ resolution = self._resolve_hit_pitch(pitch)
531
+ if resolution is None:
532
+ return self # unknown drum name, mapped by no destination — dropped
533
+ midi_pitch, origin, primary_unmapped = resolution
534
+
535
+ resolved_velocity = self._resolve_velocity(velocity)
536
+
537
+ # Negative beat values wrap to the end of the pattern.
538
+ if beat < 0:
539
+ beat = beat % self._pattern.length # wrap from the end (any magnitude)
540
+
541
+ self._pattern.add_note_beats(
542
+ beat_position = beat,
543
+ pitch = midi_pitch,
544
+ velocity = resolved_velocity,
545
+ duration_beats = duration,
546
+ origin = origin,
547
+ primary_unmapped = primary_unmapped
548
+ )
549
+ return self
550
+
551
+ def note_on (self, pitch: typing.Union[int, str], beat: float, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY) -> "PatternBuilder":
552
+
553
+ """
554
+ Place an explicit Note On event without a duration.
555
+ Useful for drones or infinite sustains. Must be paired with
556
+ a ``note_off()`` later to silence the note.
557
+
558
+ Parameters:
559
+ pitch: MIDI note number (0-127) or a drum name string.
560
+ beat: The beat position (0.0 is the start).
561
+ velocity: MIDI velocity (0-127, default 100), or a
562
+ ``(low, high)`` tuple for a single random draw.
563
+
564
+ A drum name this device's ``drum_note_map`` lacks is dropped (warned
565
+ once) rather than raising — consistent with the step-note methods. A
566
+ string pitch with no ``drum_note_map`` at all is still a configuration
567
+ error and raises.
568
+ """
569
+
570
+ midi_pitch = self._resolve_pitch_lenient(pitch)
571
+ if midi_pitch is None:
572
+ return self # drum name this device can't voice — dropped (warned once)
573
+ resolved_velocity = self._resolve_velocity(velocity)
574
+ if beat < 0:
575
+ beat = beat % self._pattern.length # wrap from the end (any magnitude)
576
+
577
+ self._pattern.add_raw_note_beats(
578
+ message_type = 'note_on',
579
+ beat_position = beat,
580
+ pitch = midi_pitch,
581
+ velocity = resolved_velocity,
582
+ origin = pitch if isinstance(pitch, str) else None
583
+ )
584
+ return self
585
+
586
+ def note_off (self, pitch: typing.Union[int, str], beat: float) -> "PatternBuilder":
587
+
588
+ """
589
+ Place an explicit Note Off event to silence a drone.
590
+
591
+ Parameters:
592
+ pitch: MIDI note number (0-127) or a drum name string.
593
+ beat: The beat position (0.0 is the start).
594
+
595
+ A drum name this device's ``drum_note_map`` lacks is dropped (warned
596
+ once) rather than raising; with no ``drum_note_map`` at all it raises.
597
+ """
598
+
599
+ midi_pitch = self._resolve_pitch_lenient(pitch)
600
+ if midi_pitch is None:
601
+ return self # nothing to silence — this device can't voice the name
602
+ if beat < 0:
603
+ beat = beat % self._pattern.length # wrap from the end (any magnitude)
604
+
605
+ self._pattern.add_raw_note_beats(
606
+ message_type = 'note_off',
607
+ beat_position = beat,
608
+ pitch = midi_pitch,
609
+ velocity = 0,
610
+ origin = pitch if isinstance(pitch, str) else None
611
+ )
612
+ return self
613
+
614
+ def drone (self, pitch: typing.Union[int, str], beat: float = 0.0, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY) -> "PatternBuilder":
615
+
616
+ """
617
+ A musical alias for ``note_on``. Places a raw Note On event without a duration,
618
+ typically used for sustained notes that span multiple cycles.
619
+ Must be silenced later using ``drone_off()``.
620
+
621
+ Parameters:
622
+ pitch: MIDI note number (0-127) or a drum name string.
623
+ beat: The beat position (0.0 is the start).
624
+ velocity: MIDI velocity (0-127, default 100), or a
625
+ ``(low, high)`` tuple for a single random draw.
626
+ """
627
+
628
+ self.note_on(pitch, beat=beat, velocity=velocity)
629
+ return self
630
+
631
+ def drone_off (self, pitch: typing.Union[int, str]) -> "PatternBuilder":
632
+
633
+ """
634
+ A musical alias for ``note_off``. Places a raw Note Off event at beat 0.0.
635
+ Used to stop a sequence started by ``drone()``.
636
+
637
+ Parameters:
638
+ pitch: MIDI note number (0-127) or a drum name string.
639
+ """
640
+
641
+ self.note_off(pitch, beat=0.0)
642
+ return self
643
+
644
+ def silence (self, beat: float = 0.0) -> "PatternBuilder":
645
+
646
+ """
647
+ Sends an 'All Notes Off' (CC 123) and 'All Sound Off' (CC 120) message
648
+ on the pattern's channel to immediately silence any ringing notes or drones.
649
+
650
+ Parameters:
651
+ beat: The beat position (0.0 is the start).
652
+ """
653
+
654
+ self.cc(control=123, value=0, beat=beat)
655
+ self.cc(control=120, value=0, beat=beat)
656
+ return self
657
+
658
+ def hit (self, pitch: typing.Union[int, str], beats: typing.List[float], velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1) -> "PatternBuilder":
659
+
660
+ """
661
+ Place multiple short 'hits' at a list of beat positions.
662
+
663
+ Parameters:
664
+ pitch: MIDI note number or drum name.
665
+ beats: List of beat positions.
666
+ velocity: MIDI velocity (0-127), or a ``(low, high)`` tuple
667
+ for a fresh random draw per hit.
668
+ duration: Note duration in beats.
669
+
670
+ Example:
671
+ ```python
672
+ p.hit("snare", [1, 3]) # Standard backbeat
673
+ p.hit("snare", [1, 3], velocity=(80, 110)) # Human velocity range
674
+ ```
675
+ """
676
+
677
+ for beat in beats:
678
+ self.note(pitch=pitch, beat=beat, velocity=velocity, duration=duration)
679
+ return self
680
+
681
+ def hit_steps (self, pitch: typing.Union[int, str], steps: typing.List[int], velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, grid: typing.Optional[int] = None, probability: float = 1.0, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> "PatternBuilder":
682
+
683
+ """
684
+ Place short hits at specific step (grid) positions.
685
+
686
+ Parameters:
687
+ pitch: MIDI note number or drum name.
688
+ steps: A list of grid indices (0 to ``grid - 1``).
689
+ velocity: MIDI velocity (0-127), or a ``(low, high)`` tuple
690
+ for a fresh random draw per step.
691
+ duration: Note duration in beats.
692
+ grid: How many grid slots the pattern is divided into.
693
+ Defaults to the pattern's ``default_grid`` (set from the
694
+ decorator's ``steps``/``step_duration``, or sixteenth-note
695
+ resolution when ``unit`` is omitted).
696
+ probability: Chance (0.0 to 1.0) that each hit will play.
697
+ seed: Fix the probability gating for this call (an int); omit to
698
+ use the pattern's RNG.
699
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
700
+
701
+ Example:
702
+ ```python
703
+ # Typical sixteenth-note hi-hats with some probability variation
704
+ p.hit_steps("hh", range(16), velocity=70, probability=0.8)
705
+
706
+ # Humanised hi-hats — each step gets a fresh random velocity.
707
+ p.hit_steps("hh", range(16), velocity=(40, 90))
708
+ ```
709
+ """
710
+
711
+ rng = self._rng_from(seed, rng)
712
+
713
+ if grid is None:
714
+ grid = self._default_grid
715
+
716
+ if grid <= 0:
717
+ return self
718
+
719
+ step_duration = self._pattern.length / grid
720
+
721
+ for i in steps:
722
+
723
+ if probability < 1.0 and rng.random() >= probability:
724
+ continue
725
+
726
+ beat = i * step_duration
727
+ self.note(pitch=pitch, beat=beat, velocity=velocity, duration=duration)
728
+ return self
729
+
730
+ def motif (
731
+ self,
732
+ m: "subsequence.motifs.Motif",
733
+ beat: float = 0.0,
734
+ span: typing.Optional[float] = None,
735
+ root: int = 60,
736
+ velocity: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
737
+ fit: typing.Optional[float] = None,
738
+ fit_weights: typing.Optional[typing.List[float]] = None,
739
+ resolution: typing.Optional[int] = None,
740
+ ) -> "PatternBuilder":
741
+
742
+ """
743
+ Place an immutable :class:`~subsequence.motifs.Motif` onto the pattern.
744
+
745
+ Note events route through the universal ``note()`` funnel (drum names,
746
+ mirrors, velocity tuples all work); control gestures emit through the
747
+ same machinery as ``cc()`` / ``cc_ramp()`` / ``pitch_bend()`` /
748
+ ``nrpn()`` / ``osc()``. Pitch specs resolve here, late: ints are MIDI,
749
+ strings are drum names, scale degrees resolve against the composition
750
+ key + scale anchored near ``root=``. Per-event probabilities roll
751
+ fresh each cycle against the pattern's seeded stream.
752
+
753
+ Parameters:
754
+ m: The motif value (anything exposing ``.events`` / ``.length``
755
+ places; ``.controls`` is read when present).
756
+ beat: Where the motif starts within the pattern.
757
+ span: Clamp — events whose onset falls at or beyond *span* beats
758
+ into the motif are dropped (the ``arpeggio()`` convention).
759
+ root: Register anchor for scale-degree resolution: the tonic
760
+ lands at its nearest instance to this MIDI note (ties resolve
761
+ upward) and the melody keeps its written contour from there.
762
+ velocity: Optional override applied to every note (otherwise each
763
+ event's own velocity is used).
764
+ fit: The chord-tones-on-strong-beats dial, 0.0–1.0: resolved
765
+ Degree/int pitches landing on strong beats (metric weight
766
+ >= 0.5) snap to the nearest chord tone with this
767
+ probability. Defaults to the motif's own ``fit`` (0.7 on
768
+ generated motifs, none on hand-written ones — typed degrees
769
+ are sacred); inactive without a chord context. ChordTone
770
+ and Approach events never snap — their harmony reading is
771
+ inherent (an Approach's chromaticism is the point).
772
+ fit_weights: Custom per-step metric weight list (the
773
+ ``build_ghost_bias`` precedent) for additive or
774
+ non-isochronous meters; defaults to the time signature's
775
+ table.
776
+ resolution: Pulses between control-ramp messages (defaults to
777
+ each control verb's own default). Kept out of the value by
778
+ design: beats and shapes are music, traffic density is wire.
779
+ """
780
+
781
+ events = getattr(m, "events", None)
782
+
783
+ if events is None or not hasattr(m, "length"):
784
+ raise TypeError(f"motif() places Motif-like values (.events/.length) — got {type(m).__name__}")
785
+
786
+ effective_fit = fit if fit is not None else getattr(m, "fit", None)
787
+ fit_table: typing.Optional[typing.List[float]] = None
788
+ snap_probability = 0.0
789
+
790
+ if effective_fit:
791
+ snap_probability = float(effective_fit)
792
+ fit_table = list(fit_weights) if fit_weights is not None else subsequence.sequence_utils.build_metric_weights(
793
+ self.time_signature, grid = self._default_grid
794
+ )
795
+
796
+ for event in events:
797
+
798
+ if span is not None and event.beat >= span:
799
+ continue
800
+ if event.probability < 1.0 and self.rng.random() >= event.probability:
801
+ continue
802
+
803
+ resolved = self._resolve_motif_pitch(event.pitch, root, beat + event.beat)
804
+
805
+ # The fit dial reads only Degree/int content: drums have no
806
+ # pitch to snap, ChordTones already are chord tones, and an
807
+ # Approach's chromaticism is the point.
808
+ if (
809
+ fit_table is not None
810
+ and isinstance(resolved, int)
811
+ and isinstance(event.pitch, (int, subsequence.motifs.Degree))
812
+ ):
813
+ resolved = self._fit_snap(resolved, beat + event.beat, snap_probability, fit_table)
814
+
815
+ self.note(
816
+ pitch = resolved,
817
+ beat = beat + event.beat,
818
+ velocity = velocity if velocity is not None else event.velocity,
819
+ duration = event.duration,
820
+ )
821
+
822
+ for control in getattr(m, "controls", ()):
823
+
824
+ if span is not None and control.beat >= span:
825
+ continue
826
+ if control.probability < 1.0 and self.rng.random() >= control.probability:
827
+ continue
828
+
829
+ self._emit_control(control, beat, resolution)
830
+
831
+ return self
832
+
833
+ def _resolve_motif_pitch (self, pitch: typing.Any, root: int, event_beat: float = 0.0) -> typing.Union[int, str]:
834
+
835
+ """Resolve one stored pitch spec to a MIDI int or drum name, late.
836
+
837
+ ``event_beat`` is the event's position within this cycle — chord-
838
+ relative specs resolve against the chord sounding *under the event*
839
+ (``p.harmony.chord_at``), not the cycle-start snapshot.
840
+ """
841
+
842
+ if pitch is None:
843
+ raise ValueError(
844
+ "This motif is a rhythm skeleton (pitches stripped) — "
845
+ "re-pitch it with .pitched() before placing"
846
+ )
847
+
848
+ if isinstance(pitch, (int, str)):
849
+ return pitch
850
+
851
+ if isinstance(pitch, subsequence.motifs.Degree):
852
+ return self._resolve_degree_pitch(pitch, root)
853
+
854
+ if isinstance(pitch, subsequence.motifs.ChordTone):
855
+ return self._resolve_chord_tone_pitch(pitch, root, event_beat)
856
+
857
+ if isinstance(pitch, subsequence.motifs.Approach):
858
+ return self._resolve_approach_pitch(pitch, root, event_beat)
859
+
860
+ raise TypeError(f"Unknown pitch spec: {type(pitch).__name__}")
861
+
862
+ def _resolve_approach_pitch (self, approach: "subsequence.motifs.Approach", root: int, event_beat: float) -> int:
863
+
864
+ """Resolve an Approach: one semitone below its target's pitch.
865
+
866
+ A ``ChordTone`` target reads the chord at the NEXT boundary after the
867
+ event (the harmony window's anticipation data) — the approach is the
868
+ tension, the target is where the harmony lands. When the window
869
+ holds no committed next chord (the live mode horizon's edge), the
870
+ sounding chord stands in. ``Degree``/``int`` targets resolve as
871
+ usual (no harmony needed).
872
+ """
873
+
874
+ target = approach.target
875
+
876
+ if isinstance(target, subsequence.motifs.ChordTone):
877
+
878
+ if self.harmony is None:
879
+ raise ValueError(
880
+ "an Approach at a chord tone needs the harmonic clock — "
881
+ "call composition.harmony(...) (a style or a bound progression)"
882
+ )
883
+
884
+ chord = self.harmony.next_chord_at(event_beat)
885
+
886
+ if chord is None:
887
+ chord = self.harmony.chord_at(event_beat)
888
+ if chord is None:
889
+ raise ValueError(
890
+ f"No chord is known around beat {event_beat:g} of this cycle — "
891
+ "the harmony window does not cover it"
892
+ )
893
+
894
+ tones = chord.tones(root, count = target.index)
895
+ resolved = int(tones[target.index - 1]) + 12 * target.octave
896
+
897
+ elif isinstance(target, subsequence.motifs.Degree):
898
+ resolved = self._resolve_degree_pitch(target, root)
899
+
900
+ elif isinstance(target, int):
901
+ resolved = target
902
+
903
+ else:
904
+ raise TypeError(f"cannot approach {type(target).__name__} content")
905
+
906
+ pitch = resolved - 1
907
+
908
+ if not 0 <= pitch <= 127:
909
+ raise ValueError(
910
+ f"Approach resolves to MIDI {pitch}, outside 0–127 — adjust root= or the target's octave"
911
+ )
912
+
913
+ return pitch
914
+
915
+ def _fit_snap (self, pitch: int, event_beat: float, fit: float, weights: typing.List[float]) -> int:
916
+
917
+ """The fit dial: snap a strong-beat pitch to the nearest chord tone, with probability *fit*.
918
+
919
+ Strong beats are the metric-weight table's >= 0.5 positions
920
+ (downbeats and beats); off-grid events take the nearest grid
921
+ position's weight. Inactive without a chord context.
922
+ """
923
+
924
+ if self.harmony is None:
925
+ return pitch
926
+
927
+ bar_beats = float(self.time_signature[0])
928
+ grid = len(weights)
929
+ step = (event_beat % bar_beats) * grid / bar_beats
930
+ weight = weights[int(round(step)) % grid]
931
+
932
+ if weight < 0.5:
933
+ return pitch
934
+ if self.rng.random() >= fit:
935
+ return pitch
936
+
937
+ chord = self.harmony.chord_at(event_beat)
938
+
939
+ if chord is None:
940
+ return pitch
941
+
942
+ chord_pcs = {tone % 12 for tone in chord.tones(pitch)}
943
+
944
+ if pitch % 12 in chord_pcs:
945
+ return pitch
946
+
947
+ # Nearest chord tone, ties upward.
948
+ for delta in (1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6):
949
+ if (pitch + delta) % 12 in chord_pcs and 0 <= pitch + delta <= 127:
950
+ return pitch + delta
951
+
952
+ return pitch
953
+
954
+ def _resolve_chord_tone_pitch (self, tone: "subsequence.motifs.ChordTone", root: int, event_beat: float) -> int:
955
+
956
+ """Resolve a 1-based chord-tone index against the chord under the event.
957
+
958
+ Reads the harmony window (``p.harmony.chord_at(event_beat)``): indices
959
+ walk the sounding chord's tones nearest ``root``, cycling into higher
960
+ octaves past the chord's natural size, plus whole-octave shifts.
961
+ """
962
+
963
+ if self.harmony is None:
964
+ raise ValueError(
965
+ "ChordTone pitches resolve against the harmonic clock — "
966
+ "call composition.harmony(...) (a style or a bound progression)"
967
+ )
968
+
969
+ chord = self.harmony.chord_at(event_beat)
970
+
971
+ if chord is None:
972
+ raise ValueError(
973
+ f"No chord is known at beat {event_beat:g} of this cycle — "
974
+ "the harmony window does not cover it"
975
+ )
976
+
977
+ tones = chord.tones(root, count = tone.index)
978
+ midi = int(tones[tone.index - 1]) + 12 * tone.octave
979
+
980
+ if not 0 <= midi <= 127:
981
+ raise ValueError(
982
+ f"Chord tone {tone.index} resolves to MIDI {midi}, outside 0–127 — "
983
+ "adjust root= or the tone's octaves"
984
+ )
985
+
986
+ return midi
987
+
988
+ def _resolve_degree_pitch (self, degree: "subsequence.motifs.Degree", root: int) -> int:
989
+
990
+ """
991
+ Resolve a 1-based scale degree against the composition key + scale.
992
+
993
+ The tonic anchors at its nearest instance to ``root`` (ties resolve
994
+ upward); the degree then builds from the anchored tonic, so a written
995
+ melody keeps its contour. Steps beyond the scale length carry into
996
+ higher octaves (8 = tonic an octave up in seven-note scales).
997
+ """
998
+
999
+ if self.key is None:
1000
+ raise ValueError("Scale degrees resolve against a key — set Composition(key=...)")
1001
+
1002
+ mode = self.scale or "ionian"
1003
+ pcs = subsequence.intervals.scale_pitch_classes(subsequence.chords.key_name_to_pc(self.key), mode)
1004
+
1005
+ idx = (degree.step - 1) % len(pcs)
1006
+ carry = (degree.step - 1) // len(pcs)
1007
+
1008
+ diff = (pcs[0] - root) % 12
1009
+ tonic = root + diff if diff <= 6 else root + diff - 12
1010
+ offset = (pcs[idx] - pcs[0]) % 12
1011
+
1012
+ midi = tonic + offset + 12 * (carry + degree.octave) + degree.chroma
1013
+
1014
+ if not 0 <= midi <= 127:
1015
+ raise ValueError(
1016
+ f"Degree {degree.step} resolves to MIDI {midi}, outside 0–127 — "
1017
+ f"adjust root= or the degree's octaves"
1018
+ )
1019
+
1020
+ return midi
1021
+
1022
+ def _emit_control (self, control: "subsequence.motifs.ControlEvent", beat: float, resolution: typing.Optional[int]) -> None:
1023
+
1024
+ """Emit one stored control gesture through the matching builder verb."""
1025
+
1026
+ signal = control.signal
1027
+ onset = beat + control.beat
1028
+ extra: typing.Dict[str, typing.Any] = {} if resolution is None else {"resolution": resolution}
1029
+
1030
+ if isinstance(signal, subsequence.motifs.CC):
1031
+ if control.end is None:
1032
+ self.cc(signal.control, int(round(control.start)), beat=onset)
1033
+ else:
1034
+ self.cc_ramp(signal.control, int(round(control.start)), int(round(control.end)), beat_start=onset, beat_end=onset + control.span, shape=control.shape, **extra)
1035
+
1036
+ elif isinstance(signal, subsequence.motifs.PitchBend):
1037
+ if control.end is None:
1038
+ self.pitch_bend(control.start, beat=onset)
1039
+ else:
1040
+ self.pitch_bend_ramp(control.start, control.end, beat_start=onset, beat_end=onset + control.span, shape=control.shape, **extra)
1041
+
1042
+ elif isinstance(signal, subsequence.motifs.NRPN):
1043
+ if control.end is None:
1044
+ self.nrpn(signal.parameter, int(round(control.start)), beat=onset, fine=signal.fine, null_reset=signal.null_reset)
1045
+ else:
1046
+ self.nrpn_ramp(signal.parameter, int(round(control.start)), int(round(control.end)), beat_start=onset, beat_end=onset + control.span, shape=control.shape, fine=signal.fine, null_reset=signal.null_reset, **extra)
1047
+
1048
+ elif isinstance(signal, subsequence.motifs.RPN):
1049
+ if control.end is None:
1050
+ self.rpn(signal.parameter, int(round(control.start)), beat=onset, fine=signal.fine, null_reset=signal.null_reset)
1051
+ else:
1052
+ self.rpn_ramp(signal.parameter, int(round(control.start)), int(round(control.end)), beat_start=onset, beat_end=onset + control.span, shape=control.shape, fine=signal.fine, null_reset=signal.null_reset, **extra)
1053
+
1054
+ elif isinstance(signal, subsequence.motifs.OSC):
1055
+ if control.end is None:
1056
+ self.osc(signal.address, control.start, beat=onset)
1057
+ else:
1058
+ self.osc_ramp(signal.address, control.start, control.end, beat_start=onset, beat_end=onset + control.span, shape=control.shape, **extra)
1059
+
1060
+ else:
1061
+ raise TypeError(f"Unknown control signal: {type(signal).__name__}")
1062
+
1063
+ def phrase (
1064
+ self,
1065
+ value: typing.Any,
1066
+ root: int = 60,
1067
+ velocity: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
1068
+ fit: typing.Optional[float] = None,
1069
+ resolution: typing.Optional[int] = None,
1070
+ align: str = "pattern",
1071
+ offset: float = 0.0,
1072
+ ) -> "PatternBuilder":
1073
+
1074
+ """Place this cycle's window of a Phrase — position computed, never stored.
1075
+
1076
+ The playback position is stateless arithmetic over the engine's own
1077
+ counters: ``pos = (p.cycle * pattern_length + offset) % phrase.length``
1078
+ — deterministic under live reload, ``form_jump``, and render, with
1079
+ zero new state. A pattern shorter than the phrase walks through it
1080
+ cycle by cycle; deliberately mismatched lengths are phase drift
1081
+ (polymeter against the phrase). When the cycle window crosses the
1082
+ phrase's end, the phrase loops.
1083
+
1084
+ Patterns that should own the phrase's length call
1085
+ ``p.set_length(phrase.length)`` once instead.
1086
+
1087
+ Parameters:
1088
+ value: A Phrase (or any value with ``.length``/``.slice``; a
1089
+ Motif places its window directly).
1090
+ root: Register anchor for degree resolution (see ``motif()``).
1091
+ velocity: Optional override applied to every note.
1092
+ fit: Passed through to ``motif()`` (active with the melody
1093
+ engine stage).
1094
+ resolution: Control-ramp pulse density (see ``motif()``).
1095
+ align: ``"pattern"`` (default) counts pattern cycles;
1096
+ ``"section"`` uses the bar within the current form section,
1097
+ so the phrase restarts when the section does.
1098
+ offset: Beats added to the computed position (a phase shift).
1099
+
1100
+ Example:
1101
+ ```python
1102
+ @comp.pattern(channel=4, bars=2)
1103
+ def lead (p):
1104
+ p.phrase(lead_line, root=72)
1105
+ ```
1106
+ """
1107
+
1108
+ length = getattr(value, "length", None)
1109
+
1110
+ if length is None or not hasattr(value, "slice"):
1111
+ raise TypeError(f"phrase() places Phrase-like values (.length/.slice) — got {type(value).__name__}")
1112
+ if length <= 0:
1113
+ raise ValueError("cannot place an empty phrase")
1114
+
1115
+ if align == "pattern":
1116
+ position = (self.cycle * float(self._pattern.length) + offset) % length
1117
+ elif align == "section":
1118
+ if self.section is None:
1119
+ raise ValueError('phrase(align="section") needs a form — call composition.form(...)')
1120
+ position = (self.section.bar * float(self.time_signature[0]) + offset) % length
1121
+ else:
1122
+ raise ValueError(f'align must be "pattern" or "section" — got {align!r}')
1123
+
1124
+ window_beats = float(self._pattern.length)
1125
+ placed = 0.0
1126
+
1127
+ while placed < window_beats - 1e-9:
1128
+
1129
+ take = min(window_beats - placed, length - position)
1130
+ piece = value.slice(position, position + take)
1131
+ fragment = piece.flatten() if hasattr(piece, "flatten") else piece
1132
+
1133
+ self.motif(fragment, beat=placed, root=root, velocity=velocity, fit=fit, resolution=resolution)
1134
+
1135
+ placed += take
1136
+ position = 0.0 # crossed the phrase end — loop to its start
1137
+
1138
+ return self
1139
+
1140
+ def section_motif (self, part: typing.Optional[str] = None) -> typing.Optional[typing.Any]:
1141
+
1142
+ """The Motif/Phrase bound to the current section (and part), or ``None``.
1143
+
1144
+ Reads the ``composition.section_motifs()`` registry for the section
1145
+ currently playing. A section with no binding returns ``None`` —
1146
+ bind material or rest; no fallback guessing::
1147
+
1148
+ @comp.pattern(channel=4, bars=2)
1149
+ def lead (p):
1150
+ line = p.section_motif("lead")
1151
+ if line is not None:
1152
+ p.phrase(line, root=72)
1153
+ """
1154
+
1155
+ if self.section is None or self._section_motifs is None:
1156
+ return None
1157
+
1158
+ return self._section_motifs.get((self.section.name, part))
1159
+
1160
+ def capture (self, beat: float = 0.0, span: float = 4.0) -> "subsequence.motifs.Motif":
1161
+
1162
+ """
1163
+ Read the notes placed so far back out as a :class:`~subsequence.motifs.Motif`.
1164
+
1165
+ The captured motif is **absolute MIDI and lossy by design**: relative
1166
+ specs (degrees, chord tones) do not survive resolution, timing is
1167
+ pulse-truncated, probabilities have already rolled, and control
1168
+ gestures are not captured. The round trip is generate → place →
1169
+ capture → hand-edit → rebind.
1170
+
1171
+ Parameters:
1172
+ beat: Window start within the pattern.
1173
+ span: Window length in beats (also the captured motif's length).
1174
+ """
1175
+
1176
+ ppq = subsequence.constants.MIDI_QUARTER_NOTE
1177
+ lo, hi = int(beat * ppq), int((beat + span) * ppq)
1178
+ events = []
1179
+
1180
+ for pulse in sorted(self._pattern.steps):
1181
+
1182
+ if not lo <= pulse < hi:
1183
+ continue
1184
+
1185
+ for placed in self._pattern.steps[pulse].notes:
1186
+ events.append(subsequence.motifs.MotifEvent(
1187
+ beat = pulse / ppq - beat,
1188
+ pitch = placed.pitch,
1189
+ velocity = placed.velocity,
1190
+ duration = max(placed.duration, 1) / ppq,
1191
+ ))
1192
+
1193
+ return subsequence.motifs.Motif(events=tuple(events), length=span)
1194
+
1195
+ def sequence (self, steps: typing.List[int], pitches: typing.Union[int, str, typing.List[typing.Union[int, str]]], velocities: typing.Union[int, typing.Tuple[int, int], typing.List[int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, durations: typing.Union[float, typing.List[float]] = 0.1, grid: typing.Optional[int] = None, probability: float = 1.0, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> "PatternBuilder":
1196
+
1197
+ """
1198
+ A multi-parameter step sequencer.
1199
+
1200
+ Define which grid steps fire, and then provide a list of pitches,
1201
+ velocities, and durations. If you provide a list for any parameter,
1202
+ Subsequence will step through it as it places each note.
1203
+
1204
+ Parameters:
1205
+ steps: List of grid indices to trigger. An empty list is a
1206
+ no-op — no notes are placed and the builder is returned
1207
+ unchanged (handy when probabilistic gating rejects every step).
1208
+ pitches: Pitch or list of pitches.
1209
+ velocities: Velocity (default 100), ``(low, high)`` tuple for
1210
+ a fresh random draw per step, or a list of velocities
1211
+ matched to the steps one-to-one (a short list repeats its
1212
+ final value, a long list is truncated — both warn).
1213
+ durations: Duration or list of durations (default 0.1).
1214
+ grid: Grid resolution. Defaults to the pattern's
1215
+ ``default_grid`` (derived from the decorator's ``beats``/``steps``
1216
+ and ``unit``).
1217
+ probability: Chance (0.0 to 1.0) that each step will play.
1218
+ seed: Fix the probability gating for this call (an int); omit to
1219
+ use the pattern's RNG.
1220
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
1221
+ """
1222
+
1223
+ if not steps:
1224
+ return self
1225
+
1226
+ rng = self._rng_from(seed, rng)
1227
+
1228
+ if grid is None:
1229
+ grid = self._default_grid
1230
+
1231
+ if grid <= 0:
1232
+ return self
1233
+
1234
+ n = len(steps)
1235
+ pitches_list = _expand_sequence_param("pitches", pitches, n)
1236
+ # Treat a (low, high) tuple as a single random-range descriptor
1237
+ # rather than a 2-element list to cycle through.
1238
+ if isinstance(velocities, tuple):
1239
+ velocities_list = [velocities] * n
1240
+ else:
1241
+ velocities_list = _expand_sequence_param("velocities", velocities, n)
1242
+ durations_list = _expand_sequence_param("durations", durations, n)
1243
+
1244
+ step_duration = self._pattern.length / grid
1245
+
1246
+ for i, step_idx in enumerate(steps):
1247
+
1248
+ if probability < 1.0 and rng.random() >= probability:
1249
+ continue
1250
+
1251
+ beat = step_idx * step_duration
1252
+ self.note(pitch=pitches_list[i], beat=beat, velocity=velocities_list[i], duration=durations_list[i])
1253
+ return self
1254
+
1255
+ def seq (self, notation: str, pitch: typing.Union[str, int, None] = None, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> "PatternBuilder":
1256
+
1257
+ """
1258
+ Build a pattern using an expressive string-based 'mini-notation'.
1259
+
1260
+ The notation distributes events evenly across the current pattern length.
1261
+
1262
+ **Syntax:**
1263
+
1264
+ - ``x y z``: Items separated by spaces are distributed across the bar.
1265
+ - ``[a b]``: Groups items into a single subdivided step.
1266
+ - ``~`` or ``.``: A rest.
1267
+ - ``_``: Extends the previous note (sustain).
1268
+ - ``x?0.6``: Probability suffix — fires with the given probability (0.0–1.0).
1269
+
1270
+ Parameters:
1271
+ notation: The mini-notation string.
1272
+ pitch: If provided, all symbols in the string are triggers for
1273
+ this specific pitch. If ``None``, symbols are interpreted as
1274
+ pitches (e.g., "60" or "kick").
1275
+ velocity: MIDI velocity (default 100), or a ``(low, high)``
1276
+ tuple for a fresh random draw per event.
1277
+ seed: Fix the ``?`` probability gating for this call (an int);
1278
+ omit to use the pattern's RNG.
1279
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
1280
+
1281
+ Example:
1282
+ ```python
1283
+ # Simple kick rhythm
1284
+ p.seq("kick . [kick kick] .")
1285
+
1286
+ # Subdivided melody
1287
+ p.seq("60 [62 64] 67 60")
1288
+
1289
+ # Ghost snare: snare on 2 and 4, ghost note 50% of the time
1290
+ p.seq(". snare?0.5 . snare")
1291
+ ```
1292
+ """
1293
+
1294
+ rng = self._rng_from(seed, rng)
1295
+
1296
+ events = subsequence.mini_notation.parse(notation, total_duration=float(self._pattern.length))
1297
+
1298
+ for event in events:
1299
+
1300
+ # Apply probability before placing the note.
1301
+ if event.probability < 1.0 and rng.random() >= event.probability:
1302
+ continue
1303
+
1304
+ current_pitch = pitch
1305
+
1306
+ # If no global pitch provided, use the symbol as the pitch
1307
+ if current_pitch is None:
1308
+ # Try converting to int if it looks like a number
1309
+ if event.symbol.isdigit():
1310
+ current_pitch = int(event.symbol)
1311
+ else:
1312
+ current_pitch = event.symbol
1313
+
1314
+ self.note(
1315
+ pitch = current_pitch,
1316
+ beat = event.time,
1317
+ duration = event.duration,
1318
+ velocity = velocity
1319
+ )
1320
+ return self
1321
+
1322
+ def repeat (self, pitch: typing.Union[int, str], spacing: float, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.25) -> "PatternBuilder":
1323
+
1324
+ """
1325
+ Repeat a note at a fixed beat interval for the whole pattern.
1326
+
1327
+ The classic 'Note Repeat' of MPC, Push, and Maschine fame: one
1328
+ pitch firing at a steady rate — running hi-hats, a pulsing bass
1329
+ note, a metronome click.
1330
+
1331
+ Parameters:
1332
+ pitch: MIDI note number or drum name.
1333
+ spacing: Time between each note in beats (0.25 = sixteenth notes).
1334
+ velocity: MIDI velocity (default 100), or a ``(low, high)``
1335
+ tuple for a fresh random draw per note.
1336
+ duration: Note duration in beats.
1337
+
1338
+ Example:
1339
+ ```python
1340
+ p.repeat("hh", spacing=0.25) # sixteenth notes
1341
+ p.repeat("hh", spacing=0.25, velocity=(40, 80)) # humanised
1342
+ ```
1343
+ """
1344
+
1345
+ if spacing <= 0:
1346
+ raise ValueError("Spacing must be positive")
1347
+
1348
+ beat = 0.0
1349
+
1350
+ while beat < self._pattern.length:
1351
+ self.note(pitch=pitch, beat=beat, velocity=velocity, duration=duration)
1352
+ beat += spacing
1353
+ return self
1354
+
1355
+ def arpeggio (
1356
+ self,
1357
+ notes: typing.Any,
1358
+ root: typing.Optional[int] = None,
1359
+ velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY,
1360
+ count: typing.Optional[int] = None,
1361
+ inversion: int = 0,
1362
+ beat: float = 0.0,
1363
+ span: typing.Optional[float] = None,
1364
+ spacing: float = 0.25,
1365
+ duration: typing.Optional[float] = None,
1366
+ direction: str = "up",
1367
+ seed: typing.Optional[int] = None,
1368
+ rng: typing.Optional[random.Random] = None
1369
+ ) -> "PatternBuilder":
1370
+
1371
+ """
1372
+ Arpeggiate a chord (or a list of pitches) — cycle the notes one at a time
1373
+ at regular beat intervals.
1374
+
1375
+ Like ``chord()`` and ``strum()``, the first argument can be a chord — the
1376
+ ``chord`` passed to your pattern function, or any chord from
1377
+ ``p.progression()`` — and ``root`` / ``count`` / ``inversion`` voice it
1378
+ exactly as they do. So "play this as a chord, a strum, or an arpeggio" is a
1379
+ one-word verb swap::
1380
+
1381
+ for chord, start, length in p.progression("phrygian_minor", harmonic_rhythm=...):
1382
+ p.arpeggio(chord, root=48, beat=start, span=length, spacing=0.25, count=4)
1383
+
1384
+ Pass a list of pitches instead to arpeggiate something that isn't a chord (a
1385
+ scale fragment, a custom voicing). Unlike a held ``chord()``, an arpeggio is
1386
+ a stream of single notes, so it has no ``sustain`` / ``legato`` / ``detached``
1387
+ — use ``duration`` for how long each note rings and ``span`` for how much of
1388
+ the bar the figure fills.
1389
+
1390
+ An empty pitch list rests (places nothing), so a live arpeggiator over
1391
+ ``p.held_notes()`` is simply silent when no keys are held::
1392
+
1393
+ p.arpeggio(p.held_notes(), direction="up")
1394
+
1395
+ Parameters:
1396
+ notes: A chord to arpeggiate (anything with a ``.tones()`` method — the
1397
+ pattern's ``chord``, or a chord from ``p.progression()``), or a list
1398
+ of MIDI note numbers (e.g. ``60``) / drum-name strings when the
1399
+ pattern has a ``drum_note_map``. For pitched note *names* use the
1400
+ integer constants in ``subsequence.constants.midi_notes`` (e.g.
1401
+ ``notes.C4``). In the list form, a drum name the map lacks is
1402
+ dropped (warned once); a string with no map at all still raises.
1403
+ root: MIDI root note for the chord form (e.g. 48), exactly as ``chord()``.
1404
+ Required for a chord; not used for a plain pitch list.
1405
+ velocity: MIDI velocity for all notes (default 100 — arpeggios sit in the
1406
+ melodic-line velocity bucket, not the softened-chord bucket; pass
1407
+ ``velocity=90`` to match ``chord()``), or a ``(low, high)`` tuple for
1408
+ a fresh random draw per note.
1409
+ count: Number of voices for the chord form (cycles tones into higher
1410
+ octaves if larger than the chord's natural size). Chord form only.
1411
+ inversion: Chord inversion for the chord form (ignored when voice leading
1412
+ is on). Chord form only.
1413
+ beat: Beat to start the figure at (default 0.0 = the start of the
1414
+ pattern). Use it to place an arpeggio over one progression chord.
1415
+ span: How many beats the figure fills, starting at ``beat`` (default: to
1416
+ the end of the pattern). Pass the chord's ``length`` from a
1417
+ progression loop to confine the arpeggio to its slot.
1418
+ spacing: Time between each note in beats (default 0.25 = 16th note).
1419
+ duration: Note duration in beats. Defaults to ``spacing`` (each note
1420
+ fills its slot exactly).
1421
+ direction: Order in which the notes are cycled:
1422
+
1423
+ - ``"up"`` — lowest to highest, then wrap (default).
1424
+ - ``"down"`` — highest to lowest, then wrap.
1425
+ - ``"up_down"`` — ascend then descend (ping-pong), cycling.
1426
+ - ``"random"`` — shuffled once per call using *rng*.
1427
+
1428
+ seed: Fix the ``direction="random"`` shuffle for this call (an
1429
+ int); omit to use the pattern's RNG.
1430
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
1431
+
1432
+ Example:
1433
+ ```python
1434
+ # Arpeggiate the pattern's current chord, four voices ascending
1435
+ p.arpeggio(chord, root=60, count=4, spacing=0.25)
1436
+
1437
+ # A plain list of pitches — ping-pong: C E G E C E G E ...
1438
+ p.arpeggio([60, 64, 67], spacing=0.25, direction="up_down")
1439
+
1440
+ # One chord of a progression, confined to its slot, humanised
1441
+ p.arpeggio(chord, root=48, beat=start, span=length, velocity=(60, 95))
1442
+ ```
1443
+ """
1444
+
1445
+ if beat < 0:
1446
+ raise ValueError("arpeggio beat must be >= 0 — use a positive start within the pattern")
1447
+
1448
+ if spacing <= 0:
1449
+ raise ValueError("Spacing must be positive")
1450
+
1451
+ # Resolve the first argument into a concrete pitch list. A chord-like object
1452
+ # (it has .tones()) is voiced via root/count/inversion exactly as chord() does;
1453
+ # anything else is treated as an explicit list of pitches (today's behaviour).
1454
+ resolved: typing.List[int]
1455
+
1456
+ if hasattr(notes, "tones"):
1457
+ if root is None:
1458
+ raise ValueError("arpeggio(<chord>, …) needs a root — e.g. arpeggio(chord, root=48); pass a root MIDI note, or hand a list of pitches instead")
1459
+ resolved = notes.tones(root=root, inversion=inversion, count=count)
1460
+ else:
1461
+ if root is not None or count is not None or inversion != 0:
1462
+ raise ValueError("arpeggio root=, count=, and inversion= only apply to the chord form — arpeggio(chord, root=48, count=4); with a plain pitch list, drop them")
1463
+ if not notes:
1464
+ return self # nothing held (e.g. p.arpeggio(p.held_notes()) with no keys down) — rest
1465
+ resolved = [r for r in (self._resolve_pitch_lenient(p) for p in notes) if r is not None]
1466
+ if not resolved:
1467
+ return self # every named voice was dropped (this device lacks them all)
1468
+
1469
+ if direction == "up":
1470
+ pass # already in ascending order as supplied
1471
+ elif direction == "down":
1472
+ resolved = list(reversed(resolved))
1473
+ elif direction == "up_down":
1474
+ if len(resolved) > 1:
1475
+ resolved = resolved + list(reversed(resolved[1:-1]))
1476
+ elif direction == "random":
1477
+ rng = self._rng_from(seed, rng)
1478
+ resolved = list(resolved)
1479
+ rng.shuffle(resolved)
1480
+ else:
1481
+ raise ValueError(f"direction must be 'up', 'down', 'up_down', or 'random', got '{direction}'")
1482
+
1483
+ if duration is None:
1484
+ duration = spacing
1485
+
1486
+ # Window the figure to [beat, beat + span), clamped to the pattern end so a
1487
+ # positioned arpeggio (e.g. one chord of a progression) stays in its slot.
1488
+ pattern_length = float(self._pattern.length)
1489
+
1490
+ if span is None:
1491
+ end = pattern_length
1492
+ else:
1493
+ if span <= 0:
1494
+ raise ValueError(f"span must be positive, got {span:g}")
1495
+ end = beat + span
1496
+
1497
+ end = min(end, pattern_length)
1498
+
1499
+ # Place notes one at a time via self.note() so a (low, high)
1500
+ # velocity tuple produces a fresh random draw per arp note.
1501
+ position = beat
1502
+ i = 0
1503
+ while position < end:
1504
+ self.note(
1505
+ pitch = resolved[i % len(resolved)],
1506
+ beat = position,
1507
+ velocity = velocity,
1508
+ duration = duration,
1509
+ )
1510
+ position += spacing
1511
+ i += 1
1512
+ return self
1513
+
1514
+ def _warn_positioned_articulation (self, method: str, beat: float) -> None:
1515
+
1516
+ """Warn (once per pattern) that ``sustain``/``detached`` ring from the pattern
1517
+ length, not from ``beat``.
1518
+
1519
+ ``chord``/``strum`` size ``sustain``/``detached`` against the whole pattern (the
1520
+ one-chord-fills-the-bar model). With a non-zero ``beat`` — e.g. placing several
1521
+ chords across a progression — that almost always rings the chord far past its
1522
+ slot, so we flag it. Deduped on the pattern so a hot-reloading builder warns once.
1523
+ """
1524
+
1525
+ if self._pattern._warned_positioned_articulation:
1526
+ return
1527
+ self._pattern._warned_positioned_articulation = True
1528
+ logger.warning(
1529
+ "%s(beat=%g, …) was called with sustain= or detached= set — those size the ring "
1530
+ "from the pattern length, not from beat, so the chord can sustain past its slot. "
1531
+ "For a positioned chord (e.g. over a progression) set duration= explicitly instead.",
1532
+ method, beat,
1533
+ )
1534
+
1535
+ def chord (self, chord_obj: typing.Any, root: int, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY, sustain: bool = False, duration: float = 1.0, inversion: int = 0, count: typing.Optional[int] = None, legato: typing.Optional[float] = None, detached: typing.Optional[float] = None, beat: float = 0.0) -> "PatternBuilder":
1536
+
1537
+ """
1538
+ Place a chord at ``beat`` (the start of the pattern by default).
1539
+
1540
+ Note: If the pattern was registered with ``voice_leading=True``,
1541
+ this method automatically chooses the best inversion.
1542
+
1543
+ Parameters:
1544
+ chord_obj: The chord to play (usually the ``chord`` parameter
1545
+ passed to your pattern function).
1546
+ root: MIDI root note (e.g., 60 for Middle C).
1547
+ velocity: MIDI velocity (default 90), or a ``(low, high)``
1548
+ tuple for a fresh random draw per chord tone (each
1549
+ voice gets a slightly different velocity — useful for
1550
+ humanising the "fingers" feel).
1551
+ sustain: If True, the notes last for the entire pattern duration.
1552
+ Mutually exclusive with ``legato`` and ``detached``.
1553
+ duration: Note duration in beats (default 1.0). Ignored when
1554
+ ``legato`` or ``detached`` is set, since those recalculate
1555
+ durations.
1556
+ inversion: Specific chord inversion (ignored if voice leading is on).
1557
+ count: Number of notes to play (cycles tones if higher than
1558
+ the chord's natural size).
1559
+ legato: If given, calls ``p.legato(ratio)`` after placing the
1560
+ chord, stretching each note to fill ``ratio`` of the gap to
1561
+ the next note. Mutually exclusive with ``sustain`` and
1562
+ ``detached``.
1563
+ detached: If given, the chord rings until ``detached`` beats
1564
+ before the next cycle — equivalent to setting
1565
+ ``duration = pattern.length - detached``. Use this for a
1566
+ declarative polyphony-safety margin so the chord always
1567
+ releases before the next chord begins. Mutually exclusive
1568
+ with ``sustain`` and ``legato``.
1569
+ beat: Beat offset to place the chord at (default 0.0 = the start of the
1570
+ pattern). ``sustain`` and ``detached`` still measure their ring from the
1571
+ pattern length, not from ``beat`` — when placing several positioned chords
1572
+ (e.g. over a progression) set ``duration`` explicitly instead.
1573
+
1574
+ Example::
1575
+
1576
+ # Shorthand for: p.chord(...) then p.legato(0.9)
1577
+ p.chord(chord, root=root, velocity=85, count=4, legato=0.9)
1578
+
1579
+ # Hold the chord almost the full cycle, releasing 0.25 beats
1580
+ # before the next chord begins.
1581
+ p.chord(chord, root=root, velocity=85, count=5, detached=0.25)
1582
+ """
1583
+
1584
+ set_count = (1 if sustain else 0) + (1 if legato is not None else 0) + (1 if detached is not None else 0)
1585
+ if set_count > 1:
1586
+ raise ValueError("sustain=, legato=, and detached= are mutually exclusive — use one or the other")
1587
+
1588
+ if beat != 0.0 and (sustain or detached is not None):
1589
+ self._warn_positioned_articulation("chord", beat)
1590
+
1591
+ pitches = chord_obj.tones(root=root, inversion=inversion, count=count)
1592
+
1593
+ if sustain:
1594
+ duration = float(self._pattern.length)
1595
+ elif detached is not None:
1596
+ duration = float(self._pattern.length) - detached
1597
+ if duration <= 0:
1598
+ raise ValueError(f"detached ({detached}) must be less than the pattern length ({self._pattern.length:g} beats) so the chord keeps a positive duration")
1599
+
1600
+ for pitch in pitches:
1601
+ self._pattern.add_note_beats(
1602
+ beat_position = beat,
1603
+ pitch = pitch,
1604
+ velocity = self._resolve_velocity(velocity),
1605
+ duration_beats = duration
1606
+ )
1607
+
1608
+ if legato is not None:
1609
+ self.legato(legato)
1610
+ return self
1611
+
1612
+ def strum (self, chord_obj: typing.Any, root: int, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY, sustain: bool = False, duration: float = 1.0, inversion: int = 0, count: typing.Optional[int] = None, spacing: float = 0.05, direction: str = "up", legato: typing.Optional[float] = None, detached: typing.Optional[float] = None, beat: float = 0.0) -> "PatternBuilder":
1613
+
1614
+ """
1615
+ Play a chord with a small time offset between each note (strum effect).
1616
+
1617
+ Works exactly like ``chord()`` but staggers the notes instead of
1618
+ playing them simultaneously. The first note lands on ``beat`` (0 by default);
1619
+ subsequent notes are delayed by ``spacing`` beats each.
1620
+
1621
+ Parameters:
1622
+ chord_obj: The chord to play (usually the ``chord`` parameter
1623
+ passed to your pattern function).
1624
+ root: MIDI root note (e.g., 60 for Middle C).
1625
+ velocity: MIDI velocity (default 90), or a ``(low, high)``
1626
+ tuple for a fresh random draw per strum note.
1627
+ sustain: If True, the notes last for the entire pattern duration.
1628
+ Mutually exclusive with ``legato`` and ``detached``.
1629
+ duration: Note duration in beats (default 1.0). Ignored when
1630
+ ``legato`` or ``detached`` is set, since those recalculate
1631
+ durations.
1632
+ inversion: Specific chord inversion (ignored if voice leading is on).
1633
+ count: Number of notes to play (cycles tones if higher than
1634
+ the chord's natural size).
1635
+ spacing: Time in beats between each note onset (default 0.05).
1636
+ direction: ``"up"`` for low-to-high, ``"down"`` for high-to-low.
1637
+ beat: Beat offset for the first note (default 0.0); the stagger is added
1638
+ on top. ``sustain``/``detached`` ring from the pattern length, not from
1639
+ ``beat`` — set ``duration`` explicitly when placing positioned strums.
1640
+ legato: If given, calls ``p.legato(ratio)`` after placing the
1641
+ chord, stretching each note to fill ``ratio`` of the gap to
1642
+ the next note. Mutually exclusive with ``sustain`` and
1643
+ ``detached``.
1644
+ detached: If given, every strum note rings with a uniform
1645
+ duration of ``pattern.length - detached - (count - 1) * spacing``.
1646
+ The last note ends exactly ``detached`` beats before the
1647
+ next cycle; earlier notes end proportionally sooner, so
1648
+ releases are staggered in the same shape as the placements
1649
+ (the hand lifts the way it landed). Polyphony-safe:
1650
+ guarantees nothing from this strum is still sounding when
1651
+ the next chord begins. Mutually exclusive with ``sustain``
1652
+ and ``legato``.
1653
+
1654
+ Example::
1655
+
1656
+ # Gentle upward strum with legato
1657
+ p.strum(chord, root=52, velocity=85, spacing=0.06, legato=0.95)
1658
+
1659
+ # Fast downward strum
1660
+ p.strum(chord, root=52, direction="down", spacing=0.03)
1661
+
1662
+ # Five-voice strum with a 0.25-beat safety gap before the
1663
+ # next chord — won't exhaust polyphony on a 5-voice synth.
1664
+ p.strum(chord, root=48, count=5, spacing=0.1, detached=0.25)
1665
+ """
1666
+
1667
+ set_count = (1 if sustain else 0) + (1 if legato is not None else 0) + (1 if detached is not None else 0)
1668
+ if set_count > 1:
1669
+ raise ValueError("sustain=, legato=, and detached= are mutually exclusive — use one or the other")
1670
+
1671
+ if beat != 0.0 and (sustain or detached is not None):
1672
+ self._warn_positioned_articulation("strum", beat)
1673
+
1674
+ if spacing <= 0:
1675
+ raise ValueError("spacing must be positive")
1676
+
1677
+ if direction not in ("up", "down"):
1678
+ raise ValueError(f"direction must be 'up' or 'down', got '{direction}'")
1679
+
1680
+ pitches = chord_obj.tones(root=root, inversion=inversion, count=count)
1681
+
1682
+ if direction == "down":
1683
+ pitches = list(reversed(pitches))
1684
+
1685
+ if sustain:
1686
+ duration = float(self._pattern.length)
1687
+ elif detached is not None:
1688
+ duration = float(self._pattern.length) - detached - (len(pitches) - 1) * spacing
1689
+ if duration <= 0:
1690
+ raise ValueError(f"detached ({detached}) plus the strum stagger exceeds the pattern length ({self._pattern.length:g} beats) — reduce detached, spacing, or count")
1691
+
1692
+ for i, pitch in enumerate(pitches):
1693
+ self.note(pitch=pitch, beat=beat + i * spacing, velocity=velocity, duration=duration)
1694
+
1695
+ if legato is not None:
1696
+ self.legato(legato)
1697
+ return self
1698
+
1699
+ def progression (self, source: subsequence.progressions.ProgressionSource, harmonic_rhythm: subsequence.progressions.HarmonicRhythmSpec, key: typing.Optional[str] = None, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> subsequence.progressions.Progression:
1700
+
1701
+ """Realise a chord progression across the pattern, returning it to place yourself.
1702
+
1703
+ Returns a freshly realised :class:`~subsequence.progressions.Progression`
1704
+ — an iterable of ``(chord, start, length)`` events laying a progression
1705
+ end-to-end across the pattern's length, each chord given a length drawn
1706
+ from *harmonic_rhythm* (the musical term for how often the chords
1707
+ change). You loop over it and play each chord however you like —
1708
+ block, strummed, or arpeggiated::
1709
+
1710
+ for chord, start, length in p.progression("phrygian_minor",
1711
+ harmonic_rhythm=between(WHOLE, 3 * WHOLE, step=WHOLE), seed=7):
1712
+ p.strum(chord, root=48, beat=start, duration=length - 0.25, spacing=0.04, count=4)
1713
+
1714
+ This is the **part-level** progression seam: it re-realises a fresh
1715
+ value each rebuild (the breathing behaviour), runs entirely outside
1716
+ the global harmonic clock — so a part can inhabit its own harmonic
1717
+ world (polytonality) or move faster than the clock's span floor —
1718
+ and never advances engine state.
1719
+
1720
+ For a one-call block-chord part with no loop, use ``composition.chords()``.
1721
+
1722
+ Parameters:
1723
+ source: A built-in chord-graph style name (e.g. ``"phrygian_minor"``) to
1724
+ *generate* a progression; an explicit element list — ints where
1725
+ diatonic, name or roman strings (``["Cm7", 6, "bVII"]``), ``Chord``
1726
+ objects — cycled to fill the pattern; or a
1727
+ :class:`~subsequence.progressions.Progression` value (its spans
1728
+ cycled, decoration preserved).
1729
+ harmonic_rhythm: How long each chord lasts, in beats. One of: a single
1730
+ number (static); a list of lengths (a shaped rhythm such as
1731
+ ``[WHOLE, HALF, HALF]``, cycled per chord); or ``between(low, high,
1732
+ step=...)`` for a bounded, optionally-quantised random length.
1733
+ key: Key for styles and key-relative elements (degrees/romans);
1734
+ defaults to the composition's key.
1735
+ seed: If given, the progression is realised from a fresh ``Random(seed)``
1736
+ so it is identical on every cycle (a fixed phrase). When omitted, the
1737
+ pattern's own RNG is used, so it can vary per cycle (still reproducible
1738
+ under a composition seed).
1739
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
1740
+
1741
+ Returns:
1742
+ A ``Progression`` you can iterate as ``(chord, start, length)`` tuples
1743
+ (or read via ``.events()`` / ``print()``).
1744
+ """
1745
+
1746
+ rng = self._rng_from(seed, rng)
1747
+ resolved_key = key if key is not None else self.key
1748
+ return subsequence.progressions.realize(
1749
+ source = source,
1750
+ harmonic_rhythm = harmonic_rhythm,
1751
+ key = resolved_key,
1752
+ length = float(self._pattern.length),
1753
+ rng = rng,
1754
+ scale = self.scale or "ionian",
1755
+ )
1756
+
1757
+ def broken_chord (self, chord_obj: typing.Any, root: int, order: typing.List[int], spacing: float = 0.25, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY, duration: typing.Optional[float] = None, inversion: int = 0, beat: float = 0.0, span: typing.Optional[float] = None) -> "PatternBuilder":
1758
+
1759
+ """
1760
+ Play a chord as an arpeggio in a specific or random order.
1761
+
1762
+ This generates the chord tones and maps them according to the provided
1763
+ ``order`` list of indices, then delegates to ``arpeggio()``. It is ideal
1764
+ for broken chords or random chord-tone melodies.
1765
+
1766
+ Because the order is a list of node indices, the number of generated tones
1767
+ is automatically set to ``max(order) + 1`` to ensure all indices are valid.
1768
+ Higher indices will cycle into the next octave.
1769
+
1770
+ Parameters:
1771
+ chord_obj: The chord to play (usually from ``p.section.chord``).
1772
+ root: MIDI root note (e.g., 60 for Middle C).
1773
+ order: List of indices into the chord tones array, dictating playback order.
1774
+ spacing: Time between each note in beats (default 0.25 = 16th note).
1775
+ velocity: MIDI velocity for all notes (default 90 — broken_chord is a
1776
+ chord voice, so it sits in the softer chord velocity bucket like
1777
+ ``chord()`` and ``strum()``), or a ``(low, high)`` tuple for a
1778
+ fresh random draw per note.
1779
+ duration: Note duration in beats. Defaults to ``spacing``.
1780
+ inversion: Specific chord inversion (ignored if voice leading is on).
1781
+ beat: Beat to start the broken chord at (default 0.0).
1782
+ span: How many beats to fill from ``beat`` (default: to the end of the
1783
+ pattern). Like ``arpeggio()``, use it to place a broken chord over
1784
+ one chord of a progression.
1785
+
1786
+ Example::
1787
+
1788
+ # A 5-note broken chord using a predefined pattern
1789
+ p.broken_chord(chord, root=60, order=[4, 0, 2, 1, 3], spacing=0.25)
1790
+
1791
+ # A fully random broken chord using the pattern's deterministic RNG
1792
+ order = list(range(5))
1793
+ p.rng.shuffle(order)
1794
+ p.broken_chord(chord, root=60, order=order)
1795
+ """
1796
+
1797
+ if not order:
1798
+ raise ValueError("order list cannot be empty")
1799
+
1800
+ for idx in order:
1801
+ if not isinstance(idx, int) or idx < 0:
1802
+ raise ValueError("order must contain only non-negative integers")
1803
+
1804
+ required_count = max(order) + 1
1805
+ tones = chord_obj.tones(root=root, inversion=inversion, count=required_count)
1806
+ pitches = [tones[i] for i in order]
1807
+
1808
+ self.arpeggio(notes=pitches, spacing=spacing, velocity=velocity, duration=duration, direction="up", beat=beat, span=span)
1809
+ return self
1810
+
1811
+ def swing (self, percent: float = 57.0, grid: float = 0.25, strength: float = 1.0) -> "PatternBuilder":
1812
+
1813
+ """
1814
+ Apply swing feel to all notes in the pattern.
1815
+
1816
+ A shortcut for ``p.groove(Groove.swing(percent, grid), strength)``. Swing is a
1817
+ groove where every other grid note is delayed - the simplest way to
1818
+ give a mechanical pattern a pushed, human feel.
1819
+
1820
+ 50% is perfectly straight (no swing). 57% is the Ableton default
1821
+ (a gentle shuffle). 67% is classic triplet swing.
1822
+
1823
+ Parameters:
1824
+ percent: Swing amount as a percentage (50-75 is the useful range).
1825
+ 50 = straight, 57 = moderate shuffle, 67 ≈ triplet swing.
1826
+ grid: Grid size in beats (0.25 = 16th notes, 0.5 = 8th notes).
1827
+ strength: How much swing to apply (0.0-1.0). 0.0 = no effect,
1828
+ 1.0 = full swing at the given percent. Useful for dialling
1829
+ back the feel without changing the swing percentage.
1830
+
1831
+ Example::
1832
+
1833
+ p.hit_steps("hh", range(16), velocity=80)
1834
+ p.swing(57) # gentle 16th-note shuffle
1835
+ p.swing(57, strength=0.5) # half-strength — subtler feel
1836
+ """
1837
+
1838
+ self.groove(subsequence.groove.Groove.swing(percent=percent, grid=grid), strength=strength)
1839
+ return self
1840
+
1841
+
1842
+ def groove (self, template: subsequence.groove.Groove, strength: float = 1.0) -> "PatternBuilder":
1843
+
1844
+ """
1845
+ Apply a groove template to all notes in the pattern.
1846
+
1847
+ A groove is a repeating pattern of per-step timing offsets and
1848
+ optional velocity adjustments. It gives a pattern its characteristic
1849
+ rhythmic feel - swing, shuffle, MPC pocket, or any custom shape.
1850
+
1851
+ Construct a groove with one of the factory methods:
1852
+
1853
+ - ``Groove.swing(percent)`` - simple swing by percentage
1854
+ (or use the ``p.swing()`` shortcut for common cases)
1855
+ - ``Groove.from_agr(path)`` - import timing from an Ableton .agr file
1856
+ - ``Groove(offsets=[...], grid=0.25, velocities=[...])`` - fully custom
1857
+
1858
+ ``p.groove()`` is a post-build transform - call it after all notes
1859
+ have been placed. It pairs well with ``p.randomize()`` for
1860
+ structured feel plus organic micro-variation.
1861
+
1862
+ Parameters:
1863
+ template: A ``Groove`` instance defining the timing/velocity template.
1864
+ strength: How much of the groove to apply (0.0-1.0). 0.0 = no
1865
+ effect, 1.0 = full groove. Blends timing offsets and velocity
1866
+ deviation proportionally - equivalent to Ableton's
1867
+ TimingAmount and VelocityAmount dials.
1868
+
1869
+ Example::
1870
+
1871
+ groove = subsequence.Groove.swing(percent=57)
1872
+
1873
+ @composition.pattern(channel=10, beats=4)
1874
+ def drums (p):
1875
+ p.hit_steps("kick", [0, 8], velocity=100)
1876
+ p.hit_steps("hh", range(16), velocity=80)
1877
+ p.groove(groove) # full strength
1878
+ p.groove(groove, strength=0.5) # half-strength blend
1879
+ """
1880
+
1881
+ self._pattern.steps = subsequence.groove.apply_groove(
1882
+ self._pattern.steps, template, strength=strength
1883
+ )
1884
+ return self
1885
+
1886
+ # These methods transform existing notes after they have been placed.
1887
+ # Call them at the end of your builder function, after all notes are
1888
+ # in position. They operate on self._pattern.steps (the pulse-position
1889
+ # dict) and can be chained in any order.
1890
+
1891
+ def dropout (self, probability: float, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> "PatternBuilder":
1892
+
1893
+ """
1894
+ Randomly remove notes from the pattern.
1895
+
1896
+ This operates on all notes currently placed in the builder.
1897
+
1898
+ Parameters:
1899
+ probability: The chance (0.0 to 1.0) of each pulse POSITION being
1900
+ removed — all notes sharing that position (a chord's voices,
1901
+ layered drums) live or die together.
1902
+ seed: Fix the dropout for this call (an int); omit to use the
1903
+ pattern's RNG.
1904
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
1905
+ """
1906
+
1907
+ rng = self._rng_from(seed, rng)
1908
+
1909
+ positions_to_remove = []
1910
+
1911
+ for position in list(self._pattern.steps.keys()):
1912
+
1913
+ if rng.random() < probability:
1914
+ positions_to_remove.append(position)
1915
+
1916
+ for position in positions_to_remove:
1917
+ del self._pattern.steps[position]
1918
+ return self
1919
+
1920
+ def velocity_shape (self, low: int = subsequence.constants.velocity.VELOCITY_SHAPE_LOW, high: int = subsequence.constants.velocity.VELOCITY_SHAPE_HIGH) -> "PatternBuilder":
1921
+
1922
+
1923
+ """
1924
+ Apply organic velocity variation to all notes in the pattern.
1925
+
1926
+ Uses a van der Corput sequence to distribute velocities evenly
1927
+ across the specified range, which often sounds more 'human' than
1928
+ purely random velocity variation.
1929
+
1930
+ Parameters:
1931
+ low: Minimum velocity (default 64).
1932
+ high: Maximum velocity (default 127).
1933
+ """
1934
+
1935
+ positions = sorted(self._pattern.steps.keys())
1936
+
1937
+ if not positions:
1938
+ return self
1939
+
1940
+ vdc_values = subsequence.sequence_utils.generate_van_der_corput_sequence(len(positions))
1941
+
1942
+ for position, vdc_value in zip(positions, vdc_values):
1943
+
1944
+ step = self._pattern.steps[position]
1945
+
1946
+ for note in step.notes:
1947
+ note.velocity = int(low + (high - low) * vdc_value)
1948
+ return self
1949
+
1950
+ def duck_map (
1951
+ self,
1952
+ steps: typing.Iterable[int],
1953
+ floor: float = 0.0,
1954
+ grid: typing.Optional[int] = None,
1955
+ ) -> typing.List[float]:
1956
+
1957
+ """
1958
+ Build a per-step velocity multiplier list for sidechain-style ducking.
1959
+
1960
+ Returns a list of floats, one per grid step: ``floor`` at each trigger
1961
+ step in ``steps``, ``1.0`` everywhere else. Pass the result to
1962
+ ``p.data`` for another pattern to read, then apply with
1963
+ ``p.scale_velocities()``.
1964
+
1965
+ Parameters:
1966
+ steps: Grid indices that trigger ducking (e.g. kick hit positions).
1967
+ floor: Multiplier written at trigger steps. ``0.0`` = full silence,
1968
+ ``1.0`` = no effect. Values in between give partial ducking.
1969
+ grid: Grid resolution (defaults to ``p.grid``).
1970
+
1971
+ Returns:
1972
+ ``List[float]`` of length ``grid``.
1973
+
1974
+ Example::
1975
+
1976
+ # Full duck on kick hits
1977
+ p.data["kick_sc"] = p.duck_map(kick_steps)
1978
+
1979
+ # Softer duck
1980
+ p.data["kick_sc"] = p.duck_map(kick_steps, floor=0.3)
1981
+
1982
+ # Velocity-proportional: deeper duck for harder kicks
1983
+ p.data["kick_sc"] = p.duck_map(kick_steps, floor=1.0 - (velocity / 127))
1984
+ """
1985
+
1986
+ if grid is None:
1987
+ grid = self._default_grid
1988
+
1989
+ trigger = set(steps)
1990
+ return [floor if s in trigger else 1.0 for s in range(grid)]
1991
+
1992
+ def build_velocity_ramp (
1993
+ self,
1994
+ low: int,
1995
+ high: int,
1996
+ shape: str = "linear",
1997
+ grid: typing.Optional[int] = None,
1998
+ ) -> typing.List[int]:
1999
+
2000
+ """
2001
+ Build a per-step velocity list that ramps from *low* to *high*.
2002
+
2003
+ A musician-friendly shortcut for the common pattern of generating
2004
+ a fixed-length velocity sweep using an easing curve. Returns
2005
+ ``List[int]`` ready to pass directly to ``velocities=`` parameters.
2006
+
2007
+ Parameters:
2008
+ low: Velocity at the first step (0–127).
2009
+ high: Velocity at the last step (0–127).
2010
+ shape: Easing curve name (see ``subsequence.easing``). Common
2011
+ values: ``"linear"``, ``"ease_in"``, ``"ease_out"``,
2012
+ ``"ease_in_out"``. Defaults to ``"linear"``.
2013
+ grid: Number of steps (defaults to ``p.grid``).
2014
+
2015
+ Returns:
2016
+ ``List[int]`` of length ``grid``, values clamped to 0–127.
2017
+
2018
+ Example::
2019
+
2020
+ # Snare roll that swells into a downbeat
2021
+ p.sequence(
2022
+ steps=range(16),
2023
+ pitches="snare_1",
2024
+ durations=0.1,
2025
+ velocities=p.build_velocity_ramp(25, 100, "ease_in"),
2026
+ )
2027
+
2028
+ # Fade-out ghost fill
2029
+ p.ghost_fill("snare_1", 1,
2030
+ velocity=p.build_velocity_ramp(80, 20, "ease_out"),
2031
+ bias="sixteenths", no_overlap=True)
2032
+ """
2033
+
2034
+ if grid is None:
2035
+ grid = self._default_grid
2036
+
2037
+ return [
2038
+ max(0, min(127, int(v)))
2039
+ for v in subsequence.easing.ramp(grid, float(low), float(high), shape)
2040
+ ]
2041
+
2042
+ def scale_velocities (
2043
+ self,
2044
+ factors: typing.Sequence[float],
2045
+ grid: typing.Optional[int] = None,
2046
+ ) -> "PatternBuilder":
2047
+
2048
+ """
2049
+ Scale note velocities by a per-step multiplier list.
2050
+
2051
+ Each note's velocity is multiplied by the factor at the corresponding
2052
+ grid step index. A factor of ``1.0`` leaves the velocity unchanged;
2053
+ ``0.0`` silences the note; ``0.5`` halves it.
2054
+
2055
+ Parameters:
2056
+ factors: Per-step multipliers, one float per grid step.
2057
+ Values outside ``[0.0, 1.0]`` are valid — result is clamped to
2058
+ ``[0, 127]`` after scaling.
2059
+ grid: Grid resolution (defaults to ``p.grid``). Must match the
2060
+ length of ``factors``.
2061
+
2062
+ Returns:
2063
+ ``self`` for fluent chaining.
2064
+
2065
+ Example::
2066
+
2067
+ # Sidechain ducking: silence bass on kick steps, full volume elsewhere.
2068
+ kick_steps = {0, 4, 8, 12}
2069
+ p.data["kick_sc"] = [0.0 if s in kick_steps else 1.0 for s in range(p.grid)]
2070
+
2071
+ # In the bass pattern:
2072
+ p.scale_velocities(p.data.get("kick_sc", [1.0] * p.grid))
2073
+ """
2074
+
2075
+ if grid is None:
2076
+ grid = self._default_grid
2077
+
2078
+ if grid <= 0:
2079
+ return self
2080
+
2081
+ step_duration = self._pattern.length / grid
2082
+ pulses_per_step = step_duration * subsequence.constants.MIDI_QUARTER_NOTE
2083
+
2084
+ for pulse, step in self._pattern.steps.items():
2085
+ # A note in the last half-step rounds up to idx == grid, which is
2086
+ # really the wrap back to step 0 of the next cycle (patterns are
2087
+ # cyclic) — wrap it so a note pushed late by swing/randomize scales
2088
+ # by factors[0] rather than silently keeping its velocity.
2089
+ idx = int(round(pulse / pulses_per_step)) % grid
2090
+
2091
+ if 0 <= idx < len(factors):
2092
+ for note in step.notes:
2093
+ note.velocity = max(0, min(127, int(note.velocity * factors[idx])))
2094
+
2095
+ return self
2096
+
2097
+ def randomize (
2098
+ self,
2099
+ timing: float = 0.03,
2100
+ velocity: float = 0.0,
2101
+ seed: typing.Optional[int] = None,
2102
+ rng: typing.Optional[random.Random] = None
2103
+ ) -> "PatternBuilder":
2104
+
2105
+ """
2106
+ Add random variations to note timing and velocity.
2107
+
2108
+ Introduces small imperfections — the micro-variations that distinguish
2109
+ a played performance from a perfectly quantized sequence.
2110
+
2111
+ Called with no arguments, only timing variation is applied
2112
+ (velocity defaults to 0.0 — no change). Pass a velocity value
2113
+ to also randomise dynamics:
2114
+
2115
+ # Timing only (default)
2116
+ p.randomize()
2117
+
2118
+ # Both axes
2119
+ p.randomize(timing=0.04, velocity=0.08)
2120
+
2121
+ # Stronger feel
2122
+ p.randomize(timing=0.08, velocity=0.15)
2123
+
2124
+ Resolution note: the sequencer runs at 24 PPQN. At 120 BPM, one
2125
+ pulse ≈ 20ms. Timing shifts smaller than roughly 0.04 beats may
2126
+ have no audible effect because they round to zero pulses.
2127
+ Recommended range: timing=0.02–0.08, velocity=0.05–0.15.
2128
+
2129
+ When the composition has a seed set, ``p.rng`` is deterministic,
2130
+ so ``p.randomize()`` produces the same result on every run.
2131
+
2132
+ Parameters:
2133
+ timing: Maximum timing offset in beats (e.g. 0.05 = ±1.2
2134
+ pulses at 24 PPQN). Notes shift by a random amount
2135
+ within ``[-timing, +timing]`` beats. Clamped to
2136
+ pulse 0 at the lower bound.
2137
+ velocity: Maximum velocity scale factor (0.0 to 1.0). Each
2138
+ note's velocity is multiplied by a random value in
2139
+ ``[1 - velocity, 1 + velocity]``, clamped to 1–127.
2140
+ seed: Fix the variations for this call (an int); omit to use the
2141
+ pattern's RNG (seeded when the composition has a seed).
2142
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
2143
+ """
2144
+
2145
+ rng = self._rng_from(seed, rng)
2146
+
2147
+ max_timing_pulses = timing * subsequence.constants.MIDI_QUARTER_NOTE
2148
+ new_steps: typing.Dict[int, subsequence.pattern.Step] = {}
2149
+
2150
+ for pulse, step in self._pattern.steps.items():
2151
+
2152
+ if timing != 0.0:
2153
+ offset = rng.uniform(-max_timing_pulses, max_timing_pulses)
2154
+ new_pulse = max(0, int(round(pulse + offset)))
2155
+ else:
2156
+ new_pulse = pulse
2157
+
2158
+ if new_pulse not in new_steps:
2159
+ new_steps[new_pulse] = subsequence.pattern.Step()
2160
+
2161
+ # Process notes: randomise velocity once per note, then place in new bucket.
2162
+ for note in step.notes:
2163
+ if velocity != 0.0:
2164
+ scale = rng.uniform(1.0 - velocity, 1.0 + velocity)
2165
+ note.velocity = max(1, min(127, int(round(note.velocity * scale))))
2166
+
2167
+ new_steps[new_pulse].notes.append(note)
2168
+
2169
+ self._pattern.steps = new_steps
2170
+ return self
2171
+
2172
+ def legato (self, ratio: float = 1.0) -> "PatternBuilder":
2173
+
2174
+ """
2175
+ Adjust note durations to fill the gap until the next note.
2176
+
2177
+ Parameters:
2178
+ ratio: How much of the gap to fill (0.0 to 1.0).
2179
+ 1.0 is full legato, < 1.0 is staccato.
2180
+ """
2181
+
2182
+ if not self._pattern.steps:
2183
+ return self
2184
+
2185
+ sorted_positions = sorted(self._pattern.steps.keys())
2186
+ total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
2187
+
2188
+ for i, position in enumerate(sorted_positions):
2189
+
2190
+ # Calculate gap to next note
2191
+ if i < len(sorted_positions) - 1:
2192
+ gap = sorted_positions[i + 1] - position
2193
+ else:
2194
+ # Wrap around: gap is distance to end + distance to first note
2195
+ gap = (total_pulses - position) + sorted_positions[0]
2196
+
2197
+ # Apply ratio and enforce minimum duration
2198
+ new_duration = max(1, int(gap * ratio))
2199
+
2200
+ step = self._pattern.steps[position]
2201
+ for note in step.notes:
2202
+ note.duration = new_duration
2203
+ return self
2204
+
2205
+ def duration (self, beats: float) -> "PatternBuilder":
2206
+
2207
+ """
2208
+ Set every note's duration to a fixed length in beats.
2209
+
2210
+ This overrides any existing note durations, acting as a global
2211
+ 'gate time' relative to the beat (1.0 = a quarter note). Short
2212
+ values clip notes tight; long values let them ring. For a
2213
+ guaranteed gap before each next onset regardless of note spacing,
2214
+ use :meth:`detached`; for a classic staccato articulation, either
2215
+ a short fixed value (``p.duration(0.1)``) or ``p.detached()`` works.
2216
+
2217
+ Parameters:
2218
+ beats: Fixed note duration in beats (relative to a quarter note).
2219
+ 0.5 = eighth-note length, 0.25 = sixteenth-note length. Must be positive.
2220
+ """
2221
+
2222
+ if beats <= 0:
2223
+ raise ValueError("Note duration (beats) must be positive")
2224
+
2225
+ duration_pulses = int(beats * subsequence.constants.MIDI_QUARTER_NOTE)
2226
+ duration_pulses = max(1, duration_pulses)
2227
+
2228
+ for step in self._pattern.steps.values():
2229
+ for note in step.notes:
2230
+ note.duration = duration_pulses
2231
+ return self
2232
+
2233
+ def detached (self, beats: float = 0.05) -> "PatternBuilder":
2234
+
2235
+ """
2236
+ Shorten note durations so a guaranteed silence precedes the next onset.
2237
+
2238
+ The complement of :meth:`legato`. For every placed note, the duration
2239
+ is shrunk so that at least ``beats`` beats of silence remain before
2240
+ the next note begins (wrapping around to the first note for the last
2241
+ one). Use this when you want a clean detached articulation, or as a
2242
+ polyphony-safety margin between chord transitions on a monophonic or
2243
+ voice-limited synth.
2244
+
2245
+ Parameters:
2246
+ beats: Minimum gap in beats before the next onset (default 0.05
2247
+ — roughly 25 ms at 120 BPM). Must be positive.
2248
+
2249
+ Example::
2250
+
2251
+ # Bassline on a mono synth: each 16th note ends 0.05 beats
2252
+ # before the next, so the synth never retriggers mid-note.
2253
+ p.arpeggio(chord.tones(36, count=4), spacing=0.25).detached()
2254
+
2255
+ # Explicit larger gap for a longer release tail.
2256
+ p.melody(state, spacing=0.25).detached(0.1)
2257
+ """
2258
+
2259
+ if beats <= 0:
2260
+ raise ValueError("detached beats must be positive")
2261
+
2262
+ if not self._pattern.steps:
2263
+ return self
2264
+
2265
+ sorted_positions = sorted(self._pattern.steps.keys())
2266
+ total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
2267
+ detached_pulses = int(beats * subsequence.constants.MIDI_QUARTER_NOTE)
2268
+
2269
+ for i, position in enumerate(sorted_positions):
2270
+
2271
+ # Calculate gap to next note (wrap-around for the last one)
2272
+ if i < len(sorted_positions) - 1:
2273
+ gap = sorted_positions[i + 1] - position
2274
+ else:
2275
+ gap = (total_pulses - position) + sorted_positions[0]
2276
+
2277
+ new_duration = max(1, gap - detached_pulses)
2278
+
2279
+ for note in self._pattern.steps[position].notes:
2280
+ note.duration = new_duration
2281
+ return self
2282
+
2283
+ def snap_to_scale (self, key: str, mode: str = "ionian", strength: float = 1.0, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> "PatternBuilder":
2284
+
2285
+ """
2286
+ Snap all notes in the pattern to the nearest pitch in a scale.
2287
+
2288
+ Useful after generative or sensor-driven pitch work (random walks,
2289
+ mapping data values to note numbers, etc.) to ensure every note lands
2290
+ on a musically valid scale degree. The snap is applied in
2291
+ place; notes already on a scale degree are left unchanged.
2292
+
2293
+ When a note falls equidistant between two scale tones, the upward
2294
+ direction is preferred.
2295
+
2296
+ Parameters:
2297
+ key: Root note name (e.g. ``"C"``, ``"F#"``, ``"Bb"``).
2298
+ mode: Scale mode. Any key in :data:`subsequence.intervals.DIATONIC_MODE_MAP`
2299
+ is accepted: ``"ionian"`` (default), ``"dorian"``, ``"minor"``,
2300
+ ``"harmonic_minor"``, etc.
2301
+ strength: Probability that each note is snapped (0.0–1.0).
2302
+ At 1.0 (default), every note snaps to the scale.
2303
+ At 0.0, no notes are affected.
2304
+ Values in between create melodies that are mostly in key
2305
+ with occasional chromatic passing tones. Uses the
2306
+ pattern's seeded RNG for reproducibility.
2307
+ seed: Fix the partial-strength snapping for this call (an int);
2308
+ omit to use the pattern's RNG.
2309
+ rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
2310
+
2311
+ Example:
2312
+ ```python
2313
+ @composition.pattern(channel=1, beats=4)
2314
+ def melody (p):
2315
+ for beat in range(16):
2316
+ pitch = 60 + random.randint(-5, 5)
2317
+ p.note(pitch, beat=beat * 0.25)
2318
+ p.snap_to_scale("G", "dorian", strength=0.8)
2319
+ ```
2320
+ """
2321
+
2322
+ rng = self._rng_from(seed, rng)
2323
+
2324
+ key_pc = subsequence.chords.key_name_to_pc(key)
2325
+ scale_pcs = subsequence.intervals.scale_pitch_classes(key_pc, mode)
2326
+
2327
+ for step in self._pattern.steps.values():
2328
+ for note in step.notes:
2329
+ if strength >= 1.0 or rng.random() < strength:
2330
+ note.pitch = subsequence.intervals.quantize_pitch(note.pitch, scale_pcs)
2331
+ return self
2332
+
2333
+ def apply_tuning (
2334
+ self,
2335
+ tuning: "subsequence.tuning.Tuning",
2336
+ bend_range: float = 2.0,
2337
+ channels: typing.Optional[typing.List[int]] = None,
2338
+ reference_note: int = 60,
2339
+ ) -> "PatternBuilder":
2340
+
2341
+ """Apply a microtonal tuning to this pattern via pitch bend injection.
2342
+
2343
+ For each note in the pattern, the nearest 12-TET MIDI pitch is
2344
+ computed and a pitchwheel ``CcEvent`` is injected at the note's onset
2345
+ to shift the synthesiser to the exact tuned frequency. Existing pitch
2346
+ bend events (from ``p.portamento()``, ``p.slide()``, etc.) are shifted
2347
+ additively so they still work correctly within the tuned pitch space.
2348
+
2349
+ For polyphonic patterns, supply a ``channels`` pool. Notes will be
2350
+ spread across those channels so each can carry an independent pitch
2351
+ bend. For monophonic patterns, leave ``channels=None``.
2352
+
2353
+ The synthesiser's pitch-bend range must match ``bend_range``. Most
2354
+ synths default to ±2 semitones. For tunings that deviate more than
2355
+ one semitone from 12-TET, increase ``bend_range`` (e.g., 12 or 24)
2356
+ and configure the synth to match.
2357
+
2358
+ Parameters:
2359
+ tuning: The :class:`~subsequence.tuning.Tuning` to apply.
2360
+ bend_range: Synth pitch-bend range in semitones (default ±2).
2361
+ channels: Channel pool for polyphonic rotation. ``None`` keeps
2362
+ all notes on the pattern's own channel.
2363
+ reference_note: MIDI note number that maps to scale degree 0.
2364
+ Default 60 (middle C).
2365
+
2366
+ Example:
2367
+ ```python
2368
+ from subsequence import Tuning
2369
+
2370
+ meantone = Tuning.from_scl("meanquar.scl")
2371
+
2372
+ @composition.pattern(channel=1, beats=4)
2373
+ def melody (p):
2374
+ p.seq("x x x x", pitch=60)
2375
+ p.apply_tuning(meantone, bend_range=2.0)
2376
+ ```
2377
+ """
2378
+ import subsequence.tuning
2379
+ subsequence.tuning.apply_tuning_to_pattern(
2380
+ self._pattern,
2381
+ tuning,
2382
+ bend_range=bend_range,
2383
+ channels=channels,
2384
+ reference_note=reference_note,
2385
+ )
2386
+ self._tuning_applied = True
2387
+ return self
2388
+
2389
+ def reverse (self) -> "PatternBuilder":
2390
+
2391
+ """
2392
+ Flip the pattern backwards in time (retrograde).
2393
+ """
2394
+
2395
+ total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
2396
+ old_steps = self._pattern.steps
2397
+ new_steps: typing.Dict[int, subsequence.pattern.Step] = {}
2398
+
2399
+ for position, step in old_steps.items():
2400
+ # Reflect around the bar so onsets stay on the grid and the downbeat
2401
+ # is fixed — a true retrograde reverses the inter-onset intervals
2402
+ # (e.g. [0, 24] → [0, 72] in a 96-pulse bar, not the off-grid
2403
+ # [71, 95] the old (total-1)-position produced).
2404
+ new_position = (total_pulses - position) % total_pulses
2405
+
2406
+ if new_position not in new_steps:
2407
+ new_steps[new_position] = subsequence.pattern.Step()
2408
+
2409
+ new_steps[new_position].notes.extend(step.notes)
2410
+
2411
+ self._pattern.steps = new_steps
2412
+ return self
2413
+
2414
+ def stretch (self, factor: float) -> "PatternBuilder":
2415
+
2416
+ """
2417
+ Stretch the pattern in time, scaling note positions and durations.
2418
+
2419
+ ``stretch(2.0)`` makes everything twice as long (half speed) — what
2420
+ theorists call *augmentation*; ``stretch(0.5)`` squeezes the pattern
2421
+ into half the time (double speed) — *diminution*. Any positive
2422
+ factor works: ``stretch(2/3)`` compresses a dotted feel into
2423
+ straight time, for example.
2424
+
2425
+ Notes whose start lands past the end of the pattern are dropped,
2426
+ and compression leaves the freed space empty — the pattern is not
2427
+ tiled to fill it. Durations scale without clipping, so a stretched
2428
+ note may ring past the pattern's end exactly like a legato note,
2429
+ and ``stretch(1.0)`` is a true no-op. Positions and durations
2430
+ truncate to the pulse grid (matching ``note()``'s beat-to-pulse
2431
+ truncation).
2432
+
2433
+ Parameters:
2434
+ factor: Time multiplier. Greater than 1.0 slows the pattern
2435
+ down, less than 1.0 speeds it up. Must be positive.
2436
+ """
2437
+
2438
+ if factor <= 0:
2439
+ raise ValueError("Stretch factor must be positive")
2440
+
2441
+ total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
2442
+ old_steps = self._pattern.steps
2443
+ new_steps: typing.Dict[int, subsequence.pattern.Step] = {}
2444
+
2445
+ for position, step in old_steps.items():
2446
+ new_position = int(position * factor)
2447
+
2448
+ if new_position >= total_pulses:
2449
+ continue
2450
+
2451
+ if new_position not in new_steps:
2452
+ new_steps[new_position] = subsequence.pattern.Step()
2453
+
2454
+ new_steps[new_position].notes.extend(
2455
+ dataclasses.replace(
2456
+ note,
2457
+ duration = max(1, int(note.duration * factor)),
2458
+ )
2459
+ for note in step.notes
2460
+ )
2461
+
2462
+ self._pattern.steps = new_steps
2463
+ return self
2464
+
2465
+ def rotate (self, steps: int, grid: typing.Optional[int] = None) -> "PatternBuilder":
2466
+
2467
+ """
2468
+ Rotate the pattern by a number of grid steps, wrapping around.
2469
+
2470
+ Notes pushed past the end of the pattern re-enter at the start
2471
+ (and vice versa for negative values) — the step-sequencer rotation
2472
+ familiar from Euclidean rhythm tools.
2473
+
2474
+ Parameters:
2475
+ steps: Positive values rotate later in time, negative values earlier.
2476
+ grid: The grid resolution. Defaults to the pattern's
2477
+ ``default_grid`` (derived from the decorator's ``beats``/``steps``
2478
+ and ``step_duration``).
2479
+ """
2480
+
2481
+ if grid is None:
2482
+ grid = self._default_grid
2483
+
2484
+ if grid <= 0:
2485
+ return self
2486
+
2487
+ total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
2488
+ pulses_per_step = total_pulses / grid
2489
+ shift_pulses = int(steps * pulses_per_step)
2490
+
2491
+ old_steps = self._pattern.steps
2492
+ new_steps: typing.Dict[int, subsequence.pattern.Step] = {}
2493
+
2494
+ for position, step in old_steps.items():
2495
+ new_position = (position + shift_pulses) % total_pulses
2496
+
2497
+ if new_position not in new_steps:
2498
+ new_steps[new_position] = subsequence.pattern.Step()
2499
+
2500
+ new_steps[new_position].notes.extend(step.notes)
2501
+
2502
+ self._pattern.steps = new_steps
2503
+ return self
2504
+
2505
+ def transpose (self, semitones: int) -> "PatternBuilder":
2506
+
2507
+ """
2508
+ Shift all note pitches up or down.
2509
+
2510
+ Parameters:
2511
+ semitones: Positive for up, negative for down.
2512
+ """
2513
+
2514
+ for step in self._pattern.steps.values():
2515
+
2516
+ for note in step.notes:
2517
+ note.pitch = max(0, min(127, note.pitch + semitones))
2518
+ return self
2519
+
2520
+ def invert (self, pivot: int = 60) -> "PatternBuilder":
2521
+
2522
+ """
2523
+ Invert all pitches around a pivot note.
2524
+ """
2525
+
2526
+ for step in self._pattern.steps.values():
2527
+
2528
+ for note in step.notes:
2529
+ note.pitch = max(0, min(127, pivot + (pivot - note.pitch)))
2530
+ return self
2531
+
2532
+ def every (self, n: int, fn: typing.Callable[["PatternBuilder"], None]) -> "PatternBuilder":
2533
+
2534
+ """
2535
+ Apply a transformation every Nth cycle.
2536
+
2537
+ Parameters:
2538
+ n: The cycle frequency (e.g., 4 = every 4th bar).
2539
+ fn: A function (often a lambda) that receives the builder and
2540
+ calls further methods.
2541
+
2542
+ Example:
2543
+ ```python
2544
+ # Reverse every 4th bar
2545
+ p.every(4, lambda p: p.reverse())
2546
+ ```
2547
+ """
2548
+
2549
+ if n < 1:
2550
+ raise ValueError(f"every() cycle length must be at least 1 bar — got {n} (every(1, ...) applies the change every bar)")
2551
+
2552
+ if self.cycle % n == 0:
2553
+ fn(self)
2554
+ return self
2555
+
2556
+ def bar_cycle (self, length: int) -> BarCycle:
2557
+
2558
+ """Return the current bar's position within a repeating cycle of bars.
2559
+
2560
+ A thin wrapper around ``p.bar % length`` that replaces opaque modulo
2561
+ arithmetic with readable, musician-friendly properties.
2562
+
2563
+ Parameters:
2564
+ length: The cycle length in bars (e.g., 4, 8, 16).
2565
+
2566
+ Returns:
2567
+ A :class:`BarCycle` with ``.bar``, ``.first``, ``.last``,
2568
+ and ``.progress`` properties.
2569
+
2570
+ Example:
2571
+ ```python
2572
+ # Every 4 bars (replaces: if p.bar % 4 == 0)
2573
+ if p.bar_cycle(4).first:
2574
+ p.hit_steps("snare_1", [0, 8], velocity=110)
2575
+
2576
+ # Last bar of every 16-bar cycle (replaces: if p.bar % 16 == 15)
2577
+ if p.bar_cycle(16).last:
2578
+ p.euclidean("hi_hat_open", 3)
2579
+
2580
+ # Build intensity over an 8-bar arc
2581
+ intensity = p.bar_cycle(8).progress # 0.0 → 0.875
2582
+ p.velocity_shape(low=int(40 + 40 * intensity), high=100)
2583
+ ```
2584
+ """
2585
+
2586
+ if length < 1:
2587
+ raise ValueError(f"bar_cycle() cycle length must be at least 1 bar — got {length}")
2588
+
2589
+ return BarCycle(bar=self.bar % length, length=length)