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.
- subsequence/__init__.py +231 -0
- subsequence/__main__.py +24 -0
- subsequence/assets/web/index.html +345 -0
- subsequence/cadences.py +113 -0
- subsequence/chord_graphs/__init__.py +100 -0
- subsequence/chord_graphs/aeolian_minor.py +158 -0
- subsequence/chord_graphs/chromatic_mediant.py +113 -0
- subsequence/chord_graphs/diminished.py +97 -0
- subsequence/chord_graphs/dorian_minor.py +127 -0
- subsequence/chord_graphs/functional_major.py +102 -0
- subsequence/chord_graphs/hooktheory_major.py +88 -0
- subsequence/chord_graphs/lydian_major.py +130 -0
- subsequence/chord_graphs/mixolydian.py +98 -0
- subsequence/chord_graphs/phrygian_minor.py +76 -0
- subsequence/chord_graphs/suspended.py +109 -0
- subsequence/chord_graphs/turnaround_global.py +157 -0
- subsequence/chord_graphs/whole_tone.py +77 -0
- subsequence/chords.py +419 -0
- subsequence/composition.py +6099 -0
- subsequence/conductor.py +238 -0
- subsequence/constants/__init__.py +24 -0
- subsequence/constants/durations.py +37 -0
- subsequence/constants/instruments/__init__.py +13 -0
- subsequence/constants/instruments/gm_cc.py +46 -0
- subsequence/constants/instruments/gm_drums.py +53 -0
- subsequence/constants/instruments/gm_instruments.py +32 -0
- subsequence/constants/instruments/roland_tr8s.py +320 -0
- subsequence/constants/instruments/vermona_drm1_drums.py +87 -0
- subsequence/constants/midi_notes.py +29 -0
- subsequence/constants/pulses.py +17 -0
- subsequence/constants/velocity.py +22 -0
- subsequence/definitions.py +232 -0
- subsequence/display.py +617 -0
- subsequence/easing.py +347 -0
- subsequence/event_emitter.py +109 -0
- subsequence/form_state.py +665 -0
- subsequence/forms.py +257 -0
- subsequence/groove.py +323 -0
- subsequence/harmonic_rhythm.py +83 -0
- subsequence/harmonic_state.py +352 -0
- subsequence/harmony.py +197 -0
- subsequence/held_notes.py +91 -0
- subsequence/helpers/__init__.py +0 -0
- subsequence/helpers/network.py +58 -0
- subsequence/helpers/wing.py +430 -0
- subsequence/intervals.py +436 -0
- subsequence/keystroke.py +249 -0
- subsequence/link_clock.py +128 -0
- subsequence/live_client.py +187 -0
- subsequence/live_reloader.py +298 -0
- subsequence/live_server.py +161 -0
- subsequence/melodic_state.py +483 -0
- subsequence/midi.py +97 -0
- subsequence/midi_utils.py +329 -0
- subsequence/mini_notation.py +164 -0
- subsequence/motifs.py +2356 -0
- subsequence/osc.py +194 -0
- subsequence/pattern.py +363 -0
- subsequence/pattern_algorithmic.py +2010 -0
- subsequence/pattern_builder.py +2589 -0
- subsequence/pattern_midi.py +1208 -0
- subsequence/progressions.py +1913 -0
- subsequence/py.typed +0 -0
- subsequence/roles.py +63 -0
- subsequence/sequence_utils.py +3123 -0
- subsequence/sequencer.py +2086 -0
- subsequence/tuning.py +453 -0
- subsequence/voicings.py +151 -0
- subsequence/web_ui.py +337 -0
- subsequence/weighted_graph.py +156 -0
- subsequence-0.6.4.dist-info/METADATA +208 -0
- subsequence-0.6.4.dist-info/RECORD +78 -0
- subsequence-0.6.4.dist-info/WHEEL +5 -0
- subsequence-0.6.4.dist-info/entry_points.txt +2 -0
- subsequence-0.6.4.dist-info/licenses/LICENSE +661 -0
- subsequence-0.6.4.dist-info/scm_file_list.json +193 -0
- subsequence-0.6.4.dist-info/scm_version.json +8 -0
- subsequence-0.6.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2010 @@
|
|
|
1
|
+
"""Mixin class providing algorithmic and generative pattern-building methods.
|
|
2
|
+
|
|
3
|
+
This module is not intended to be used directly. ``PatternAlgorithmicMixin``
|
|
4
|
+
is inherited by ``PatternBuilder`` in ``pattern_builder.py``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import random
|
|
8
|
+
import typing
|
|
9
|
+
import warnings
|
|
10
|
+
import subsequence.constants
|
|
11
|
+
import subsequence.constants.velocity
|
|
12
|
+
import subsequence.easing
|
|
13
|
+
import subsequence.melodic_state
|
|
14
|
+
import subsequence.pattern
|
|
15
|
+
import subsequence.sequence_utils
|
|
16
|
+
import subsequence.weighted_graph
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PatternAlgorithmicMixin:
|
|
20
|
+
|
|
21
|
+
"""Algorithmic and generative note-placement methods for PatternBuilder.
|
|
22
|
+
|
|
23
|
+
All methods here operate on ``self._pattern`` (a ``Pattern`` instance)
|
|
24
|
+
and ``self.rng`` (a ``random.Random`` instance), both of which are set
|
|
25
|
+
by ``PatternBuilder.__init__``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# ── Instance attributes provided by PatternBuilder at runtime ────────
|
|
29
|
+
# Declared here so mypy can type-check all methods in this mixin.
|
|
30
|
+
|
|
31
|
+
_pattern: subsequence.pattern.Pattern
|
|
32
|
+
_default_grid: int
|
|
33
|
+
rng: random.Random
|
|
34
|
+
cycle: int
|
|
35
|
+
data: typing.Dict[str, typing.Any]
|
|
36
|
+
key: typing.Optional[str]
|
|
37
|
+
scale: typing.Optional[str]
|
|
38
|
+
|
|
39
|
+
if typing.TYPE_CHECKING:
|
|
40
|
+
# Cross-mixin method stubs: implemented by PatternBuilder,
|
|
41
|
+
# called from methods in this mixin.
|
|
42
|
+
import subsequence.pattern_builder # noqa: F401 — type-checking only
|
|
43
|
+
def note (
|
|
44
|
+
self,
|
|
45
|
+
pitch: typing.Union[int, str],
|
|
46
|
+
beat: float,
|
|
47
|
+
velocity: typing.Union[int, typing.Tuple[int, int]],
|
|
48
|
+
duration: float,
|
|
49
|
+
) -> "subsequence.pattern_builder.PatternBuilder": ...
|
|
50
|
+
def _resolve_pitch (self, pitch: typing.Union[int, str]) -> int: ...
|
|
51
|
+
def _resolve_pitch_lenient (self, pitch: typing.Union[int, str]) -> typing.Optional[int]: ...
|
|
52
|
+
def _has_pitch_at_beat (self, pitch: typing.Union[int, str], beat: float) -> bool: ...
|
|
53
|
+
|
|
54
|
+
def _rng_from (self, seed: typing.Optional[int], rng: typing.Optional[random.Random]) -> random.Random:
|
|
55
|
+
|
|
56
|
+
"""Resolve the effective random generator for a generative call.
|
|
57
|
+
|
|
58
|
+
Determinism has one friendly knob — ``seed=`` (an int) — and one advanced
|
|
59
|
+
form — ``rng=`` (a ``random.Random`` instance). Precedence, most explicit
|
|
60
|
+
first:
|
|
61
|
+
|
|
62
|
+
1. ``rng=`` — an explicit generator you supplied (wins; warns if ``seed=`` was also given).
|
|
63
|
+
2. ``seed=`` — a fresh ``random.Random(seed)``, fixed for this call.
|
|
64
|
+
3. ``self.rng`` — the pattern's own generator (the default; reproducible under the composition seed).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if rng is not None:
|
|
68
|
+
if seed is not None:
|
|
69
|
+
warnings.warn("seed= and rng= were both given — rng= wins; pass only one", stacklevel=3)
|
|
70
|
+
return rng
|
|
71
|
+
|
|
72
|
+
if seed is not None:
|
|
73
|
+
return random.Random(seed)
|
|
74
|
+
|
|
75
|
+
return self.rng
|
|
76
|
+
|
|
77
|
+
def _resolve_velocity (self, velocity: typing.Union[int, typing.Tuple[int, int]], rng: typing.Optional[random.Random] = None) -> int:
|
|
78
|
+
|
|
79
|
+
"""Resolve a velocity argument to a single integer.
|
|
80
|
+
|
|
81
|
+
Accepts a plain ``int`` (returned unchanged) or a ``(low, high)``
|
|
82
|
+
tuple from which a random value is drawn via ``rng.randint``.
|
|
83
|
+
Centralised here so every note-placement method offers the same
|
|
84
|
+
idiom — ``velocity=(60, 90)`` works wherever ``velocity=`` is
|
|
85
|
+
accepted.
|
|
86
|
+
|
|
87
|
+
Raises ``TypeError`` for any other shape so a typo surfaces at
|
|
88
|
+
the call site rather than as a malformed MIDI event later in
|
|
89
|
+
the sequencer dispatch loop.
|
|
90
|
+
|
|
91
|
+
Parameters:
|
|
92
|
+
velocity: An int 0-127 or a ``(low, high)`` 2-tuple
|
|
93
|
+
describing an inclusive random range.
|
|
94
|
+
rng: Random generator. Defaults to ``self.rng``.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A single integer velocity.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
if isinstance(velocity, tuple):
|
|
101
|
+
if len(velocity) != 2:
|
|
102
|
+
raise ValueError(f"velocity tuple must be (low, high), got {velocity!r}")
|
|
103
|
+
|
|
104
|
+
low, high = int(velocity[0]), int(velocity[1])
|
|
105
|
+
if low > high:
|
|
106
|
+
raise ValueError(f"velocity range must be (low, high) with low <= high, got {velocity!r}")
|
|
107
|
+
|
|
108
|
+
if rng is None:
|
|
109
|
+
rng = self.rng
|
|
110
|
+
|
|
111
|
+
return rng.randint(low, high)
|
|
112
|
+
if isinstance(velocity, bool):
|
|
113
|
+
raise TypeError(f"velocity must be int or (low, high) tuple, got bool: {velocity!r}")
|
|
114
|
+
if isinstance(velocity, (int, float)):
|
|
115
|
+
return int(velocity)
|
|
116
|
+
raise TypeError(f"velocity must be int or (low, high) tuple, got {type(velocity).__name__}: {velocity!r}")
|
|
117
|
+
|
|
118
|
+
def _place_gated_sequence (
|
|
119
|
+
self,
|
|
120
|
+
sequence: typing.Sequence[typing.Any],
|
|
121
|
+
event_for: typing.Callable[[int, typing.Any], typing.Optional[typing.Tuple[typing.Union[int, str], typing.Union[int, typing.Tuple[int, int]], float]]],
|
|
122
|
+
probability: float,
|
|
123
|
+
rng: random.Random,
|
|
124
|
+
no_overlap: bool = False,
|
|
125
|
+
) -> None:
|
|
126
|
+
|
|
127
|
+
"""Place per-step events from a sequence, gated by probability and overlap.
|
|
128
|
+
|
|
129
|
+
The shared placement kernel: steps are evenly spaced across the pattern
|
|
130
|
+
length; for each step, ``event_for(index, value)`` returns either
|
|
131
|
+
``None`` (a silent step — no probability draw is consumed) or a
|
|
132
|
+
``(pitch, velocity, duration)`` event. Surviving events pass the
|
|
133
|
+
probability gate and, when ``no_overlap`` is set, the same-pitch check,
|
|
134
|
+
then land via ``self.note()`` (which resolves ``(low, high)`` velocity
|
|
135
|
+
tuples per hit).
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
if not sequence:
|
|
139
|
+
return # nothing to place (e.g. a pattern shorter than one step)
|
|
140
|
+
|
|
141
|
+
step_duration = self._pattern.length / len(sequence)
|
|
142
|
+
|
|
143
|
+
for i, value in enumerate(sequence):
|
|
144
|
+
|
|
145
|
+
event = event_for(i, value)
|
|
146
|
+
|
|
147
|
+
if event is None:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if probability < 1.0 and rng.random() < (1.0 - probability):
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
pitch, velocity, duration = event
|
|
154
|
+
|
|
155
|
+
if no_overlap and self._has_pitch_at_beat(pitch, i * step_duration):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
self.note(pitch=pitch, beat=i * step_duration, velocity=velocity, duration=duration)
|
|
159
|
+
|
|
160
|
+
def _place_rhythm_sequence (
|
|
161
|
+
self,
|
|
162
|
+
sequence: typing.List[int],
|
|
163
|
+
pitch: typing.Union[int, str],
|
|
164
|
+
velocity: typing.Union[int, typing.Tuple[int, int]],
|
|
165
|
+
duration: float,
|
|
166
|
+
probability: float,
|
|
167
|
+
rng: random.Random,
|
|
168
|
+
no_overlap: bool = False
|
|
169
|
+
) -> None:
|
|
170
|
+
|
|
171
|
+
"""Place hits from a binary sequence into the pattern.
|
|
172
|
+
|
|
173
|
+
Shared implementation for ``euclidean()``, ``bresenham()``, and the
|
|
174
|
+
other single-voice binary-rhythm verbs. Each active step (1) becomes
|
|
175
|
+
a note; zeros are rests.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def _event (i: int, hit_value: typing.Any) -> typing.Optional[typing.Tuple[typing.Union[int, str], typing.Union[int, typing.Tuple[int, int]], float]]:
|
|
179
|
+
if hit_value == 0:
|
|
180
|
+
return None
|
|
181
|
+
return (pitch, velocity, duration)
|
|
182
|
+
|
|
183
|
+
self._place_gated_sequence(sequence, _event, probability, rng, no_overlap=no_overlap)
|
|
184
|
+
|
|
185
|
+
def euclidean (self, pitch: typing.Union[int, str], pulses: int, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, probability: float = 1.0, no_overlap: bool = False, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> "subsequence.pattern_builder.PatternBuilder":
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
Generate a Euclidean rhythm.
|
|
189
|
+
|
|
190
|
+
This distributes a fixed number of 'pulses' as evenly as possible
|
|
191
|
+
across the pattern. This produces many of the world's most
|
|
192
|
+
common musical rhythms.
|
|
193
|
+
|
|
194
|
+
Parameters:
|
|
195
|
+
pitch: MIDI note or drum name.
|
|
196
|
+
pulses: Total number of notes to place.
|
|
197
|
+
velocity: MIDI velocity, or a ``(low, high)`` tuple for a
|
|
198
|
+
fresh random draw per hit.
|
|
199
|
+
duration: Note duration.
|
|
200
|
+
probability: Chance (0.0–1.0) that each pulse plays — 1.0 places them all, lower thins the rhythm.
|
|
201
|
+
seed: Fix the thinning for this call (an int); omit to use the pattern's RNG.
|
|
202
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
203
|
+
no_overlap: If True, skip steps where a note of the same pitch
|
|
204
|
+
already exists. Useful for layering ghost notes around
|
|
205
|
+
hand-placed anchors.
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
```python
|
|
209
|
+
# A classic 3-against-16 rhythm
|
|
210
|
+
p.euclidean("kick", pulses=3)
|
|
211
|
+
```
|
|
212
|
+
"""
|
|
213
|
+
rng = self._rng_from(seed, rng)
|
|
214
|
+
|
|
215
|
+
steps = self._default_grid
|
|
216
|
+
sequence = subsequence.sequence_utils.generate_euclidean_sequence(steps=steps, pulses=pulses)
|
|
217
|
+
self._place_rhythm_sequence(sequence, pitch, velocity, duration, probability, rng, no_overlap=no_overlap)
|
|
218
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
219
|
+
|
|
220
|
+
def bresenham (self, pitch: typing.Union[int, str], pulses: int, velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, probability: float = 1.0, no_overlap: bool = False, seed: typing.Optional[int] = None, rng: typing.Optional[random.Random] = None) -> "subsequence.pattern_builder.PatternBuilder":
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
Generate a rhythm using the Bresenham line algorithm.
|
|
224
|
+
|
|
225
|
+
This is an alternative to Euclidean rhythms that often results in
|
|
226
|
+
slightly different (but still mathematically even) distributions.
|
|
227
|
+
|
|
228
|
+
Parameters:
|
|
229
|
+
pitch: MIDI note or drum name.
|
|
230
|
+
pulses: Total number of notes to place.
|
|
231
|
+
velocity: MIDI velocity, or a ``(low, high)`` tuple for a
|
|
232
|
+
fresh random draw per hit.
|
|
233
|
+
duration: Note duration.
|
|
234
|
+
probability: Chance (0.0–1.0) that each pulse plays — 1.0 places them all, lower thins the rhythm.
|
|
235
|
+
seed: Fix the thinning for this call (an int); omit to use the pattern's RNG.
|
|
236
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
237
|
+
no_overlap: If True, skip steps where a note of the same pitch
|
|
238
|
+
already exists. Useful for layering ghost notes around
|
|
239
|
+
hand-placed anchors.
|
|
240
|
+
"""
|
|
241
|
+
rng = self._rng_from(seed, rng)
|
|
242
|
+
|
|
243
|
+
steps = self._default_grid
|
|
244
|
+
sequence = subsequence.sequence_utils.generate_bresenham_sequence(steps=steps, pulses=pulses)
|
|
245
|
+
self._place_rhythm_sequence(sequence, pitch, velocity, duration, probability, rng, no_overlap=no_overlap)
|
|
246
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
247
|
+
|
|
248
|
+
def bresenham_poly (
|
|
249
|
+
self,
|
|
250
|
+
parts: typing.Dict[typing.Union[int, str], float],
|
|
251
|
+
velocity: typing.Union[int, typing.Dict[typing.Union[int, str], int]] = subsequence.constants.velocity.DEFAULT_VELOCITY,
|
|
252
|
+
duration: float = 0.1,
|
|
253
|
+
grid: typing.Optional[int] = None,
|
|
254
|
+
probability: float = 1.0,
|
|
255
|
+
no_overlap: bool = False,
|
|
256
|
+
seed: typing.Optional[int] = None,
|
|
257
|
+
rng: typing.Optional[random.Random] = None, ) -> "subsequence.pattern_builder.PatternBuilder":
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
Distribute multiple drum voices across the pattern using weighted Bresenham.
|
|
261
|
+
|
|
262
|
+
Each step is assigned to exactly one voice - voices never overlap, producing
|
|
263
|
+
interlocking rhythmic patterns. Density weights control how frequently each
|
|
264
|
+
voice fires. If the weights sum to less than 1.0, the remainder becomes
|
|
265
|
+
evenly-distributed rests (silent steps).
|
|
266
|
+
|
|
267
|
+
Because notes are placed via ``self.note()``, all post-placement transforms
|
|
268
|
+
(``groove``, ``randomize``, ``velocity_shape``, ``rotate``, etc.) work normally.
|
|
269
|
+
|
|
270
|
+
Parameters:
|
|
271
|
+
parts: Mapping of pitch (MIDI note or drum name) to density weight.
|
|
272
|
+
Higher weight means more hits per bar. Weights in the range (0, 1]
|
|
273
|
+
are typical; a weight of 0.5 targets roughly one hit every two steps.
|
|
274
|
+
velocity: Either a single MIDI velocity applied to all voices, or a dict
|
|
275
|
+
mapping each pitch to its own velocity. Pitches absent from the dict
|
|
276
|
+
fall back to the default velocity (100).
|
|
277
|
+
duration: Note duration in beats (default 0.1).
|
|
278
|
+
grid: Number of steps to divide the pattern into. Defaults to the
|
|
279
|
+
pattern's ``default_grid``.
|
|
280
|
+
probability: Chance (0.0–1.0) that each hit plays — 1.0 places them all, lower thins.
|
|
281
|
+
seed: Fix the thinning for this call (an int); omit to use the pattern's RNG.
|
|
282
|
+
no_overlap: If True, skip steps where a note of the same pitch already
|
|
283
|
+
exists. Useful for layering ghost notes around hand-placed anchors.
|
|
284
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
```python
|
|
288
|
+
p.bresenham_poly(
|
|
289
|
+
parts={"kick_1": 0.25, "snare_1": 0.125, "hi_hat_closed": 0.5},
|
|
290
|
+
velocity={"kick_1": 100, "snare_1": 90, "hi_hat_closed": 70},
|
|
291
|
+
)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Layering with hand-placed hits:
|
|
295
|
+
```python
|
|
296
|
+
# Algorithmic base - interlocking texture, no overlaps within this layer
|
|
297
|
+
p.bresenham_poly(
|
|
298
|
+
parts={"hi_hat_closed": 0.5, "snare_2": 0.1},
|
|
299
|
+
velocity={"hi_hat_closed": 65, "snare_2": 40},
|
|
300
|
+
)
|
|
301
|
+
# Hand-placed anchors on top - these CAN overlap the algorithmic layer
|
|
302
|
+
p.hit_steps("kick_1", [0, 8], velocity=110)
|
|
303
|
+
p.hit_steps("snare_1", [4, 12], velocity=100)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Stable vs shifting patterns:
|
|
307
|
+
Because the algorithm redistributes all positions when weights change,
|
|
308
|
+
a single voice with a continuously ramping density will shift positions
|
|
309
|
+
every bar. This is great for background texture (hats, shakers) but
|
|
310
|
+
can sound jarring for prominent, distinctive sounds (claps, cowbells).
|
|
311
|
+
|
|
312
|
+
**For stable patterns** - use ``bresenham()`` with integer pulses.
|
|
313
|
+
Positions stay fixed until the pulse count steps up::
|
|
314
|
+
|
|
315
|
+
pulses = max(1, round(density * 16))
|
|
316
|
+
p.bresenham("hand_clap", pulses=pulses, velocity=95)
|
|
317
|
+
|
|
318
|
+
**For shifting texture** - use ``bresenham_poly()`` with continuous
|
|
319
|
+
density. Positions evolve every bar::
|
|
320
|
+
|
|
321
|
+
p.bresenham_poly(parts={"hi_hat_closed": density}, velocity=70)
|
|
322
|
+
|
|
323
|
+
**To stabilise a solo voice** - pair it with a second voice. More
|
|
324
|
+
voices in a single call means less positional shift per voice::
|
|
325
|
+
|
|
326
|
+
p.bresenham_poly(
|
|
327
|
+
parts={"hand_clap": 0.12, "snare_2": 0.08},
|
|
328
|
+
velocity={"hand_clap": 95, "snare_2": 40},
|
|
329
|
+
)
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
if not parts:
|
|
333
|
+
raise ValueError("parts dict cannot be empty")
|
|
334
|
+
|
|
335
|
+
if any(w < 0 for w in parts.values()):
|
|
336
|
+
raise ValueError("All density weights must be non-negative")
|
|
337
|
+
rng = self._rng_from(seed, rng)
|
|
338
|
+
|
|
339
|
+
if grid is None:
|
|
340
|
+
grid = self._default_grid
|
|
341
|
+
|
|
342
|
+
voice_names = list(parts.keys())
|
|
343
|
+
weights = [parts[name] for name in voice_names]
|
|
344
|
+
|
|
345
|
+
# If weights don't fill the bar, add an implicit rest voice.
|
|
346
|
+
weight_sum = sum(weights)
|
|
347
|
+
rest_index: typing.Optional[int] = None
|
|
348
|
+
if weight_sum < 1.0:
|
|
349
|
+
rest_index = len(voice_names)
|
|
350
|
+
weights.append(1.0 - weight_sum)
|
|
351
|
+
|
|
352
|
+
sequence = subsequence.sequence_utils.generate_bresenham_sequence_weighted(
|
|
353
|
+
steps=grid, weights=weights
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _event (step_idx: int, voice_idx: typing.Any) -> typing.Optional[typing.Tuple[typing.Union[int, str], typing.Union[int, typing.Tuple[int, int]], float]]:
|
|
357
|
+
if voice_idx == rest_index:
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
pitch = voice_names[voice_idx]
|
|
361
|
+
|
|
362
|
+
if isinstance(velocity, dict):
|
|
363
|
+
vel = velocity.get(pitch, subsequence.constants.velocity.DEFAULT_VELOCITY)
|
|
364
|
+
else:
|
|
365
|
+
vel = velocity
|
|
366
|
+
|
|
367
|
+
return (pitch, vel, duration)
|
|
368
|
+
|
|
369
|
+
self._place_gated_sequence(sequence, _event, probability, rng, no_overlap=no_overlap)
|
|
370
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def build_ghost_bias (grid: int, bias: str) -> typing.List[float]:
|
|
374
|
+
|
|
375
|
+
"""Build probability weights for ghost notes or other generative functions.
|
|
376
|
+
|
|
377
|
+
Generates a list of probability weights (values between 0.0 and 1.0) spanning
|
|
378
|
+
a given grid size. These curves shape probability over a beat,
|
|
379
|
+
assigning higher or lower chances of an event occurring based on the rhythmic
|
|
380
|
+
position within the beat (downbeat, offbeat, syncopated 16th note, etc).
|
|
381
|
+
|
|
382
|
+
This is a public escape hatch: call it yourself, manipulate the returned list,
|
|
383
|
+
then pass the result as ``bias=`` to :meth:`ghost_fill()`. This lets you pin
|
|
384
|
+
specific steps, boost a weak position, or combine two named curves.
|
|
385
|
+
|
|
386
|
+
Parameters:
|
|
387
|
+
grid: The total number of steps in the sequence (usually 16 or 32).
|
|
388
|
+
bias: The probability distribution shape to generate:
|
|
389
|
+
|
|
390
|
+
- ``"uniform"`` - 1.0 everywhere.
|
|
391
|
+
- ``"offbeat"`` - 1.0 on 8th note off-beats (&), 0.3 on 16ths (e/a), 0.05 on downbeats.
|
|
392
|
+
- ``"sixteenths"`` - 1.0 on 16th notes (e/a), 0.3 on 8th off-beats (&), 0.05 on downbeats.
|
|
393
|
+
- ``"before"`` - 1.0 preceding a beat, 0.25 on other 16ths, 0.05 on beats.
|
|
394
|
+
- ``"after"`` - 1.0 following a beat, 0.25 on other 16ths, 0.05 on beats.
|
|
395
|
+
- ``"downbeat"`` - 1.0 on downbeats, 0.15 on 8th off-beats, 0.05 on other 16ths.
|
|
396
|
+
- ``"upbeat"`` - 1.0 on 8th note off-beats only, 0.05 everywhere else.
|
|
397
|
+
- ``"e_and_a"`` - 1.0 on all non-downbeat 16th positions, 0.05 on downbeats.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
A ``List[float]`` of length ``grid`` where each value is a probability
|
|
401
|
+
multiplier from 0.0 to 1.0. The list is a plain Python list — modify
|
|
402
|
+
it freely before passing to ``ghost_fill(bias=...)``.
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
```python
|
|
406
|
+
# Start from a named curve, then zero out beat 3 (step 8) entirely
|
|
407
|
+
# and give the step before the snare (step 11) maximum weight.
|
|
408
|
+
weights = p.build_ghost_bias(16, "sixteenths")
|
|
409
|
+
weights[8] = 0.0 # silence around beat 3
|
|
410
|
+
weights[11] = 1.0 # boost the "and" before beat 4
|
|
411
|
+
p.ghost_fill("snare_1", density=0.25, velocity=(25, 45),
|
|
412
|
+
bias=weights, no_overlap=True)
|
|
413
|
+
```
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
steps_per_beat = max(1, grid // 4)
|
|
417
|
+
weights: typing.List[float] = []
|
|
418
|
+
|
|
419
|
+
for i in range(grid):
|
|
420
|
+
pos = i % steps_per_beat
|
|
421
|
+
|
|
422
|
+
if bias == "uniform":
|
|
423
|
+
weights.append(1.0)
|
|
424
|
+
elif bias == "offbeat":
|
|
425
|
+
if pos == 0:
|
|
426
|
+
weights.append(0.05)
|
|
427
|
+
elif steps_per_beat > 1 and pos == steps_per_beat // 2:
|
|
428
|
+
weights.append(1.0)
|
|
429
|
+
else:
|
|
430
|
+
weights.append(0.3)
|
|
431
|
+
elif bias == "sixteenths":
|
|
432
|
+
if pos == 0:
|
|
433
|
+
weights.append(0.05)
|
|
434
|
+
elif steps_per_beat > 1 and pos == steps_per_beat // 2:
|
|
435
|
+
weights.append(0.3)
|
|
436
|
+
else:
|
|
437
|
+
weights.append(1.0)
|
|
438
|
+
elif bias == "before":
|
|
439
|
+
if pos == steps_per_beat - 1:
|
|
440
|
+
weights.append(1.0)
|
|
441
|
+
elif pos == 0:
|
|
442
|
+
weights.append(0.05)
|
|
443
|
+
else:
|
|
444
|
+
weights.append(0.25)
|
|
445
|
+
elif bias == "after":
|
|
446
|
+
if steps_per_beat > 1 and pos == 1:
|
|
447
|
+
weights.append(1.0)
|
|
448
|
+
elif pos == 0:
|
|
449
|
+
weights.append(0.05)
|
|
450
|
+
else:
|
|
451
|
+
weights.append(0.25)
|
|
452
|
+
elif bias == "downbeat":
|
|
453
|
+
if pos == 0:
|
|
454
|
+
weights.append(1.0)
|
|
455
|
+
elif steps_per_beat > 1 and pos == steps_per_beat // 2:
|
|
456
|
+
weights.append(0.15)
|
|
457
|
+
else:
|
|
458
|
+
weights.append(0.05)
|
|
459
|
+
elif bias == "upbeat":
|
|
460
|
+
if steps_per_beat > 1 and pos == steps_per_beat // 2:
|
|
461
|
+
weights.append(1.0)
|
|
462
|
+
else:
|
|
463
|
+
weights.append(0.05)
|
|
464
|
+
elif bias == "e_and_a":
|
|
465
|
+
if pos == 0:
|
|
466
|
+
weights.append(0.05)
|
|
467
|
+
else:
|
|
468
|
+
weights.append(1.0)
|
|
469
|
+
else:
|
|
470
|
+
raise ValueError(
|
|
471
|
+
f"Unknown ghost_fill bias {bias!r}. "
|
|
472
|
+
f"Use 'uniform', 'offbeat', 'sixteenths', 'before', 'after', "
|
|
473
|
+
f"'downbeat', 'upbeat', 'e_and_a', or a list of floats."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return weights
|
|
477
|
+
|
|
478
|
+
def ghost_fill (
|
|
479
|
+
self,
|
|
480
|
+
pitch: typing.Union[int, str],
|
|
481
|
+
density: float = 0.3,
|
|
482
|
+
velocity: typing.Union[
|
|
483
|
+
int,
|
|
484
|
+
typing.Tuple[int, int],
|
|
485
|
+
typing.Sequence[typing.Union[int, float]],
|
|
486
|
+
typing.Callable[[int], typing.Union[int, float]]
|
|
487
|
+
] = subsequence.constants.velocity.GHOST_FILL_VELOCITY,
|
|
488
|
+
bias: typing.Union[str, typing.List[float]] = "uniform",
|
|
489
|
+
no_overlap: bool = True,
|
|
490
|
+
grid: typing.Optional[int] = None,
|
|
491
|
+
duration: float = 0.1,
|
|
492
|
+
seed: typing.Optional[int] = None,
|
|
493
|
+
rng: typing.Optional[random.Random] = None,
|
|
494
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
495
|
+
|
|
496
|
+
"""Fill the pattern with probability-biased ghost notes.
|
|
497
|
+
|
|
498
|
+
A single method for generating musically-aware ghost note layers.
|
|
499
|
+
Combines density control, velocity randomisation, and rhythmic bias
|
|
500
|
+
to produce the micro-detail layering heard in dense electronic
|
|
501
|
+
music production.
|
|
502
|
+
|
|
503
|
+
Parameters:
|
|
504
|
+
pitch: MIDI note number or drum name.
|
|
505
|
+
density: Overall density (0.0–1.0). How many available steps
|
|
506
|
+
receive ghost notes. 0.3 = roughly 30% of steps at peak bias.
|
|
507
|
+
velocity: Single velocity, ``(low, high)`` tuple for random range,
|
|
508
|
+
a list/sequence of values (indexed by step), or a callable
|
|
509
|
+
that takes the step index ``i`` and returns a velocity.
|
|
510
|
+
Allows dynamic values like Perlin noise curves.
|
|
511
|
+
bias: Probability distribution shape:
|
|
512
|
+
|
|
513
|
+
- ``"uniform"`` - equal probability everywhere
|
|
514
|
+
- ``"offbeat"`` - prefer 8th-note off-beats (&)
|
|
515
|
+
- ``"sixteenths"`` - prefer 16th-note subdivisions (e/a)
|
|
516
|
+
- ``"before"`` - cluster just before beat positions
|
|
517
|
+
- ``"after"`` - cluster just after beat positions
|
|
518
|
+
- ``"downbeat"`` - reinforce the beat (inverse of offbeat)
|
|
519
|
+
- ``"upbeat"`` - strictly 8th-note off-beats only
|
|
520
|
+
- ``"e_and_a"`` - all non-downbeat 16th positions
|
|
521
|
+
- Or: a list of floats (one per grid step) for a custom field.
|
|
522
|
+
Use :meth:`build_ghost_bias` to generate a named curve
|
|
523
|
+
and then modify specific steps before passing it here.
|
|
524
|
+
|
|
525
|
+
no_overlap: If True (default), skip where same pitch already exists.
|
|
526
|
+
Essential for layering ghosts around hand-placed anchors.
|
|
527
|
+
grid: Grid resolution. Defaults to the pattern's default grid.
|
|
528
|
+
duration: Note duration in beats (default 0.1).
|
|
529
|
+
seed: Fix the ghost layer for this call (an int); omit to use the
|
|
530
|
+
pattern's RNG.
|
|
531
|
+
|
|
532
|
+
**Tip — freeze the layer each cycle:** ``seed=`` starts a fresh
|
|
533
|
+
random stream on every rebuild, so the same steps — and the same
|
|
534
|
+
``(low, high)`` velocity draws — are chosen on every cycle: the
|
|
535
|
+
ghost layer is locked in place. The default ``self.rng``
|
|
536
|
+
advances state across rebuilds, so placement differs every cycle.
|
|
537
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
538
|
+
|
|
539
|
+
Example:
|
|
540
|
+
```python
|
|
541
|
+
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
|
|
542
|
+
p.hit_steps("snare_1", [4, 12], velocity=95)
|
|
543
|
+
|
|
544
|
+
# Different ghost placement each cycle (default)
|
|
545
|
+
p.ghost_fill("kick_1", density=0.2, velocity=(30, 45),
|
|
546
|
+
bias="sixteenths", no_overlap=True)
|
|
547
|
+
|
|
548
|
+
# The same ghost layer every cycle — placement frozen
|
|
549
|
+
p.ghost_fill("snare_1", density=0.15, velocity=(25, 40),
|
|
550
|
+
bias="before", seed=42)
|
|
551
|
+
```
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
rng = self._rng_from(seed, rng)
|
|
555
|
+
|
|
556
|
+
if grid is None:
|
|
557
|
+
grid = self._default_grid
|
|
558
|
+
|
|
559
|
+
if grid <= 0:
|
|
560
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
561
|
+
|
|
562
|
+
if isinstance(bias, list):
|
|
563
|
+
weights = list(bias)
|
|
564
|
+
if len(weights) < grid:
|
|
565
|
+
weights.extend([weights[-1] if weights else 0.0] * (grid - len(weights)))
|
|
566
|
+
elif len(weights) > grid:
|
|
567
|
+
weights = weights[:grid]
|
|
568
|
+
else:
|
|
569
|
+
weights = self.build_ghost_bias(grid, bias)
|
|
570
|
+
|
|
571
|
+
max_weight = max(weights) if weights else 1.0
|
|
572
|
+
|
|
573
|
+
if max_weight <= 0:
|
|
574
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
575
|
+
|
|
576
|
+
step_duration = self._pattern.length / grid
|
|
577
|
+
|
|
578
|
+
for i in range(grid):
|
|
579
|
+
prob = density * weights[i] / max_weight
|
|
580
|
+
|
|
581
|
+
if rng.random() >= prob:
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
if no_overlap and self._has_pitch_at_beat(pitch, i * step_duration):
|
|
585
|
+
continue
|
|
586
|
+
|
|
587
|
+
if callable(velocity):
|
|
588
|
+
vel = int(velocity(i))
|
|
589
|
+
elif isinstance(velocity, tuple):
|
|
590
|
+
if len(velocity) != 2:
|
|
591
|
+
raise ValueError(f"ghost_fill velocity tuple must be (low, high); for a per-step sequence use a list, got {velocity!r}")
|
|
592
|
+
vel = rng.randint(int(velocity[0]), int(velocity[1]))
|
|
593
|
+
elif isinstance(velocity, list):
|
|
594
|
+
vel = int(velocity[i % len(velocity)])
|
|
595
|
+
else:
|
|
596
|
+
vel = int(typing.cast(typing.Union[int, float], velocity))
|
|
597
|
+
|
|
598
|
+
self.note(pitch=pitch, beat=i * step_duration, velocity=vel, duration=duration)
|
|
599
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
600
|
+
|
|
601
|
+
def cellular_1d (
|
|
602
|
+
self,
|
|
603
|
+
pitch: typing.Union[int, str],
|
|
604
|
+
rule: int = 30,
|
|
605
|
+
generation: typing.Optional[int] = None,
|
|
606
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CA_VELOCITY,
|
|
607
|
+
duration: float = 0.1,
|
|
608
|
+
no_overlap: bool = False,
|
|
609
|
+
probability: float = 1.0,
|
|
610
|
+
seed: typing.Optional[int] = None,
|
|
611
|
+
rng: typing.Optional[random.Random] = None, ) -> "subsequence.pattern_builder.PatternBuilder":
|
|
612
|
+
|
|
613
|
+
"""Generate an evolving rhythm using a 1D cellular automaton.
|
|
614
|
+
|
|
615
|
+
Uses an elementary CA (1D binary cellular automaton) to produce
|
|
616
|
+
rhythmic patterns that change organically each bar. The CA state
|
|
617
|
+
evolves by one generation per cycle, creating patterns that are
|
|
618
|
+
deterministic yet surprising - structured chaos.
|
|
619
|
+
|
|
620
|
+
Rule 30 is the default: it produces quasi-random patterns with hidden
|
|
621
|
+
self-similarity. Rule 90 produces fractal patterns. Rule 110 is
|
|
622
|
+
Turing-complete.
|
|
623
|
+
|
|
624
|
+
Parameters:
|
|
625
|
+
pitch: MIDI note number or drum name.
|
|
626
|
+
rule: Wolfram rule number (0–255). Default 30.
|
|
627
|
+
generation: CA generation to render. Defaults to ``self.cycle``
|
|
628
|
+
so the pattern evolves each bar automatically.
|
|
629
|
+
velocity: MIDI velocity, or a ``(low, high)`` tuple for a
|
|
630
|
+
fresh random draw per hit.
|
|
631
|
+
duration: Note duration in beats.
|
|
632
|
+
no_overlap: If True, skip where same pitch already exists.
|
|
633
|
+
probability: Chance (0.0–1.0) that each hit plays — 1.0 places them all, lower thins.
|
|
634
|
+
seed: Fix the thinning for this call (an int); omit to use the pattern's RNG.
|
|
635
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
636
|
+
|
|
637
|
+
Example:
|
|
638
|
+
```python
|
|
639
|
+
p.hit_steps("kick_1", [0, 8], velocity=100)
|
|
640
|
+
p.cellular_1d("kick_1", rule=30, velocity=40, no_overlap=True)
|
|
641
|
+
```
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
if generation is None:
|
|
645
|
+
generation = self.cycle
|
|
646
|
+
rng = self._rng_from(seed, rng)
|
|
647
|
+
|
|
648
|
+
steps = self._default_grid
|
|
649
|
+
sequence = subsequence.sequence_utils.generate_cellular_automaton_1d(
|
|
650
|
+
steps=steps, rule=rule, generation=generation
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
self._place_rhythm_sequence(
|
|
654
|
+
sequence, pitch, velocity, duration, probability, rng, no_overlap=no_overlap
|
|
655
|
+
)
|
|
656
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
657
|
+
|
|
658
|
+
def cellular_2d (
|
|
659
|
+
self,
|
|
660
|
+
pitches: typing.List[typing.Union[int, str]],
|
|
661
|
+
rule: str = "B368/S245",
|
|
662
|
+
generation: typing.Optional[int] = None,
|
|
663
|
+
velocity: typing.Union[int, typing.Tuple[int, int], typing.List[int]] = subsequence.constants.velocity.DEFAULT_CA_VELOCITY,
|
|
664
|
+
duration: float = 0.1,
|
|
665
|
+
no_overlap: bool = False,
|
|
666
|
+
probability: float = 1.0,
|
|
667
|
+
initial_state: typing.Union[str, typing.List[typing.List[int]]] = "center",
|
|
668
|
+
density: float = 0.5,
|
|
669
|
+
seed: typing.Optional[int] = None,
|
|
670
|
+
rng: typing.Optional[random.Random] = None, ) -> "subsequence.pattern_builder.PatternBuilder":
|
|
671
|
+
|
|
672
|
+
"""Generate polyphonic patterns using a 2D Life-like cellular automaton.
|
|
673
|
+
|
|
674
|
+
Evolves a 2D grid where rows map to pitches or instruments and columns
|
|
675
|
+
map to time steps. Live cells in the final generation become note
|
|
676
|
+
onsets, producing patterns with spatial structure that evolves each bar.
|
|
677
|
+
|
|
678
|
+
The default rule B368/S245 (Morley/"Move") produces chaotic, active
|
|
679
|
+
patterns. B3/S23 is Conway's Life; B36/S23 is HighLife.
|
|
680
|
+
|
|
681
|
+
Parameters:
|
|
682
|
+
pitches: MIDI note numbers or drum names, one per row. Row 0
|
|
683
|
+
maps to the first pitch.
|
|
684
|
+
rule: Birth/Survival notation, e.g. ``"B3/S23"`` for Conway's
|
|
685
|
+
Life, ``"B368/S245"`` for Morley.
|
|
686
|
+
generation: CA generation to render. Defaults to ``self.cycle``
|
|
687
|
+
so the grid evolves each bar automatically.
|
|
688
|
+
velocity: Single MIDI velocity for all rows, or a list with one
|
|
689
|
+
value per row.
|
|
690
|
+
duration: Note duration in beats.
|
|
691
|
+
no_overlap: If True, skip notes where same pitch already exists.
|
|
692
|
+
probability: Chance (0.0–1.0) that each live cell plays — 1.0 places them all, lower thins.
|
|
693
|
+
initial_state: The generation-0 grid. ``"center"`` (default) lights a
|
|
694
|
+
single cell at the centre; ``"random"`` fills cells with probability
|
|
695
|
+
*density* (seed it with *seed* for a reproducible fill); or pass an
|
|
696
|
+
explicit ``list[list[int]]`` (rows × cols) for a custom start.
|
|
697
|
+
density: Fill probability for ``initial_state="random"`` (0.0–1.0).
|
|
698
|
+
seed: RNG seed (an int) for the ``"random"`` initial grid, so it
|
|
699
|
+
reproduces. Ignored for ``"center"`` or an explicit grid (a
|
|
700
|
+
warning is emitted if passed there).
|
|
701
|
+
rng: Random generator for the probability thinning. Defaults to
|
|
702
|
+
``self.rng``.
|
|
703
|
+
|
|
704
|
+
Example:
|
|
705
|
+
```python
|
|
706
|
+
pitches = [36, 38, 42, 46] # kick, snare, hihat, open hihat
|
|
707
|
+
p.cellular_2d(pitches, rule="B3/S23", initial_state="random", seed=7, density=0.3)
|
|
708
|
+
```
|
|
709
|
+
"""
|
|
710
|
+
|
|
711
|
+
if not pitches:
|
|
712
|
+
raise ValueError("pitches list cannot be empty")
|
|
713
|
+
|
|
714
|
+
if isinstance(velocity, list) and not velocity:
|
|
715
|
+
raise ValueError("velocity list cannot be empty")
|
|
716
|
+
|
|
717
|
+
if generation is None:
|
|
718
|
+
generation = self.cycle
|
|
719
|
+
|
|
720
|
+
if rng is None:
|
|
721
|
+
rng = self.rng
|
|
722
|
+
|
|
723
|
+
# Translate (initial_state, seed) into the underlying generator's seed arg,
|
|
724
|
+
# which stays int-or-grid: 1 = single centre cell, any other int = an
|
|
725
|
+
# RNG-seeded fill at *density*, a grid = an explicit starting state.
|
|
726
|
+
grid_seed: typing.Union[int, typing.List[typing.List[int]]]
|
|
727
|
+
if isinstance(initial_state, list):
|
|
728
|
+
grid_seed = initial_state
|
|
729
|
+
elif initial_state == "center":
|
|
730
|
+
grid_seed = 1
|
|
731
|
+
elif initial_state == "random":
|
|
732
|
+
if isinstance(seed, int):
|
|
733
|
+
# The generator reserves 1 as its centre-cell sentinel, so remap
|
|
734
|
+
# seed=1 to a fixed surrogate - every seed stays deterministic.
|
|
735
|
+
grid_seed = seed if seed != 1 else -1
|
|
736
|
+
else:
|
|
737
|
+
grid_seed = rng.randint(2, 2_147_483_646)
|
|
738
|
+
else:
|
|
739
|
+
raise ValueError(f"cellular_2d(): initial_state must be \"center\", \"random\", or a grid — got {initial_state!r}")
|
|
740
|
+
|
|
741
|
+
if seed is not None and initial_state != "random":
|
|
742
|
+
warnings.warn(
|
|
743
|
+
f"cellular_2d(): seed= only affects initial_state=\"random\" and is ignored for "
|
|
744
|
+
f"initial_state={initial_state!r} — pass initial_state=\"random\" to seed the grid",
|
|
745
|
+
UserWarning,
|
|
746
|
+
stacklevel = 2,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
cols = self._default_grid
|
|
750
|
+
rows = len(pitches)
|
|
751
|
+
|
|
752
|
+
grid = subsequence.sequence_utils.generate_cellular_automaton_2d(
|
|
753
|
+
rows=rows,
|
|
754
|
+
cols=cols,
|
|
755
|
+
rule=rule,
|
|
756
|
+
generation=generation,
|
|
757
|
+
seed=grid_seed,
|
|
758
|
+
density=density,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
for row_idx, pitch in enumerate(pitches):
|
|
762
|
+
row_velocity: typing.Union[int, typing.Tuple[int, int]]
|
|
763
|
+
|
|
764
|
+
if isinstance(velocity, list):
|
|
765
|
+
row_velocity = int(velocity[row_idx % len(velocity)])
|
|
766
|
+
elif isinstance(velocity, tuple):
|
|
767
|
+
# (low, high) range - resolved per placed note downstream.
|
|
768
|
+
row_velocity = velocity
|
|
769
|
+
else:
|
|
770
|
+
row_velocity = int(velocity)
|
|
771
|
+
|
|
772
|
+
self._place_rhythm_sequence(
|
|
773
|
+
grid[row_idx], pitch, row_velocity, duration, probability, rng, no_overlap=no_overlap
|
|
774
|
+
)
|
|
775
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
776
|
+
|
|
777
|
+
def markov (
|
|
778
|
+
self,
|
|
779
|
+
transitions: typing.Dict[str, typing.List[typing.Tuple[str, int]]],
|
|
780
|
+
pitch_map: typing.Dict[str, int],
|
|
781
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY,
|
|
782
|
+
duration: float = 0.1,
|
|
783
|
+
spacing: float = 0.25,
|
|
784
|
+
start: typing.Optional[str] = None,
|
|
785
|
+
seed: typing.Optional[int] = None,
|
|
786
|
+
rng: typing.Optional[random.Random] = None,
|
|
787
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
788
|
+
|
|
789
|
+
"""Generate a sequence by walking a first-order Markov chain.
|
|
790
|
+
|
|
791
|
+
Builds a :class:`~subsequence.weighted_graph.WeightedGraph` from
|
|
792
|
+
``transitions`` and walks it, placing one note per ``spacing`` beats.
|
|
793
|
+
The probability of each next state depends only on the current one -
|
|
794
|
+
use this to generate basslines, melodies, or rhythm motifs that have
|
|
795
|
+
stylistic coherence without being perfectly repetitive.
|
|
796
|
+
|
|
797
|
+
The transition dict uses the same ``(target, weight)`` pair format
|
|
798
|
+
as :meth:`Composition.form`, so the idiom is already familiar.
|
|
799
|
+
|
|
800
|
+
Parameters:
|
|
801
|
+
transitions: Mapping of state name to a list of
|
|
802
|
+
``(next_state, weight)`` tuples. Higher weight means higher
|
|
803
|
+
probability of that transition.
|
|
804
|
+
pitch_map: Mapping of state name to absolute MIDI note number.
|
|
805
|
+
States absent from this dict are walked but produce no note.
|
|
806
|
+
velocity: MIDI velocity for all placed notes (default 100),
|
|
807
|
+
or a ``(low, high)`` tuple for a fresh random draw per step.
|
|
808
|
+
duration: Note duration in beats (default 0.1).
|
|
809
|
+
spacing: Time between note onsets in beats (default 0.25 = 16th note).
|
|
810
|
+
start: Name of the starting state. Defaults to the first key
|
|
811
|
+
in ``transitions`` when not provided.
|
|
812
|
+
seed: Fix the walk for this call (an int); omit to use the pattern's RNG.
|
|
813
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
814
|
+
|
|
815
|
+
Raises:
|
|
816
|
+
ValueError: If ``transitions`` or ``pitch_map`` is empty.
|
|
817
|
+
|
|
818
|
+
Example:
|
|
819
|
+
```python
|
|
820
|
+
# Walking bassline: root anchors, 3rd and 5th passing tones
|
|
821
|
+
p.markov(
|
|
822
|
+
transitions={
|
|
823
|
+
"root": [("3rd", 3), ("5th", 2), ("root", 1)],
|
|
824
|
+
"3rd": [("5th", 3), ("root", 2)],
|
|
825
|
+
"5th": [("root", 3), ("3rd", 1)],
|
|
826
|
+
},
|
|
827
|
+
pitch_map={"root": 52, "3rd": 56, "5th": 59},
|
|
828
|
+
velocity=80,
|
|
829
|
+
spacing=0.5,
|
|
830
|
+
)
|
|
831
|
+
```
|
|
832
|
+
"""
|
|
833
|
+
|
|
834
|
+
rng = self._rng_from(seed, rng)
|
|
835
|
+
|
|
836
|
+
if not transitions:
|
|
837
|
+
raise ValueError("transitions dict cannot be empty")
|
|
838
|
+
|
|
839
|
+
if not pitch_map:
|
|
840
|
+
raise ValueError("pitch_map dict cannot be empty")
|
|
841
|
+
|
|
842
|
+
graph: subsequence.weighted_graph.WeightedGraph = subsequence.weighted_graph.WeightedGraph()
|
|
843
|
+
|
|
844
|
+
for source, targets in transitions.items():
|
|
845
|
+
for target, weight in targets:
|
|
846
|
+
graph.add_transition(source, target, weight)
|
|
847
|
+
|
|
848
|
+
if start is None:
|
|
849
|
+
start = next(iter(transitions))
|
|
850
|
+
|
|
851
|
+
if spacing <= 0:
|
|
852
|
+
raise ValueError(f"markov() spacing is the time between notes in beats — it must be positive, got {spacing}")
|
|
853
|
+
|
|
854
|
+
n_steps = int(self._pattern.length / spacing)
|
|
855
|
+
|
|
856
|
+
state = start
|
|
857
|
+
beat = 0.0
|
|
858
|
+
|
|
859
|
+
for _ in range(n_steps):
|
|
860
|
+
|
|
861
|
+
if state in pitch_map:
|
|
862
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
863
|
+
self.note(pitch=pitch_map[state], beat=beat, velocity=vel, duration=duration)
|
|
864
|
+
|
|
865
|
+
state = graph.choose_next(state, rng)
|
|
866
|
+
beat += spacing
|
|
867
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
868
|
+
|
|
869
|
+
def melody (
|
|
870
|
+
self,
|
|
871
|
+
state: subsequence.melodic_state.MelodicState,
|
|
872
|
+
spacing: float = 0.25,
|
|
873
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY,
|
|
874
|
+
duration: float = 0.2,
|
|
875
|
+
chord_tones: typing.Optional[typing.List[int]] = None,
|
|
876
|
+
seed: typing.Optional[int] = None,
|
|
877
|
+
rng: typing.Optional[random.Random] = None,
|
|
878
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
879
|
+
|
|
880
|
+
"""Generate a melodic line by querying a persistent :class:`~subsequence.melodic_state.MelodicState`.
|
|
881
|
+
|
|
882
|
+
Places one note (or rest) per ``spacing`` beats for the full pattern
|
|
883
|
+
length. Pitch selection is guided by the NIR cognitive model inside
|
|
884
|
+
``state``: after a large leap the model expects a direction reversal;
|
|
885
|
+
after a small step it expects continuation. Chord tones, range
|
|
886
|
+
gravity, and a pitch-diversity penalty further shape the output.
|
|
887
|
+
|
|
888
|
+
Because ``state`` lives outside the pattern builder and persists
|
|
889
|
+
across bar rebuilds, melodic continuity is maintained automatically -
|
|
890
|
+
no manual history management is required.
|
|
891
|
+
|
|
892
|
+
Parameters:
|
|
893
|
+
state: Persistent :class:`~subsequence.melodic_state.MelodicState`
|
|
894
|
+
instance created once at module level.
|
|
895
|
+
spacing: Time between note onsets in beats (default 0.25 = 16th note).
|
|
896
|
+
velocity: MIDI velocity. An ``int`` applies a fixed level; a
|
|
897
|
+
``(low, high)`` tuple draws uniformly from that range each spacing.
|
|
898
|
+
duration: Note duration in beats (default 0.2 - slightly shorter
|
|
899
|
+
than a 16th note, giving a crisp attack).
|
|
900
|
+
chord_tones: Optional list of MIDI note numbers that are chord
|
|
901
|
+
tones this bar (e.g. from ``chord.tones(root)``). Chord-tone
|
|
902
|
+
pitch classes receive a ``chord_weight`` bonus inside ``state``.
|
|
903
|
+
seed: Fix the walk for this call (an int); omit to use the pattern's RNG.
|
|
904
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
905
|
+
|
|
906
|
+
Example:
|
|
907
|
+
```python
|
|
908
|
+
melody_state = subsequence.MelodicState(
|
|
909
|
+
key="A", mode="aeolian",
|
|
910
|
+
low=60, high=84,
|
|
911
|
+
nir_strength=0.6,
|
|
912
|
+
chord_weight=0.4,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
@composition.pattern(channel=4, beats=4)
|
|
916
|
+
def lead (p, chord):
|
|
917
|
+
tones = chord.tones(72) if chord else None
|
|
918
|
+
p.melody(melody_state, spacing=0.5, velocity=(70, 100), chord_tones=tones)
|
|
919
|
+
```
|
|
920
|
+
"""
|
|
921
|
+
|
|
922
|
+
rng = self._rng_from(seed, rng)
|
|
923
|
+
|
|
924
|
+
# A state built without key/mode adopts the composition's here
|
|
925
|
+
# (idempotent — explicit constructor arguments always win).
|
|
926
|
+
state.configure_defaults(self.key, self.scale)
|
|
927
|
+
|
|
928
|
+
if spacing <= 0:
|
|
929
|
+
raise ValueError(f"melody() spacing is the time between notes in beats — it must be positive, got {spacing}")
|
|
930
|
+
|
|
931
|
+
n_steps = int(self._pattern.length / spacing)
|
|
932
|
+
beat = 0.0
|
|
933
|
+
|
|
934
|
+
for _ in range(n_steps):
|
|
935
|
+
|
|
936
|
+
pitch = state.choose_next(chord_tones, rng, beat=beat)
|
|
937
|
+
|
|
938
|
+
if pitch is not None:
|
|
939
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
940
|
+
self.note(pitch=pitch, beat=beat, velocity=vel, duration=duration)
|
|
941
|
+
|
|
942
|
+
beat += spacing
|
|
943
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
944
|
+
|
|
945
|
+
def lsystem (
|
|
946
|
+
self,
|
|
947
|
+
pitch_map: typing.Dict[str, typing.Union[int, str]],
|
|
948
|
+
axiom: str,
|
|
949
|
+
rules: typing.Dict[str, typing.Union[str, typing.List[typing.Tuple[str, float]]]],
|
|
950
|
+
generations: int = 3,
|
|
951
|
+
spacing: typing.Optional[float] = None,
|
|
952
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
953
|
+
duration: float = 0.2,
|
|
954
|
+
seed: typing.Optional[int] = None,
|
|
955
|
+
rng: typing.Optional[random.Random] = None,
|
|
956
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
957
|
+
|
|
958
|
+
"""Generate a note sequence using L-system string rewriting.
|
|
959
|
+
|
|
960
|
+
Expands ``axiom`` by applying ``rules`` for ``generations``
|
|
961
|
+
iterations, then walks the resulting string placing a note for
|
|
962
|
+
each character found in ``pitch_map``. Unmapped characters are
|
|
963
|
+
silent rests - they advance time but produce no note.
|
|
964
|
+
|
|
965
|
+
The defining musical property is self-similarity: patterns repeat
|
|
966
|
+
at different time scales. The Fibonacci rule (``A → AB``,
|
|
967
|
+
``B → A``) places hits at golden-ratio spacings. Koch and dragon
|
|
968
|
+
curve rules produce fractal melodic contours.
|
|
969
|
+
|
|
970
|
+
With ``spacing=None`` (default) the entire expanded string is fitted
|
|
971
|
+
into the bar: each generation makes notes twice as dense while
|
|
972
|
+
preserving the overall shape. With a fixed ``spacing`` the string is
|
|
973
|
+
truncated to fit and the density stays constant.
|
|
974
|
+
|
|
975
|
+
Parameters:
|
|
976
|
+
pitch_map: Maps single characters to MIDI notes or drum names.
|
|
977
|
+
Characters absent from the map produce rests.
|
|
978
|
+
axiom: Starting string (e.g. ``"A"``).
|
|
979
|
+
rules: Production rules. Deterministic: ``{"A": "AB"}``.
|
|
980
|
+
Stochastic: ``{"A": [("AB", 3), ("BA", 1)]}``.
|
|
981
|
+
generations: Rewriting iterations. String length grows
|
|
982
|
+
exponentially - keep this to 3–8 for practical use.
|
|
983
|
+
spacing: Time between symbols in beats. ``None`` (default)
|
|
984
|
+
auto-fits the full expanded string into the bar. A float
|
|
985
|
+
uses fixed spacing and truncates excess symbols.
|
|
986
|
+
velocity: MIDI velocity. An ``(low, high)`` tuple randomises
|
|
987
|
+
per note.
|
|
988
|
+
duration: Note duration in beats.
|
|
989
|
+
seed: Fix the stochastic-rule choices and velocity draws for this
|
|
990
|
+
call (an int); omit to use the pattern's RNG.
|
|
991
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
992
|
+
|
|
993
|
+
Example:
|
|
994
|
+
```python
|
|
995
|
+
# Fibonacci kick rhythm - self-similar hit spacing
|
|
996
|
+
p.lsystem(
|
|
997
|
+
pitch_map={"A": "kick_1"},
|
|
998
|
+
axiom="A",
|
|
999
|
+
rules={"A": "AB", "B": "A"},
|
|
1000
|
+
generations=6,
|
|
1001
|
+
velocity=80,
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
# Fractal melody over scale notes
|
|
1005
|
+
p.lsystem(
|
|
1006
|
+
pitch_map={"F": 60, "G": 62, "+": 64, "-": 67},
|
|
1007
|
+
axiom="F",
|
|
1008
|
+
rules={"F": "F+G", "G": "-F"},
|
|
1009
|
+
generations=4,
|
|
1010
|
+
spacing=0.25,
|
|
1011
|
+
velocity=(70, 100),
|
|
1012
|
+
)
|
|
1013
|
+
```
|
|
1014
|
+
"""
|
|
1015
|
+
|
|
1016
|
+
rng = self._rng_from(seed, rng)
|
|
1017
|
+
|
|
1018
|
+
expanded = subsequence.sequence_utils.lsystem_expand(
|
|
1019
|
+
axiom=axiom,
|
|
1020
|
+
rules=rules,
|
|
1021
|
+
generations=generations,
|
|
1022
|
+
rng=rng,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
if not expanded:
|
|
1026
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1027
|
+
|
|
1028
|
+
if spacing is None:
|
|
1029
|
+
auto_step = self._pattern.length / len(expanded)
|
|
1030
|
+
symbols = expanded
|
|
1031
|
+
else:
|
|
1032
|
+
if spacing <= 0:
|
|
1033
|
+
raise ValueError(f"lsystem() spacing is the time between symbols in beats — it must be positive, got {spacing}")
|
|
1034
|
+
|
|
1035
|
+
auto_step = spacing
|
|
1036
|
+
n_steps = int(self._pattern.length / spacing)
|
|
1037
|
+
symbols = expanded[:n_steps]
|
|
1038
|
+
|
|
1039
|
+
beat = 0.0
|
|
1040
|
+
|
|
1041
|
+
for symbol in symbols:
|
|
1042
|
+
if symbol in pitch_map:
|
|
1043
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
1044
|
+
self.note(
|
|
1045
|
+
pitch=pitch_map[symbol],
|
|
1046
|
+
beat=beat,
|
|
1047
|
+
velocity=vel,
|
|
1048
|
+
duration=duration,
|
|
1049
|
+
)
|
|
1050
|
+
beat += auto_step
|
|
1051
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1052
|
+
|
|
1053
|
+
def thue_morse (
|
|
1054
|
+
self,
|
|
1055
|
+
pitch: typing.Union[int, str],
|
|
1056
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_VELOCITY,
|
|
1057
|
+
duration: float = 0.1,
|
|
1058
|
+
pitch_b: typing.Optional[typing.Union[int, str]] = None,
|
|
1059
|
+
velocity_b: typing.Optional[typing.Union[int, typing.Tuple[int, int]]] = None,
|
|
1060
|
+
no_overlap: bool = False,
|
|
1061
|
+
probability: float = 1.0,
|
|
1062
|
+
seed: typing.Optional[int] = None,
|
|
1063
|
+
rng: typing.Optional[random.Random] = None, ) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1064
|
+
|
|
1065
|
+
"""Place notes using the Thue-Morse aperiodic binary sequence.
|
|
1066
|
+
|
|
1067
|
+
The Thue-Morse sequence (0 1 1 0 1 0 0 1 1 0 0 1 0 1 1 0 …) is
|
|
1068
|
+
perfectly balanced, overlap-free, and self-similar but never periodic.
|
|
1069
|
+
It produces rhythmic patterns that feel structured yet never settle into
|
|
1070
|
+
a simple repeating loop - a quality distinct from Euclidean rhythms
|
|
1071
|
+
(evenly spaced) and cellular automata (rule-driven evolution).
|
|
1072
|
+
|
|
1073
|
+
In **single-pitch mode** (default), notes are placed at positions where
|
|
1074
|
+
the sequence is 1. In **two-pitch mode** (``pitch_b`` given), ``pitch``
|
|
1075
|
+
flips to the 0-positions and ``pitch_b`` takes the 1-positions - useful
|
|
1076
|
+
for alternating two drums or two chord tones.
|
|
1077
|
+
|
|
1078
|
+
Parameters:
|
|
1079
|
+
pitch: Pitch (MIDI note number or drum name). Placed at the
|
|
1080
|
+
sequence's 1-positions in single-pitch mode; at the
|
|
1081
|
+
0-positions when ``pitch_b`` takes over the 1s.
|
|
1082
|
+
velocity: MIDI velocity for ``pitch``, or a ``(low, high)``
|
|
1083
|
+
tuple for a fresh random draw per hit.
|
|
1084
|
+
duration: Note duration in beats.
|
|
1085
|
+
pitch_b: Optional second pitch placed at sequence-1 positions.
|
|
1086
|
+
When set, all steps produce a note (no rests).
|
|
1087
|
+
velocity_b: Velocity for ``pitch_b`` (int or ``(low, high)``
|
|
1088
|
+
tuple). Defaults to ``velocity``.
|
|
1089
|
+
no_overlap: Skip steps where ``pitch`` is already sounding.
|
|
1090
|
+
probability: Chance (0.0–1.0) that each active step plays — 1.0 places them all, lower thins.
|
|
1091
|
+
seed: Fix the thinning for this call (an int); omit to use the pattern's RNG.
|
|
1092
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1093
|
+
|
|
1094
|
+
Example:
|
|
1095
|
+
```python
|
|
1096
|
+
# Single-pitch Thue-Morse kick
|
|
1097
|
+
p.thue_morse("kick_1", velocity=100)
|
|
1098
|
+
|
|
1099
|
+
# Two-pitch mode: alternate kick and snare
|
|
1100
|
+
p.thue_morse("kick_1", pitch_b="snare_1", velocity=100)
|
|
1101
|
+
```
|
|
1102
|
+
"""
|
|
1103
|
+
rng = self._rng_from(seed, rng)
|
|
1104
|
+
|
|
1105
|
+
sequence = subsequence.sequence_utils.thue_morse(self._default_grid)
|
|
1106
|
+
|
|
1107
|
+
if not sequence:
|
|
1108
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1109
|
+
|
|
1110
|
+
if pitch_b is None:
|
|
1111
|
+
self._place_rhythm_sequence(sequence, pitch, velocity, duration, probability, rng, no_overlap)
|
|
1112
|
+
else:
|
|
1113
|
+
if velocity_b is None:
|
|
1114
|
+
velocity_b = velocity
|
|
1115
|
+
|
|
1116
|
+
# Every step sounds one of the two voices (no rests), and no_overlap
|
|
1117
|
+
# is honoured against whichever voice the step would place.
|
|
1118
|
+
second_pitch = pitch_b
|
|
1119
|
+
second_velocity = velocity_b
|
|
1120
|
+
|
|
1121
|
+
def _event (i: int, val: typing.Any) -> typing.Optional[typing.Tuple[typing.Union[int, str], typing.Union[int, typing.Tuple[int, int]], float]]:
|
|
1122
|
+
if val == 0:
|
|
1123
|
+
return (pitch, velocity, duration)
|
|
1124
|
+
return (second_pitch, second_velocity, duration)
|
|
1125
|
+
|
|
1126
|
+
self._place_gated_sequence(sequence, _event, probability, rng, no_overlap=no_overlap)
|
|
1127
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1128
|
+
|
|
1129
|
+
def de_bruijn (
|
|
1130
|
+
self,
|
|
1131
|
+
pitches: typing.List[typing.Union[int, str]],
|
|
1132
|
+
window: int = 2,
|
|
1133
|
+
spacing: typing.Optional[float] = None,
|
|
1134
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
1135
|
+
duration: float = 0.2,
|
|
1136
|
+
seed: typing.Optional[int] = None,
|
|
1137
|
+
rng: typing.Optional[random.Random] = None,
|
|
1138
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1139
|
+
|
|
1140
|
+
"""Generate a melody that exhaustively traverses all pitch subsequences.
|
|
1141
|
+
|
|
1142
|
+
A de Bruijn sequence B(k, n) over an alphabet of size ``k`` with window
|
|
1143
|
+
``n`` contains every possible subsequence of length ``n`` exactly once
|
|
1144
|
+
(cyclically). Mapping each symbol to a pitch produces a melody that
|
|
1145
|
+
systematically explores all possible ``n``-gram transitions - every
|
|
1146
|
+
permutation of ``window`` consecutive pitches appears exactly once.
|
|
1147
|
+
|
|
1148
|
+
With ``spacing=None`` (default) the full sequence is auto-fitted into the
|
|
1149
|
+
bar, matching the behaviour of :meth:`lsystem`. With a fixed ``spacing``
|
|
1150
|
+
the sequence is truncated to fill the available beats.
|
|
1151
|
+
|
|
1152
|
+
Parameters:
|
|
1153
|
+
pitches: List of MIDI note numbers or note strings. The alphabet
|
|
1154
|
+
size ``k`` is ``len(pitches)``.
|
|
1155
|
+
window: Subsequence length ``n``. The output has ``len(pitches) ** window``
|
|
1156
|
+
notes. Keep small (2–4) for practical bar lengths.
|
|
1157
|
+
spacing: Time between notes in beats. ``None`` auto-fits the sequence
|
|
1158
|
+
into the bar; a float uses fixed spacing and truncates.
|
|
1159
|
+
velocity: MIDI velocity. An ``(low, high)`` tuple randomises per note.
|
|
1160
|
+
duration: Note duration in beats.
|
|
1161
|
+
seed: Fix the velocity draws for this call (an int) — the note order
|
|
1162
|
+
itself is deterministic; omit to use the pattern's RNG.
|
|
1163
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1164
|
+
|
|
1165
|
+
Example:
|
|
1166
|
+
```python
|
|
1167
|
+
# All 2-note combinations of a pentatonic scale
|
|
1168
|
+
p.de_bruijn([60, 62, 64, 67, 69], window=2, velocity=(60, 100))
|
|
1169
|
+
```
|
|
1170
|
+
"""
|
|
1171
|
+
|
|
1172
|
+
rng = self._rng_from(seed, rng)
|
|
1173
|
+
|
|
1174
|
+
if not pitches:
|
|
1175
|
+
raise ValueError("pitches list cannot be empty")
|
|
1176
|
+
|
|
1177
|
+
k = len(pitches)
|
|
1178
|
+
sequence = subsequence.sequence_utils.de_bruijn(k, window)
|
|
1179
|
+
|
|
1180
|
+
if not sequence:
|
|
1181
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1182
|
+
|
|
1183
|
+
if spacing is None:
|
|
1184
|
+
auto_step = self._pattern.length / len(sequence)
|
|
1185
|
+
symbols = sequence
|
|
1186
|
+
else:
|
|
1187
|
+
if spacing <= 0:
|
|
1188
|
+
raise ValueError(f"de_bruijn() spacing is the time between notes in beats — it must be positive, got {spacing}")
|
|
1189
|
+
|
|
1190
|
+
auto_step = spacing
|
|
1191
|
+
n_steps = int(self._pattern.length / spacing)
|
|
1192
|
+
symbols = sequence[:n_steps]
|
|
1193
|
+
|
|
1194
|
+
beat = 0.0
|
|
1195
|
+
|
|
1196
|
+
for idx in symbols:
|
|
1197
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
1198
|
+
self.note(pitch=pitches[idx], beat=beat, velocity=vel, duration=duration)
|
|
1199
|
+
beat += auto_step
|
|
1200
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1201
|
+
|
|
1202
|
+
def fibonacci (
|
|
1203
|
+
self,
|
|
1204
|
+
pitch: typing.Union[int, str],
|
|
1205
|
+
count: int,
|
|
1206
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
1207
|
+
duration: float = 0.2,
|
|
1208
|
+
seed: typing.Optional[int] = None,
|
|
1209
|
+
rng: typing.Optional[random.Random] = None, ) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1210
|
+
|
|
1211
|
+
"""Place notes at golden-ratio-spaced beat positions (Fibonacci spiral timing).
|
|
1212
|
+
|
|
1213
|
+
Uses the golden angle method - ``position_i = frac(i × φ) × bar_length``,
|
|
1214
|
+
where ``frac`` keeps only the fractional part - to distribute ``count``
|
|
1215
|
+
events across the bar as a low-discrepancy (sunflower-seed) spread.
|
|
1216
|
+
The result is sorted into ascending time order. Unlike a Euclidean rhythm (maximally even
|
|
1217
|
+
spacing on a fixed grid), Fibonacci timing is irrational and places
|
|
1218
|
+
events off-grid in a way that sounds organic and avoids metronomic
|
|
1219
|
+
repetition.
|
|
1220
|
+
|
|
1221
|
+
Parameters:
|
|
1222
|
+
pitch: MIDI note number or drum name.
|
|
1223
|
+
count: Number of notes to place.
|
|
1224
|
+
velocity: MIDI velocity. An ``(low, high)`` tuple randomises per note.
|
|
1225
|
+
duration: Note duration in beats.
|
|
1226
|
+
seed: Fix the velocity draws for this call (an int) — the timing
|
|
1227
|
+
itself is deterministic; omit to use the pattern's RNG.
|
|
1228
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1229
|
+
|
|
1230
|
+
Example:
|
|
1231
|
+
```python
|
|
1232
|
+
# 11 hi-hat hits with golden-ratio spacing
|
|
1233
|
+
p.fibonacci("hi_hat_closed", count=11, velocity=(60, 90))
|
|
1234
|
+
```
|
|
1235
|
+
"""
|
|
1236
|
+
|
|
1237
|
+
rng = self._rng_from(seed, rng)
|
|
1238
|
+
|
|
1239
|
+
beats = subsequence.sequence_utils.fibonacci_rhythm(count, self._pattern.length)
|
|
1240
|
+
|
|
1241
|
+
for beat in beats:
|
|
1242
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
1243
|
+
self.note(pitch=pitch, beat=beat, velocity=vel, duration=duration)
|
|
1244
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1245
|
+
|
|
1246
|
+
def lorenz (
|
|
1247
|
+
self,
|
|
1248
|
+
pitches: typing.List[typing.Union[int, str]],
|
|
1249
|
+
spacing: float = 0.25,
|
|
1250
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
1251
|
+
duration: float = 0.2,
|
|
1252
|
+
dt: float = 0.01,
|
|
1253
|
+
sigma: float = 10.0,
|
|
1254
|
+
rho: float = 28.0,
|
|
1255
|
+
beta: float = 8.0 / 3.0,
|
|
1256
|
+
x0: float = 0.1,
|
|
1257
|
+
y0: float = 0.0,
|
|
1258
|
+
z0: float = 0.0,
|
|
1259
|
+
mapping: typing.Optional[
|
|
1260
|
+
typing.Callable[
|
|
1261
|
+
[float, float, float],
|
|
1262
|
+
typing.Optional[typing.Tuple[typing.Union[int, str], int, float]],
|
|
1263
|
+
]
|
|
1264
|
+
] = None,
|
|
1265
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1266
|
+
|
|
1267
|
+
"""Generate a note sequence driven by the Lorenz strange attractor.
|
|
1268
|
+
|
|
1269
|
+
Integrates the Lorenz system to produce a trajectory of (x, y, z) points,
|
|
1270
|
+
each normalised to [0, 1]. The three axes provide correlated but
|
|
1271
|
+
independent modulation sources: by default x drives pitch selection,
|
|
1272
|
+
y drives velocity, and z drives duration.
|
|
1273
|
+
|
|
1274
|
+
The Lorenz attractor is deterministic but extremely sensitive to initial
|
|
1275
|
+
conditions: changing ``x0`` by even 0.001 produces a divergent trajectory
|
|
1276
|
+
over time. This makes it ideal for cycle-by-cycle variation - pass
|
|
1277
|
+
``x0=p.cycle * 0.001`` to generate a unique but slowly evolving phrase
|
|
1278
|
+
each bar.
|
|
1279
|
+
|
|
1280
|
+
A custom ``mapping`` callable can override the default x/y/z → pitch/vel/dur
|
|
1281
|
+
assignment, or return ``None`` for a rest.
|
|
1282
|
+
|
|
1283
|
+
Parameters:
|
|
1284
|
+
pitches: Pitch pool. The x-axis selects an index: ``int(x * len(pitches)) % len(pitches)``.
|
|
1285
|
+
spacing: Time between notes in beats. Default 0.25 (16th note).
|
|
1286
|
+
velocity: Fixed velocity or ``(low, high)`` tuple. Overridden by ``mapping``.
|
|
1287
|
+
duration: Maximum note duration. z is scaled to ``[0.05, duration]``.
|
|
1288
|
+
Overridden by ``mapping``.
|
|
1289
|
+
dt: Integration time step. Default 0.01.
|
|
1290
|
+
sigma, rho, beta: Lorenz parameters. Defaults produce the classic
|
|
1291
|
+
butterfly attractor (chaotic regime).
|
|
1292
|
+
x0, y0, z0: Initial conditions. Use ``x0=p.cycle * small_delta``
|
|
1293
|
+
for slowly evolving variation.
|
|
1294
|
+
mapping: Optional callable ``(x, y, z) -> (pitch, velocity, duration)``
|
|
1295
|
+
or ``None`` for rest.
|
|
1296
|
+
|
|
1297
|
+
Example:
|
|
1298
|
+
```python
|
|
1299
|
+
scale = [60, 62, 64, 65, 67, 69, 71, 72]
|
|
1300
|
+
p.lorenz(scale, spacing=0.25, velocity=(50, 110), x0=p.cycle * 0.002)
|
|
1301
|
+
```
|
|
1302
|
+
"""
|
|
1303
|
+
|
|
1304
|
+
if not pitches:
|
|
1305
|
+
raise ValueError("pitches list cannot be empty")
|
|
1306
|
+
|
|
1307
|
+
if spacing <= 0:
|
|
1308
|
+
raise ValueError(f"lorenz() spacing is the time between notes in beats — it must be positive, got {spacing}")
|
|
1309
|
+
|
|
1310
|
+
n_steps = int(self._pattern.length / spacing)
|
|
1311
|
+
points = subsequence.sequence_utils.lorenz_attractor(
|
|
1312
|
+
n_steps, dt=dt, sigma=sigma, rho=rho, beta=beta, x0=x0, y0=y0, z0=z0
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
beat = 0.0
|
|
1316
|
+
|
|
1317
|
+
for x, y, z in points:
|
|
1318
|
+
|
|
1319
|
+
if mapping is not None:
|
|
1320
|
+
result = mapping(x, y, z)
|
|
1321
|
+
if result is not None:
|
|
1322
|
+
p_pitch, p_vel, p_dur = result
|
|
1323
|
+
self.note(pitch=p_pitch, beat=beat, velocity=p_vel, duration=p_dur)
|
|
1324
|
+
else:
|
|
1325
|
+
pitch_idx = int(x * len(pitches)) % len(pitches)
|
|
1326
|
+
p_pitch = pitches[pitch_idx]
|
|
1327
|
+
if isinstance(velocity, tuple):
|
|
1328
|
+
p_vel = int(velocity[0] + y * (velocity[1] - velocity[0]))
|
|
1329
|
+
else:
|
|
1330
|
+
# A fixed int means FIXED - the y axis only drives velocity
|
|
1331
|
+
# when a (low, high) range invites it (or via mapping=).
|
|
1332
|
+
p_vel = int(velocity)
|
|
1333
|
+
p_dur = 0.05 + z * max(0.0, duration - 0.05)
|
|
1334
|
+
self.note(pitch=p_pitch, beat=beat, velocity=p_vel, duration=p_dur)
|
|
1335
|
+
|
|
1336
|
+
beat += spacing
|
|
1337
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1338
|
+
|
|
1339
|
+
def reaction_diffusion (
|
|
1340
|
+
self,
|
|
1341
|
+
pitch: typing.Union[int, str],
|
|
1342
|
+
threshold: float = 0.5,
|
|
1343
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
1344
|
+
duration: float = 0.1,
|
|
1345
|
+
feed_rate: float = 0.055,
|
|
1346
|
+
kill_rate: float = 0.062,
|
|
1347
|
+
steps: int = 1000,
|
|
1348
|
+
no_overlap: bool = False,
|
|
1349
|
+
probability: float = 1.0,
|
|
1350
|
+
seed: typing.Optional[int] = None,
|
|
1351
|
+
rng: typing.Optional[random.Random] = None, ) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1352
|
+
|
|
1353
|
+
"""Generate a rhythm from a 1D Gray-Scott reaction-diffusion simulation.
|
|
1354
|
+
|
|
1355
|
+
Simulates the Gray-Scott model on a ring of ``_default_grid`` cells,
|
|
1356
|
+
then thresholds the final V-concentration to produce a binary hit
|
|
1357
|
+
pattern. Cells where concentration exceeds ``threshold`` become note
|
|
1358
|
+
events.
|
|
1359
|
+
|
|
1360
|
+
Unlike cellular automata - where rules are discrete and the state is
|
|
1361
|
+
binary - reaction-diffusion evolves a continuous concentration field
|
|
1362
|
+
governed by diffusion rates and chemical reactions. The resulting
|
|
1363
|
+
spatial patterns (spots, stripes, travelling waves) have an organic,
|
|
1364
|
+
biological character that maps naturally to rhythm.
|
|
1365
|
+
|
|
1366
|
+
The ``feed_rate`` and ``kill_rate`` parameters control pattern type:
|
|
1367
|
+
typical values that produce spots (useful rhythms) range from 0.020–0.062
|
|
1368
|
+
for feed and 0.045–0.069 for kill. The defaults (F=0.055, k=0.062)
|
|
1369
|
+
produce a stable spotted pattern.
|
|
1370
|
+
|
|
1371
|
+
Parameters:
|
|
1372
|
+
pitch: MIDI note number or drum name.
|
|
1373
|
+
threshold: V-concentration threshold for note placement (0.0–1.0).
|
|
1374
|
+
Lower values produce denser patterns.
|
|
1375
|
+
velocity: MIDI velocity. An ``(low, high)`` tuple is NOT random:
|
|
1376
|
+
each step's local V-concentration is mapped into the range
|
|
1377
|
+
deterministically, so notes are louder where the pattern is
|
|
1378
|
+
denser.
|
|
1379
|
+
duration: Note duration in beats.
|
|
1380
|
+
feed_rate: Rate of U replenishment. Default 0.055.
|
|
1381
|
+
kill_rate: Rate of V removal. Default 0.062.
|
|
1382
|
+
steps: Number of simulation iterations. More = more developed
|
|
1383
|
+
pattern. Default 1000.
|
|
1384
|
+
no_overlap: Skip steps where ``pitch`` is already sounding.
|
|
1385
|
+
probability: Chance (0.0–1.0) that each active step plays — 1.0 places them all, lower thins.
|
|
1386
|
+
seed: Fix the thinning for this call (an int); omit to use the pattern's RNG.
|
|
1387
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1388
|
+
|
|
1389
|
+
Example:
|
|
1390
|
+
```python
|
|
1391
|
+
# Organic kick pattern from reaction-diffusion
|
|
1392
|
+
p.reaction_diffusion("kick_1", threshold=0.4, feed_rate=0.037, kill_rate=0.060)
|
|
1393
|
+
```
|
|
1394
|
+
"""
|
|
1395
|
+
rng = self._rng_from(seed, rng)
|
|
1396
|
+
|
|
1397
|
+
concentrations = subsequence.sequence_utils.reaction_diffusion_1d(
|
|
1398
|
+
width=self._default_grid,
|
|
1399
|
+
steps=steps,
|
|
1400
|
+
feed_rate=feed_rate,
|
|
1401
|
+
kill_rate=kill_rate,
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
sequence = [1 if c > threshold else 0 for c in concentrations]
|
|
1405
|
+
|
|
1406
|
+
if isinstance(velocity, tuple):
|
|
1407
|
+
# Map concentration to velocity range for active steps: louder
|
|
1408
|
+
# where the pattern is denser (deterministic, not random).
|
|
1409
|
+
midi_vel_lo, midi_vel_hi = velocity
|
|
1410
|
+
|
|
1411
|
+
def _event (i: int, hit: typing.Any) -> typing.Optional[typing.Tuple[typing.Union[int, str], typing.Union[int, typing.Tuple[int, int]], float]]:
|
|
1412
|
+
if hit == 0:
|
|
1413
|
+
return None
|
|
1414
|
+
|
|
1415
|
+
vel = int(midi_vel_lo + concentrations[i] * (midi_vel_hi - midi_vel_lo))
|
|
1416
|
+
return (pitch, vel, duration)
|
|
1417
|
+
|
|
1418
|
+
self._place_gated_sequence(sequence, _event, probability, rng, no_overlap=no_overlap)
|
|
1419
|
+
else:
|
|
1420
|
+
self._place_rhythm_sequence(sequence, pitch, int(velocity), duration, probability, rng, no_overlap)
|
|
1421
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1422
|
+
|
|
1423
|
+
def self_avoiding_walk (
|
|
1424
|
+
self,
|
|
1425
|
+
pitches: typing.List[typing.Union[int, str]],
|
|
1426
|
+
spacing: float = 0.25,
|
|
1427
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
1428
|
+
duration: float = 0.2,
|
|
1429
|
+
seed: typing.Optional[int] = None,
|
|
1430
|
+
rng: typing.Optional[random.Random] = None,
|
|
1431
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1432
|
+
|
|
1433
|
+
"""Generate a melody using a self-avoiding random walk.
|
|
1434
|
+
|
|
1435
|
+
A self-avoiding walk moves ±1 step through a pitch index space, tracking
|
|
1436
|
+
visited positions and refusing to revisit them. When the walk is trapped
|
|
1437
|
+
(all neighbours visited), the visited set resets and the walk continues
|
|
1438
|
+
from the current position - creating natural phrase boundaries.
|
|
1439
|
+
|
|
1440
|
+
Compared to :func:`random_walk`, the self-avoiding variant guarantees
|
|
1441
|
+
pitch diversity within each phrase: no pitch repeats until the walk
|
|
1442
|
+
resets. The contiguous step motion (never skipping pitches) gives
|
|
1443
|
+
melodies a smooth, step-wise quality with occasional direction reversals.
|
|
1444
|
+
|
|
1445
|
+
Parameters:
|
|
1446
|
+
pitches: Ordered list of MIDI note numbers or note strings. The walk
|
|
1447
|
+
moves through indices ``[0, len(pitches) - 1]``, mapping each to
|
|
1448
|
+
the corresponding pitch.
|
|
1449
|
+
spacing: Time between notes in beats. Default 0.25 (16th note).
|
|
1450
|
+
velocity: MIDI velocity. An ``(low, high)`` tuple randomises per note.
|
|
1451
|
+
duration: Note duration in beats.
|
|
1452
|
+
seed: Fix the walk for this call (an int); omit to use the pattern's RNG.
|
|
1453
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1454
|
+
|
|
1455
|
+
Example:
|
|
1456
|
+
```python
|
|
1457
|
+
scale = subsequence.scale_notes("C", "ionian", low=60, high=72)
|
|
1458
|
+
p.self_avoiding_walk(scale, spacing=0.25, velocity=(60, 100))
|
|
1459
|
+
```
|
|
1460
|
+
"""
|
|
1461
|
+
|
|
1462
|
+
rng = self._rng_from(seed, rng)
|
|
1463
|
+
|
|
1464
|
+
if not pitches:
|
|
1465
|
+
raise ValueError("pitches list cannot be empty")
|
|
1466
|
+
|
|
1467
|
+
if spacing <= 0:
|
|
1468
|
+
raise ValueError(f"self_avoiding_walk() spacing is the time between notes in beats — it must be positive, got {spacing}")
|
|
1469
|
+
|
|
1470
|
+
n_steps = int(self._pattern.length / spacing)
|
|
1471
|
+
indices = subsequence.sequence_utils.self_avoiding_walk(
|
|
1472
|
+
n=n_steps,
|
|
1473
|
+
low=0,
|
|
1474
|
+
high=len(pitches) - 1,
|
|
1475
|
+
rng=rng,
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1478
|
+
beat = 0.0
|
|
1479
|
+
|
|
1480
|
+
for idx in indices:
|
|
1481
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
1482
|
+
self.note(pitch=pitches[idx], beat=beat, velocity=vel, duration=duration)
|
|
1483
|
+
beat += spacing
|
|
1484
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1485
|
+
|
|
1486
|
+
def thin (
|
|
1487
|
+
self,
|
|
1488
|
+
pitch: typing.Optional[typing.Union[int, str]] = None,
|
|
1489
|
+
strategy: typing.Union[str, typing.List[float]] = "strength",
|
|
1490
|
+
amount: float = 0.5,
|
|
1491
|
+
grid: typing.Optional[int] = None,
|
|
1492
|
+
seed: typing.Optional[int] = None,
|
|
1493
|
+
rng: typing.Optional[random.Random] = None,
|
|
1494
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1495
|
+
|
|
1496
|
+
"""
|
|
1497
|
+
Remove notes from the pattern based on their rhythmic position.
|
|
1498
|
+
|
|
1499
|
+
This is the musical inverse of :meth:`ghost_fill()`. Where ``ghost_fill``
|
|
1500
|
+
uses bias weights to decide where to *add* ghost notes, ``thin`` uses the
|
|
1501
|
+
same position vocabulary to decide where to *remove* notes. A high
|
|
1502
|
+
strategy weight on a position means that position is dropped first.
|
|
1503
|
+
|
|
1504
|
+
The strategy names match those in :meth:`build_ghost_bias()` and carry the
|
|
1505
|
+
same rhythmic meaning:
|
|
1506
|
+
|
|
1507
|
+
- ``"sixteenths"`` - removes 16th-note subdivisions (e/a), keeps beats and &.
|
|
1508
|
+
- ``"offbeat"`` - removes the & position, straightens the groove.
|
|
1509
|
+
- ``"e_and_a"`` - removes all non-downbeat positions, keeps only beats.
|
|
1510
|
+
- ``"downbeat"`` - removes beat positions (floating/displaced effect).
|
|
1511
|
+
- ``"upbeat"`` - removes only the & position.
|
|
1512
|
+
- ``"uniform"`` - removes from all positions equally (per-instrument dropout).
|
|
1513
|
+
- ``"strength"`` - progressive thinning: weakest positions (e/a) drop first,
|
|
1514
|
+
strongest (downbeat) last. Useful for Perlin-driven density control.
|
|
1515
|
+
|
|
1516
|
+
When ``pitch`` is given, only notes of that instrument are affected -
|
|
1517
|
+
useful for drum layers. When ``pitch`` is ``None`` (the default), all
|
|
1518
|
+
notes regardless of pitch are candidates. This makes ``thin`` a
|
|
1519
|
+
rhythm-aware generalisation of :meth:`dropout()`, and is ideal for
|
|
1520
|
+
tonal patterns such as arpeggios where each step carries a different pitch.
|
|
1521
|
+
|
|
1522
|
+
Position classification is **zone-based**: each grid step owns the pulse range
|
|
1523
|
+
``[N * step_pulses, (N + 1) * step_pulses)``, so notes shifted by swing or
|
|
1524
|
+
groove are still classified correctly regardless of call order.
|
|
1525
|
+
|
|
1526
|
+
Parameters:
|
|
1527
|
+
pitch: Drum name or MIDI note number to target, or ``None`` to thin
|
|
1528
|
+
all notes regardless of pitch. Defaults to ``None``.
|
|
1529
|
+
strategy: Named strategy string or a list of per-step drop-priority
|
|
1530
|
+
floats (0.0 = never drop, 1.0 = highest drop priority). Must have
|
|
1531
|
+
length equal to ``grid`` when a list is provided.
|
|
1532
|
+
amount: Overall thinning depth (0.0 = remove nothing, 1.0 = remove all
|
|
1533
|
+
qualifying). Effective drop probability = ``priority * amount``.
|
|
1534
|
+
Drive this with a Perlin field or section progress for smooth,
|
|
1535
|
+
organic thinning over time.
|
|
1536
|
+
grid: Step grid size. Defaults to the pattern's ``default_grid``.
|
|
1537
|
+
seed: Fix the thinning for this call (an int); omit to use the pattern's RNG.
|
|
1538
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1539
|
+
|
|
1540
|
+
Example::
|
|
1541
|
+
|
|
1542
|
+
# Thin 16th ghost notes from the kick, keep anchors and off-beats
|
|
1543
|
+
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
|
|
1544
|
+
p.ghost_fill("kick_1", density=0.3, velocity=(25, 40), bias="sixteenths")
|
|
1545
|
+
p.thin("kick_1", "sixteenths", amount=0.8)
|
|
1546
|
+
|
|
1547
|
+
# Perlin-driven progressive thinning of hi-hats
|
|
1548
|
+
sparseness = perlin_1d(p.cycle * 0.07, seed=42)
|
|
1549
|
+
p.thin("hi_hat_closed", "strength", amount=sparseness)
|
|
1550
|
+
|
|
1551
|
+
# Thin an arpeggio (all pitches) - no pitch loop needed
|
|
1552
|
+
p.thin(strategy="strength", amount=sparseness)
|
|
1553
|
+
"""
|
|
1554
|
+
|
|
1555
|
+
rng = self._rng_from(seed, rng)
|
|
1556
|
+
|
|
1557
|
+
if grid is None:
|
|
1558
|
+
grid = self._default_grid
|
|
1559
|
+
|
|
1560
|
+
if pitch is None:
|
|
1561
|
+
midi_pitch = None
|
|
1562
|
+
else:
|
|
1563
|
+
midi_pitch = self._resolve_pitch_lenient(pitch)
|
|
1564
|
+
if midi_pitch is None:
|
|
1565
|
+
# Named voice this device lacks (already warned once): there are
|
|
1566
|
+
# no such notes to thin, so this is a no-op rather than an error.
|
|
1567
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1568
|
+
|
|
1569
|
+
# Build the per-step drop-priority weights.
|
|
1570
|
+
#
|
|
1571
|
+
# Strategy names are shared with ghost_fill's bias vocabulary. The per-step
|
|
1572
|
+
# weights from build_ghost_bias() are reused directly, with semantics inverted:
|
|
1573
|
+
# ghost_fill: high weight → place a note here
|
|
1574
|
+
# thin: high weight → drop a note from here
|
|
1575
|
+
#
|
|
1576
|
+
# "strength" is defined only for thin() - it expresses a thinning hierarchy
|
|
1577
|
+
# (weakest positions drop first) which has no meaningful ghost_fill equivalent.
|
|
1578
|
+
if strategy == "strength":
|
|
1579
|
+
# Per-beat drop priorities: e/a (1.0) > & (0.6) > downbeat (0.05).
|
|
1580
|
+
# As `amount` rises, progressively weaker positions are removed first.
|
|
1581
|
+
steps_per_beat = max(1, grid // 4)
|
|
1582
|
+
priorities: typing.List[float] = []
|
|
1583
|
+
for i in range(grid):
|
|
1584
|
+
pos = i % steps_per_beat
|
|
1585
|
+
if pos == 0:
|
|
1586
|
+
priorities.append(0.05)
|
|
1587
|
+
elif steps_per_beat > 1 and pos == steps_per_beat // 2:
|
|
1588
|
+
priorities.append(0.6)
|
|
1589
|
+
else:
|
|
1590
|
+
priorities.append(1.0)
|
|
1591
|
+
elif isinstance(strategy, list):
|
|
1592
|
+
if len(strategy) != grid:
|
|
1593
|
+
raise ValueError(
|
|
1594
|
+
f"thin() custom strategy list has {len(strategy)} values "
|
|
1595
|
+
f"but grid has {grid} steps."
|
|
1596
|
+
)
|
|
1597
|
+
priorities = list(strategy)
|
|
1598
|
+
else:
|
|
1599
|
+
# Reuse build_ghost_bias() weights for all shared strategy names.
|
|
1600
|
+
# The positions that ghost_fill prefers to add to are the same
|
|
1601
|
+
# positions that thin() will prefer to remove from.
|
|
1602
|
+
priorities = self.build_ghost_bias(grid, strategy)
|
|
1603
|
+
|
|
1604
|
+
# Zone-based pulse classification.
|
|
1605
|
+
# Zone N owns pulses in [ N * step_pulses, (N+1) * step_pulses ).
|
|
1606
|
+
# Notes shifted by swing or groove remain in their original zone.
|
|
1607
|
+
total_pulses = self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE
|
|
1608
|
+
step_pulses = total_pulses / grid
|
|
1609
|
+
|
|
1610
|
+
pulses_to_remove: typing.List[int] = []
|
|
1611
|
+
|
|
1612
|
+
for pulse, step in list(self._pattern.steps.items()):
|
|
1613
|
+
|
|
1614
|
+
zone = int(pulse / step_pulses)
|
|
1615
|
+
if zone >= grid:
|
|
1616
|
+
zone = grid - 1
|
|
1617
|
+
|
|
1618
|
+
priority = priorities[zone]
|
|
1619
|
+
if priority <= 0.0:
|
|
1620
|
+
continue
|
|
1621
|
+
|
|
1622
|
+
# Separate target notes from protected notes at this pulse.
|
|
1623
|
+
if midi_pitch is None:
|
|
1624
|
+
remaining = []
|
|
1625
|
+
targets = list(step.notes)
|
|
1626
|
+
else:
|
|
1627
|
+
remaining = [n for n in step.notes if n.pitch != midi_pitch]
|
|
1628
|
+
targets = [n for n in step.notes if n.pitch == midi_pitch]
|
|
1629
|
+
|
|
1630
|
+
for note in targets:
|
|
1631
|
+
if rng.random() >= priority * amount:
|
|
1632
|
+
remaining.append(note)
|
|
1633
|
+
# else: note is dropped
|
|
1634
|
+
|
|
1635
|
+
if not remaining:
|
|
1636
|
+
pulses_to_remove.append(pulse)
|
|
1637
|
+
else:
|
|
1638
|
+
step.notes = remaining
|
|
1639
|
+
|
|
1640
|
+
for pulse in pulses_to_remove:
|
|
1641
|
+
del self._pattern.steps[pulse]
|
|
1642
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1643
|
+
|
|
1644
|
+
def ratchet (
|
|
1645
|
+
self,
|
|
1646
|
+
subdivisions: int = 2,
|
|
1647
|
+
pitch: typing.Optional[typing.Union[int, str]] = None,
|
|
1648
|
+
probability: float = 1.0,
|
|
1649
|
+
velocity_start: float = 1.0,
|
|
1650
|
+
velocity_end: float = 1.0,
|
|
1651
|
+
shape: typing.Union[str, typing.Callable[[float], float]] = "linear",
|
|
1652
|
+
gate: float = 0.5,
|
|
1653
|
+
steps: typing.Optional[typing.List[int]] = None,
|
|
1654
|
+
grid: typing.Optional[int] = None,
|
|
1655
|
+
seed: typing.Optional[int] = None,
|
|
1656
|
+
rng: typing.Optional[random.Random] = None,
|
|
1657
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1658
|
+
|
|
1659
|
+
"""Subdivide existing notes into rapid repeated hits (rolls/ratchets).
|
|
1660
|
+
|
|
1661
|
+
A post-placement transform: takes notes already in the pattern and
|
|
1662
|
+
replaces each one with ``subdivisions`` evenly-spaced sub-hits within
|
|
1663
|
+
the original note's duration window. The velocity of each sub-hit is
|
|
1664
|
+
interpolated from ``velocity_start`` to ``velocity_end`` (as multipliers
|
|
1665
|
+
on the original velocity) using the ``shape`` easing curve, so crescendo
|
|
1666
|
+
rolls, decrescendo buzzes, and flat repeats are all one parameter apart.
|
|
1667
|
+
|
|
1668
|
+
Call ``ratchet()`` after note-placement methods (``euclidean``,
|
|
1669
|
+
``hit_steps``, ``arpeggio``, etc.) and before ``swing`` or ``groove``
|
|
1670
|
+
so that the subdivisions sit inside the original note slot and swing
|
|
1671
|
+
displacement still affects the parent position.
|
|
1672
|
+
|
|
1673
|
+
Parameters:
|
|
1674
|
+
subdivisions: Number of sub-hits replacing each note (default 2).
|
|
1675
|
+
If the note's duration is shorter than ``subdivisions`` pulses,
|
|
1676
|
+
subdivisions are clamped to ``note.duration`` so they never
|
|
1677
|
+
stack on the same pulse.
|
|
1678
|
+
pitch: Only ratchet notes matching this pitch (MIDI number or drum
|
|
1679
|
+
name). ``None`` (default) ratchets all notes regardless of
|
|
1680
|
+
pitch — useful for melodic patterns such as arpeggios.
|
|
1681
|
+
probability: Chance (0.0–1.0) that each note gets ratcheted. Notes
|
|
1682
|
+
that fail the check are left completely unchanged. Default 1.0
|
|
1683
|
+
(every note is ratcheted).
|
|
1684
|
+
velocity_start: Velocity multiplier for the first sub-hit (0.0–2.0).
|
|
1685
|
+
Default 1.0 (same as the original).
|
|
1686
|
+
velocity_end: Velocity multiplier for the last sub-hit (0.0–2.0).
|
|
1687
|
+
Default 1.0. Set ``velocity_start=0.3, velocity_end=1.0`` for
|
|
1688
|
+
a crescendo roll; ``1.0, 0.2`` for a decrescendo buzz.
|
|
1689
|
+
shape: Easing curve applied to the velocity interpolation across
|
|
1690
|
+
sub-hits. Accepts any name from ``subsequence.easing`` (e.g.
|
|
1691
|
+
``"ease_in"``, ``"ease_out"``, ``"s_curve"``) or a custom
|
|
1692
|
+
callable ``f(t) → t`` for t ∈ [0, 1]. Default ``"linear"``.
|
|
1693
|
+
gate: Sub-note duration as a fraction of each subdivision slot
|
|
1694
|
+
(0.0–1.0). ``1.0`` = legato (sub-hits touch), ``0.5`` =
|
|
1695
|
+
staccato (half the slot). Default 0.5.
|
|
1696
|
+
steps: Grid positions to ratchet (e.g. ``[0, 4, 12]``). Notes are
|
|
1697
|
+
classified to grid zones the same way ``thin()`` works — swing-
|
|
1698
|
+
shifted notes remain in their original zone. ``None`` (default)
|
|
1699
|
+
applies ratchet to all eligible notes.
|
|
1700
|
+
grid: Grid resolution used for ``steps`` zone classification.
|
|
1701
|
+
Defaults to the pattern's ``default_grid``.
|
|
1702
|
+
seed: Fix the probability gating for this call (an int); omit to
|
|
1703
|
+
use the pattern's RNG.
|
|
1704
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1705
|
+
|
|
1706
|
+
Examples:
|
|
1707
|
+
```python
|
|
1708
|
+
# Subdivide every hi-hat into a triplet roll
|
|
1709
|
+
p.euclidean("hi_hat_closed", 8).ratchet(3, pitch="hi_hat_closed")
|
|
1710
|
+
|
|
1711
|
+
# Crescendo roll into a snare
|
|
1712
|
+
p.hit_steps("snare", [12]).ratchet(4, velocity_start=0.3,
|
|
1713
|
+
velocity_end=1.0,
|
|
1714
|
+
shape="ease_in")
|
|
1715
|
+
|
|
1716
|
+
# Probabilistic 2× ratchet on hi-hats only
|
|
1717
|
+
p.euclidean("hi_hat_closed", 12).ratchet(2, pitch="hi_hat_closed",
|
|
1718
|
+
probability=0.4, gate=0.3)
|
|
1719
|
+
|
|
1720
|
+
# Ratchet only steps 0 and 8 (downbeats)
|
|
1721
|
+
p.euclidean("kick_1", 6).ratchet(2, pitch="kick_1", steps=[0, 8])
|
|
1722
|
+
```
|
|
1723
|
+
"""
|
|
1724
|
+
|
|
1725
|
+
rng = self._rng_from(seed, rng)
|
|
1726
|
+
|
|
1727
|
+
if grid is None:
|
|
1728
|
+
grid = self._default_grid
|
|
1729
|
+
|
|
1730
|
+
if pitch is None:
|
|
1731
|
+
midi_pitch = None
|
|
1732
|
+
else:
|
|
1733
|
+
midi_pitch = self._resolve_pitch_lenient(pitch)
|
|
1734
|
+
if midi_pitch is None:
|
|
1735
|
+
# Named voice this device lacks (already warned once): nothing
|
|
1736
|
+
# matches, so leave the pattern unchanged rather than raising.
|
|
1737
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1738
|
+
ease_fn = subsequence.easing.get_easing(shape)
|
|
1739
|
+
|
|
1740
|
+
# Build zone set for steps mask (zone-based, matching thin()'s approach).
|
|
1741
|
+
target_zones: typing.Optional[typing.Set[int]] = None
|
|
1742
|
+
if steps is not None:
|
|
1743
|
+
target_zones = set(steps)
|
|
1744
|
+
|
|
1745
|
+
total_pulses = self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE
|
|
1746
|
+
step_pulses = total_pulses / grid
|
|
1747
|
+
|
|
1748
|
+
new_steps: typing.Dict[int, subsequence.pattern.Step] = {}
|
|
1749
|
+
|
|
1750
|
+
for pulse, step in self._pattern.steps.items():
|
|
1751
|
+
|
|
1752
|
+
# Zone classification for steps mask.
|
|
1753
|
+
if target_zones is not None:
|
|
1754
|
+
zone = int(pulse / step_pulses)
|
|
1755
|
+
if zone >= grid:
|
|
1756
|
+
zone = grid - 1
|
|
1757
|
+
in_zone = zone in target_zones
|
|
1758
|
+
else:
|
|
1759
|
+
in_zone = True
|
|
1760
|
+
|
|
1761
|
+
# Separate targeted notes from passthrough notes.
|
|
1762
|
+
if midi_pitch is None:
|
|
1763
|
+
targets = list(step.notes) if in_zone else []
|
|
1764
|
+
passthrough = [] if in_zone else list(step.notes)
|
|
1765
|
+
else:
|
|
1766
|
+
if in_zone:
|
|
1767
|
+
targets = [n for n in step.notes if n.pitch == midi_pitch]
|
|
1768
|
+
passthrough = [n for n in step.notes if n.pitch != midi_pitch]
|
|
1769
|
+
else:
|
|
1770
|
+
targets = []
|
|
1771
|
+
passthrough = list(step.notes)
|
|
1772
|
+
|
|
1773
|
+
# Passthrough notes keep their original pulse position.
|
|
1774
|
+
if passthrough:
|
|
1775
|
+
if pulse not in new_steps:
|
|
1776
|
+
new_steps[pulse] = subsequence.pattern.Step()
|
|
1777
|
+
new_steps[pulse].notes.extend(passthrough)
|
|
1778
|
+
|
|
1779
|
+
for note in targets:
|
|
1780
|
+
|
|
1781
|
+
# Probability gate — failed notes are kept unchanged.
|
|
1782
|
+
if probability < 1.0 and rng.random() < (1.0 - probability):
|
|
1783
|
+
if pulse not in new_steps:
|
|
1784
|
+
new_steps[pulse] = subsequence.pattern.Step()
|
|
1785
|
+
new_steps[pulse].notes.append(note)
|
|
1786
|
+
continue
|
|
1787
|
+
|
|
1788
|
+
# Clamp subdivisions so sub-hits never stack on the same pulse.
|
|
1789
|
+
effective_subdivs = min(subdivisions, note.duration)
|
|
1790
|
+
if effective_subdivs < 1:
|
|
1791
|
+
effective_subdivs = 1
|
|
1792
|
+
|
|
1793
|
+
slot_pulses = note.duration / effective_subdivs
|
|
1794
|
+
|
|
1795
|
+
for i in range(effective_subdivs):
|
|
1796
|
+
sub_pulse = pulse + int(round(i * slot_pulses))
|
|
1797
|
+
|
|
1798
|
+
# Velocity interpolation via easing.
|
|
1799
|
+
if effective_subdivs == 1:
|
|
1800
|
+
t = 0.0
|
|
1801
|
+
else:
|
|
1802
|
+
t = i / (effective_subdivs - 1)
|
|
1803
|
+
eased_t = ease_fn(t)
|
|
1804
|
+
vel_mul = velocity_start + (velocity_end - velocity_start) * eased_t
|
|
1805
|
+
sub_velocity = max(1, min(127, int(round(note.velocity * vel_mul))))
|
|
1806
|
+
|
|
1807
|
+
sub_duration = max(1, int(round(slot_pulses * gate)))
|
|
1808
|
+
|
|
1809
|
+
sub_note = subsequence.pattern.Note(
|
|
1810
|
+
pitch=note.pitch,
|
|
1811
|
+
velocity=sub_velocity,
|
|
1812
|
+
duration=sub_duration,
|
|
1813
|
+
channel=note.channel,
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
if sub_pulse not in new_steps:
|
|
1817
|
+
new_steps[sub_pulse] = subsequence.pattern.Step()
|
|
1818
|
+
new_steps[sub_pulse].notes.append(sub_note)
|
|
1819
|
+
|
|
1820
|
+
self._pattern.steps = new_steps
|
|
1821
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1822
|
+
|
|
1823
|
+
def evolve (
|
|
1824
|
+
self,
|
|
1825
|
+
pitches: typing.List[typing.Union[int, str]],
|
|
1826
|
+
length: typing.Optional[int] = None,
|
|
1827
|
+
drift: float = 0.0,
|
|
1828
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
1829
|
+
duration: float = 0.2,
|
|
1830
|
+
spacing: float = 0.25,
|
|
1831
|
+
seed: typing.Optional[int] = None,
|
|
1832
|
+
rng: typing.Optional[random.Random] = None, ) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1833
|
+
|
|
1834
|
+
"""Loop a pitch sequence that gradually mutates each cycle.
|
|
1835
|
+
|
|
1836
|
+
On cycle 0, the sequence is locked to the initial ``pitches`` (truncated to
|
|
1837
|
+
``length`` if provided). Each subsequent cycle, every step has a ``drift``
|
|
1838
|
+
probability of being replaced by a randomly-chosen value from the pool.
|
|
1839
|
+
When ``drift=0.0`` the loop never changes; when ``drift=1.0`` every step
|
|
1840
|
+
is redrawn every cycle.
|
|
1841
|
+
|
|
1842
|
+
State is stored in ``p.data`` under a key derived from the pitch content, so the
|
|
1843
|
+
buffer persists across pattern rebuilds. The buffer is reset whenever
|
|
1844
|
+
``cycle == 0`` so restarts produce deterministic output.
|
|
1845
|
+
|
|
1846
|
+
Combine with ``p.snap_to_scale()`` to keep drifted pitches in key:
|
|
1847
|
+
|
|
1848
|
+
```python
|
|
1849
|
+
p.evolve([60, 64, 67, 72], length=8, drift=0.12)
|
|
1850
|
+
p.snap_to_scale("C", "minor")
|
|
1851
|
+
```
|
|
1852
|
+
|
|
1853
|
+
Parameters:
|
|
1854
|
+
pitches: Initial pitch pool. The initial buffer is built from the first
|
|
1855
|
+
``length`` values (cycling if shorter than ``length``). Mutation
|
|
1856
|
+
also draws replacements from this pool.
|
|
1857
|
+
length: Number of steps in the loop. Defaults to ``len(pitches)``.
|
|
1858
|
+
drift: Per-step mutation probability each cycle (0.0–1.0).
|
|
1859
|
+
``0.0`` = locked loop, ``1.0`` = fully random each cycle.
|
|
1860
|
+
velocity: MIDI velocity. An ``(low, high)`` tuple randomises per step.
|
|
1861
|
+
duration: Note duration in beats.
|
|
1862
|
+
spacing: Beat interval between steps.
|
|
1863
|
+
seed: Fix the drift mutations and velocity draws for this call (an
|
|
1864
|
+
int); omit to use the pattern's RNG.
|
|
1865
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1866
|
+
|
|
1867
|
+
Example:
|
|
1868
|
+
```python
|
|
1869
|
+
# 8-step loop that slowly diverges from its seed
|
|
1870
|
+
p.evolve([60, 62, 64, 65, 67, 69], length=8, drift=0.1,
|
|
1871
|
+
velocity=(70, 100), spacing=0.5)
|
|
1872
|
+
p.snap_to_scale("C", "dorian")
|
|
1873
|
+
```
|
|
1874
|
+
"""
|
|
1875
|
+
|
|
1876
|
+
rng = self._rng_from(seed, rng)
|
|
1877
|
+
|
|
1878
|
+
if not pitches:
|
|
1879
|
+
raise ValueError("pitches list cannot be empty")
|
|
1880
|
+
|
|
1881
|
+
resolved_opt = [self._resolve_pitch_lenient(p) if isinstance(p, str) else p for p in pitches]
|
|
1882
|
+
resolved = [r for r in resolved_opt if r is not None]
|
|
1883
|
+
if not resolved:
|
|
1884
|
+
# Every seed name was a voice this device lacks (each warned once).
|
|
1885
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1886
|
+
n = length if length is not None else len(resolved)
|
|
1887
|
+
|
|
1888
|
+
# Stable key derived from the seed *content* (not object identity). The
|
|
1889
|
+
# documented idiom passes a fresh list literal every cycle; keying on
|
|
1890
|
+
# ``id(pitches)`` gave that literal a new address each cycle, so the
|
|
1891
|
+
# buffer re-seeded every cycle (drift never accumulated) and a dict key
|
|
1892
|
+
# leaked per cycle. Content keying makes the buffer persist so the walk
|
|
1893
|
+
# actually evolves, and scoping by the owning pattern's builder name keeps
|
|
1894
|
+
# two patterns' identical seeds from sharing one walk. ``resolved`` (the
|
|
1895
|
+
# post-lenient ints) and ``n`` are folded in so a changed step count or a
|
|
1896
|
+
# seed whose unknown names were dropped still keys stably.
|
|
1897
|
+
builder_fn = getattr(self._pattern, '_builder_fn', None)
|
|
1898
|
+
scope = getattr(builder_fn, '__name__', '') if builder_fn is not None else ''
|
|
1899
|
+
data_key = f"_evolve_{scope}_{n}_{tuple(resolved)}"
|
|
1900
|
+
|
|
1901
|
+
# Initialise or reset the buffer on cycle 0.
|
|
1902
|
+
if data_key not in self.data or self.cycle == 0:
|
|
1903
|
+
self.data[data_key] = [resolved[i % len(resolved)] for i in range(n)]
|
|
1904
|
+
|
|
1905
|
+
buffer = self.data[data_key]
|
|
1906
|
+
|
|
1907
|
+
# Mutate the buffer in place for this cycle (skipped on cycle 0 — seed plays first).
|
|
1908
|
+
if self.cycle > 0 and drift > 0.0:
|
|
1909
|
+
for i in range(n):
|
|
1910
|
+
if rng.random() < drift:
|
|
1911
|
+
buffer[i] = rng.choice(resolved)
|
|
1912
|
+
|
|
1913
|
+
# Place notes.
|
|
1914
|
+
for i, pitch in enumerate(buffer):
|
|
1915
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
1916
|
+
self.note(pitch=pitch, beat=i * spacing, velocity=vel, duration=duration)
|
|
1917
|
+
|
|
1918
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1919
|
+
|
|
1920
|
+
def branch (
|
|
1921
|
+
self,
|
|
1922
|
+
pitches: typing.List[typing.Union[int, str]],
|
|
1923
|
+
depth: int = 2,
|
|
1924
|
+
path: int = 0,
|
|
1925
|
+
mutation: float = 0.0,
|
|
1926
|
+
velocity: typing.Union[int, typing.Tuple[int, int]] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY,
|
|
1927
|
+
duration: float = 0.2,
|
|
1928
|
+
spacing: float = 0.25,
|
|
1929
|
+
seed: typing.Optional[int] = None,
|
|
1930
|
+
rng: typing.Optional[random.Random] = None,
|
|
1931
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1932
|
+
|
|
1933
|
+
"""Generate a melodic variation by navigating a fractal tree of transforms.
|
|
1934
|
+
|
|
1935
|
+
The ``pitches`` sequence is the "trunk". At each branch level, two musical
|
|
1936
|
+
transforms are assigned deterministically (derived from the original
|
|
1937
|
+
sequence), and the ``path`` index selects left or right at each level.
|
|
1938
|
+
After ``depth`` levels the result is a variation that is always
|
|
1939
|
+
structurally related to the input ``pitches``.
|
|
1940
|
+
|
|
1941
|
+
Use ``path=p.cycle`` to step through all ``2 ** depth`` variations in
|
|
1942
|
+
order; the index wraps automatically.
|
|
1943
|
+
|
|
1944
|
+
**Transforms** (assigned deterministically per level):
|
|
1945
|
+
|
|
1946
|
+
- *Retrograde* — reverse the sequence.
|
|
1947
|
+
- *Invert* — mirror each pitch around the first note.
|
|
1948
|
+
- *Transpose* — shift all pitches by the interval between the first
|
|
1949
|
+
two notes.
|
|
1950
|
+
- *Rotate* — shift the starting position by one step.
|
|
1951
|
+
- *Scale intervals* — multiply intervals from the first note by 0.5
|
|
1952
|
+
(compress) or 2.0 (expand), rounded to the nearest semitone.
|
|
1953
|
+
|
|
1954
|
+
An optional ``mutation`` layer randomly substitutes individual notes
|
|
1955
|
+
with other input pitches on top of the deterministic branching.
|
|
1956
|
+
|
|
1957
|
+
Parameters:
|
|
1958
|
+
pitches: Original pitch sequence. All variations are derived from this.
|
|
1959
|
+
depth: Branching levels. ``2 ** depth`` unique variations are
|
|
1960
|
+
available before the path wraps.
|
|
1961
|
+
path: Which variation to play (0-based). ``path=p.cycle`` advances
|
|
1962
|
+
automatically. Values wrap modulo ``2 ** depth``.
|
|
1963
|
+
mutation: Probability that any step is replaced by a random input
|
|
1964
|
+
pitch after branching (0.0 = none, 1.0 = fully random).
|
|
1965
|
+
velocity: MIDI velocity. An ``(low, high)`` tuple randomises per step.
|
|
1966
|
+
duration: Note duration in beats.
|
|
1967
|
+
spacing: Beat interval between steps.
|
|
1968
|
+
seed: Fix the mutation substitutions and velocity draws for this
|
|
1969
|
+
call (an int) — the variation tree itself is deterministic;
|
|
1970
|
+
omit to use the pattern's RNG.
|
|
1971
|
+
rng: Advanced determinism form — a ``random.Random`` (wins over ``seed=``).
|
|
1972
|
+
|
|
1973
|
+
Example:
|
|
1974
|
+
```python
|
|
1975
|
+
# Cycle through 8 variations (depth=3) of a 4-note motif
|
|
1976
|
+
p.branch([60, 64, 67, 72], depth=3, path=p.cycle,
|
|
1977
|
+
velocity=85, spacing=0.5)
|
|
1978
|
+
p.snap_to_scale("C", "minor")
|
|
1979
|
+
```
|
|
1980
|
+
"""
|
|
1981
|
+
|
|
1982
|
+
rng = self._rng_from(seed, rng)
|
|
1983
|
+
|
|
1984
|
+
if not pitches:
|
|
1985
|
+
raise ValueError("pitches list cannot be empty")
|
|
1986
|
+
|
|
1987
|
+
resolved_opt = [self._resolve_pitch_lenient(p) if isinstance(p, str) else p for p in pitches]
|
|
1988
|
+
resolved = [r for r in resolved_opt if r is not None]
|
|
1989
|
+
if not resolved:
|
|
1990
|
+
# Every seed name was a voice this device lacks (each warned once).
|
|
1991
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1992
|
+
|
|
1993
|
+
# The variation tree itself lives in sequence_utils.branch_sequence —
|
|
1994
|
+
# a reusable pure kernel (feed its output to Motif.notes() for a
|
|
1995
|
+
# storable variation). This verb resolves drum names, derives the
|
|
1996
|
+
# variation, and places it on the grid.
|
|
1997
|
+
sequence = subsequence.sequence_utils.branch_sequence(
|
|
1998
|
+
resolved,
|
|
1999
|
+
depth = depth,
|
|
2000
|
+
path = path,
|
|
2001
|
+
mutation = mutation,
|
|
2002
|
+
rng = rng,
|
|
2003
|
+
)
|
|
2004
|
+
|
|
2005
|
+
# Place notes.
|
|
2006
|
+
for i, pitch in enumerate(sequence):
|
|
2007
|
+
vel = self._resolve_velocity(velocity, rng)
|
|
2008
|
+
self.note(pitch=pitch, beat=i * spacing, velocity=vel, duration=duration)
|
|
2009
|
+
|
|
2010
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|