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,1208 @@
|
|
|
1
|
+
"""Mixin class providing MIDI and OSC control-message methods for PatternBuilder.
|
|
2
|
+
|
|
3
|
+
This module is not intended to be used directly. ``PatternMidiMixin``
|
|
4
|
+
is inherited by ``PatternBuilder`` in ``pattern_builder.py``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
import pymididefs.cc
|
|
10
|
+
import pymididefs.rpn
|
|
11
|
+
|
|
12
|
+
import subsequence.constants
|
|
13
|
+
import subsequence.easing
|
|
14
|
+
import subsequence.pattern
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PatternMidiMixin:
|
|
18
|
+
|
|
19
|
+
"""MIDI control, OSC, and note-correlated pitch bend methods for PatternBuilder.
|
|
20
|
+
|
|
21
|
+
All methods here operate on ``self._pattern`` (a ``Pattern`` instance),
|
|
22
|
+
which is set by ``PatternBuilder.__init__``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# ── Instance attributes provided by PatternBuilder at runtime ────────
|
|
26
|
+
_pattern: subsequence.pattern.Pattern
|
|
27
|
+
_default_grid: int
|
|
28
|
+
_cc_name_map: typing.Optional[typing.Dict[str, int]]
|
|
29
|
+
_nrpn_name_map: typing.Optional[typing.Dict[str, int]]
|
|
30
|
+
|
|
31
|
+
if typing.TYPE_CHECKING:
|
|
32
|
+
import subsequence.pattern_builder # noqa: F401 — type-checking only
|
|
33
|
+
def _resolve_cc (self, control: typing.Union[int, str]) -> int: ...
|
|
34
|
+
def _resolve_nrpn (self, parameter: typing.Union[int, str]) -> int: ...
|
|
35
|
+
def _resolve_rpn (self, parameter: typing.Union[int, str]) -> int: ...
|
|
36
|
+
|
|
37
|
+
# ── Shared ramp helper ──────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
def _ramp_pulse_span (
|
|
40
|
+
self,
|
|
41
|
+
pulse_start: int,
|
|
42
|
+
pulse_end: int,
|
|
43
|
+
start: float,
|
|
44
|
+
end: float,
|
|
45
|
+
shape: typing.Union[str, subsequence.easing.EasingFn],
|
|
46
|
+
resolution: int,
|
|
47
|
+
event_fn: typing.Callable[[int, float], None],
|
|
48
|
+
) -> None:
|
|
49
|
+
|
|
50
|
+
"""Walk from pulse_start to pulse_end, calling event_fn(pulse, value) at each step.
|
|
51
|
+
|
|
52
|
+
The pulse-domain kernel shared by every ramp on this mixin — the
|
|
53
|
+
beat-based ramps via :meth:`_ramp_pulses` and the note-correlated pitch
|
|
54
|
+
bends via :meth:`_generate_bend_events`. ``event_fn`` receives the pulse
|
|
55
|
+
position and the linearly-interpolated (then eased) value, and is
|
|
56
|
+
responsible for creating and appending the event.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
span = pulse_end - pulse_start
|
|
60
|
+
|
|
61
|
+
if span <= 0:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if resolution < 1:
|
|
65
|
+
raise ValueError("resolution must be at least 1 pulse")
|
|
66
|
+
|
|
67
|
+
easing_fn = subsequence.easing.get_easing(shape)
|
|
68
|
+
pulse = pulse_start
|
|
69
|
+
|
|
70
|
+
while pulse <= pulse_end:
|
|
71
|
+
t = (pulse - pulse_start) / span
|
|
72
|
+
eased_t = easing_fn(t)
|
|
73
|
+
interpolated = start + (end - start) * eased_t
|
|
74
|
+
event_fn(pulse, interpolated)
|
|
75
|
+
pulse += resolution
|
|
76
|
+
|
|
77
|
+
# The loop lands on pulse_end only when resolution divides the span —
|
|
78
|
+
# otherwise emit the target explicitly, so a ramp always reaches the
|
|
79
|
+
# value it was asked to reach.
|
|
80
|
+
if span % resolution != 0:
|
|
81
|
+
event_fn(pulse_end, start + (end - start) * easing_fn(1.0))
|
|
82
|
+
|
|
83
|
+
def _ramp_pulses (
|
|
84
|
+
self,
|
|
85
|
+
beat_start: float,
|
|
86
|
+
beat_end: float,
|
|
87
|
+
start: float,
|
|
88
|
+
end: float,
|
|
89
|
+
shape: typing.Union[str, subsequence.easing.EasingFn],
|
|
90
|
+
resolution: int,
|
|
91
|
+
event_fn: typing.Callable[[int, float], None],
|
|
92
|
+
) -> None:
|
|
93
|
+
|
|
94
|
+
"""Walk from beat_start to beat_end, calling event_fn(pulse, value) at each step.
|
|
95
|
+
|
|
96
|
+
The beat-based entry to :meth:`_ramp_pulse_span`, shared by
|
|
97
|
+
``cc_ramp()``, ``pitch_bend_ramp()``, ``nrpn_ramp()``/``rpn_ramp()``,
|
|
98
|
+
and ``osc_ramp()``.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
pulse_start = int(beat_start * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
102
|
+
pulse_end = int(beat_end * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
103
|
+
|
|
104
|
+
self._ramp_pulse_span(pulse_start, pulse_end, start, end, shape, resolution, event_fn)
|
|
105
|
+
|
|
106
|
+
# ── CC messages ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def cc (self, control: typing.Union[int, str], value: int, beat: float = 0.0) -> "subsequence.pattern_builder.PatternBuilder":
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
Send a single CC message at a beat position.
|
|
112
|
+
|
|
113
|
+
Parameters:
|
|
114
|
+
control: MIDI CC number (0–127), or a string name resolved
|
|
115
|
+
via the pattern's ``cc_name_map``.
|
|
116
|
+
value: CC value (0–127); out-of-range values are clamped.
|
|
117
|
+
beat: Beat position within the pattern.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
cc_num: int = self._resolve_cc(control)
|
|
121
|
+
pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
122
|
+
|
|
123
|
+
# Clamp to the 7-bit CC range like every sibling (cc_ramp / program_change
|
|
124
|
+
# / pitch_bend) so a computed out-of-range value is corrected here rather
|
|
125
|
+
# than silently dropped and logged when mido rejects it at dispatch time.
|
|
126
|
+
clamped_value = max(0, min(127, int(round(value))))
|
|
127
|
+
|
|
128
|
+
self._pattern.cc_events.append(
|
|
129
|
+
subsequence.pattern.CcEvent(
|
|
130
|
+
pulse = pulse,
|
|
131
|
+
message_type = 'control_change',
|
|
132
|
+
control = cc_num,
|
|
133
|
+
value = clamped_value
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
137
|
+
|
|
138
|
+
def cc_ramp (
|
|
139
|
+
self,
|
|
140
|
+
control: typing.Union[int, str],
|
|
141
|
+
start: int,
|
|
142
|
+
end: int,
|
|
143
|
+
beat_start: float = 0.0,
|
|
144
|
+
beat_end: typing.Optional[float] = None,
|
|
145
|
+
resolution: int = 1,
|
|
146
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear"
|
|
147
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
Interpolate a CC value over a beat range.
|
|
151
|
+
|
|
152
|
+
Parameters:
|
|
153
|
+
control: MIDI CC number (0–127), or a string name resolved
|
|
154
|
+
via the pattern's ``cc_name_map``.
|
|
155
|
+
start: Starting CC value (0–127).
|
|
156
|
+
end: Ending CC value (0–127).
|
|
157
|
+
beat_start: Beat position to begin the ramp.
|
|
158
|
+
beat_end: Beat position to end the ramp. Defaults to pattern length.
|
|
159
|
+
resolution: Pulses between CC messages (1 = every pulse, ~20ms at 120 BPM).
|
|
160
|
+
Higher values (e.g. 2 or 4) reduce MIDI traffic density but may sound
|
|
161
|
+
stepped at slow tempos.
|
|
162
|
+
shape: Easing curve — a name string (e.g. ``"exponential"``) or any
|
|
163
|
+
callable that maps [0, 1] → [0, 1]. Defaults to ``"linear"``.
|
|
164
|
+
See :mod:`subsequence.easing` for available shapes.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
cc_num: int = self._resolve_cc(control)
|
|
168
|
+
|
|
169
|
+
if beat_end is None:
|
|
170
|
+
beat_end = self._pattern.length
|
|
171
|
+
|
|
172
|
+
def _event (pulse: int, val: float) -> None:
|
|
173
|
+
self._pattern.cc_events.append(
|
|
174
|
+
subsequence.pattern.CcEvent(
|
|
175
|
+
pulse = pulse,
|
|
176
|
+
message_type = 'control_change',
|
|
177
|
+
control = cc_num,
|
|
178
|
+
value = max(0, min(127, int(round(val))))
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
self._ramp_pulses(beat_start, beat_end, float(start), float(end), shape, resolution, _event)
|
|
183
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
184
|
+
|
|
185
|
+
# ── Pitch bend ──────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def pitch_bend (self, value: float, beat: float = 0.0) -> "subsequence.pattern_builder.PatternBuilder":
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
Send a single pitch bend message at a beat position.
|
|
191
|
+
|
|
192
|
+
Parameters:
|
|
193
|
+
value: Pitch bend amount, normalised from -1.0 to 1.0.
|
|
194
|
+
beat: Beat position within the pattern.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
# The asymmetric clamp is correct: MIDI's 14-bit bend range is -8192..+8191.
|
|
198
|
+
midi_value = max(-8192, min(8191, int(round(value * 8192))))
|
|
199
|
+
pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
200
|
+
|
|
201
|
+
self._pattern.cc_events.append(
|
|
202
|
+
subsequence.pattern.CcEvent(
|
|
203
|
+
pulse = pulse,
|
|
204
|
+
message_type = 'pitchwheel',
|
|
205
|
+
value = midi_value
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
209
|
+
|
|
210
|
+
def pitch_bend_ramp (
|
|
211
|
+
self,
|
|
212
|
+
start: float,
|
|
213
|
+
end: float,
|
|
214
|
+
beat_start: float = 0.0,
|
|
215
|
+
beat_end: typing.Optional[float] = None,
|
|
216
|
+
resolution: int = 1,
|
|
217
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear"
|
|
218
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
Interpolate pitch bend over a beat range.
|
|
222
|
+
|
|
223
|
+
Parameters:
|
|
224
|
+
start: Starting pitch bend (-1.0 to 1.0).
|
|
225
|
+
end: Ending pitch bend (-1.0 to 1.0).
|
|
226
|
+
beat_start: Beat position to begin the ramp.
|
|
227
|
+
beat_end: Beat position to end the ramp. Defaults to pattern length.
|
|
228
|
+
resolution: Pulses between pitch bend messages (1 = every pulse).
|
|
229
|
+
Higher values (e.g. 2 or 4) reduce MIDI traffic density but may sound
|
|
230
|
+
stepped at slow tempos.
|
|
231
|
+
shape: Easing curve — a name string (e.g. ``"ease_out"``) or any
|
|
232
|
+
callable that maps [0, 1] → [0, 1]. Defaults to ``"linear"``.
|
|
233
|
+
See :mod:`subsequence.easing` for available shapes.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
if beat_end is None:
|
|
237
|
+
beat_end = self._pattern.length
|
|
238
|
+
|
|
239
|
+
def _event (pulse: int, val: float) -> None:
|
|
240
|
+
self._pattern.cc_events.append(
|
|
241
|
+
subsequence.pattern.CcEvent(
|
|
242
|
+
pulse = pulse,
|
|
243
|
+
message_type = 'pitchwheel',
|
|
244
|
+
value = max(-8192, min(8191, int(round(val * 8192))))
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self._ramp_pulses(beat_start, beat_end, start, end, shape, resolution, _event)
|
|
249
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
250
|
+
|
|
251
|
+
# ── RPN / NRPN parameter control ────────────────────────────────────────
|
|
252
|
+
# RPN (Registered) and NRPN (Non-Registered) Parameter Numbers are the
|
|
253
|
+
# standard MIDI conventions for addressing parameters beyond the 128 CC
|
|
254
|
+
# slots, with optional 14-bit value precision. Both are sequences of
|
|
255
|
+
# regular control_change messages co-scheduled at the same pulse:
|
|
256
|
+
# CC 99 / 98 NRPN parameter MSB / LSB (or 101 / 100 for RPN)
|
|
257
|
+
# CC 6 / 38 Data Entry MSB / LSB
|
|
258
|
+
# CC 101=127, 100=127 NULL — defensive deselect
|
|
259
|
+
# The Sequencer's MidiEvent.sequence tie-breaker preserves emission order
|
|
260
|
+
# at the same pulse, so the synth assigns the value to the right parameter.
|
|
261
|
+
|
|
262
|
+
def _append_param_select (self, pulse: int, parameter: int, msb_cc: int, lsb_cc: int) -> None:
|
|
263
|
+
|
|
264
|
+
"""Emit the two-CC parameter-select pair (NRPN: 99/98, RPN: 101/100).
|
|
265
|
+
|
|
266
|
+
Events are emitted on the pattern's channel — leaving ``CcEvent.channel``
|
|
267
|
+
unset (None) lets the sequencer fall through to ``pattern.channel``
|
|
268
|
+
at dispatch time, which is the normal behaviour for every other CC
|
|
269
|
+
method on this mixin.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
param_msb, param_lsb = pymididefs.cc.pack_14bit(parameter)
|
|
273
|
+
|
|
274
|
+
self._pattern.cc_events.append(
|
|
275
|
+
subsequence.pattern.CcEvent(
|
|
276
|
+
pulse = pulse,
|
|
277
|
+
message_type = 'control_change',
|
|
278
|
+
control = msb_cc,
|
|
279
|
+
value = param_msb,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
self._pattern.cc_events.append(
|
|
283
|
+
subsequence.pattern.CcEvent(
|
|
284
|
+
pulse = pulse,
|
|
285
|
+
message_type = 'control_change',
|
|
286
|
+
control = lsb_cc,
|
|
287
|
+
value = param_lsb,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _append_data_entry (self, pulse: int, value: int, fine: bool) -> None:
|
|
292
|
+
|
|
293
|
+
"""Emit Data Entry MSB (and LSB if fine=True) for a parameter value."""
|
|
294
|
+
|
|
295
|
+
if fine:
|
|
296
|
+
value_msb, value_lsb = pymididefs.cc.pack_14bit(value)
|
|
297
|
+
else:
|
|
298
|
+
if not 0 <= value <= 127:
|
|
299
|
+
raise ValueError(f"NRPN/RPN value must be 0–127 when fine=False, got {value}")
|
|
300
|
+
value_msb = value
|
|
301
|
+
value_lsb = None
|
|
302
|
+
|
|
303
|
+
self._pattern.cc_events.append(
|
|
304
|
+
subsequence.pattern.CcEvent(
|
|
305
|
+
pulse = pulse,
|
|
306
|
+
message_type = 'control_change',
|
|
307
|
+
control = pymididefs.cc.DATA_ENTRY_MSB,
|
|
308
|
+
value = value_msb,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if value_lsb is not None:
|
|
313
|
+
self._pattern.cc_events.append(
|
|
314
|
+
subsequence.pattern.CcEvent(
|
|
315
|
+
pulse = pulse,
|
|
316
|
+
message_type = 'control_change',
|
|
317
|
+
control = pymididefs.cc.DATA_ENTRY_LSB,
|
|
318
|
+
value = value_lsb,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def _validate_ramp_endpoints (self, start: int, end: int, fine: bool) -> None:
|
|
323
|
+
|
|
324
|
+
"""Reject out-of-range NRPN/RPN ramp endpoints up front.
|
|
325
|
+
|
|
326
|
+
Mirrors the strict behaviour of ``_append_data_entry`` for one-shots
|
|
327
|
+
so a typo (e.g. forgetting ``fine=True`` with a 14-bit value) raises
|
|
328
|
+
immediately rather than silently clamping at every ramp step.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
limit = 16383 if fine else 127
|
|
332
|
+
|
|
333
|
+
for label, value in (("start", start), ("end", end)):
|
|
334
|
+
if not 0 <= value <= limit:
|
|
335
|
+
raise ValueError(f"NRPN/RPN ramp {label} must be 0–{limit} (fine={fine}), got {value}")
|
|
336
|
+
|
|
337
|
+
def _append_null_reset (self, pulse: int) -> None:
|
|
338
|
+
|
|
339
|
+
"""Emit the RPN NULL sentinel (CC 101=127, CC 100=127) to deselect.
|
|
340
|
+
|
|
341
|
+
Defensive practice: prevents a stray later CC 6 / 38 from being
|
|
342
|
+
applied to whichever parameter was last selected on this channel.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
null_msb, null_lsb = pymididefs.cc.pack_14bit(pymididefs.rpn.NULL_PARAMETER)
|
|
346
|
+
|
|
347
|
+
self._pattern.cc_events.append(
|
|
348
|
+
subsequence.pattern.CcEvent(
|
|
349
|
+
pulse = pulse,
|
|
350
|
+
message_type = 'control_change',
|
|
351
|
+
control = pymididefs.cc.RPN_MSB,
|
|
352
|
+
value = null_msb,
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
self._pattern.cc_events.append(
|
|
356
|
+
subsequence.pattern.CcEvent(
|
|
357
|
+
pulse = pulse,
|
|
358
|
+
message_type = 'control_change',
|
|
359
|
+
control = pymididefs.cc.RPN_LSB,
|
|
360
|
+
value = null_lsb,
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def nrpn (
|
|
365
|
+
self,
|
|
366
|
+
parameter: typing.Union[int, str],
|
|
367
|
+
value: int,
|
|
368
|
+
beat: float = 0.0,
|
|
369
|
+
fine: bool = False,
|
|
370
|
+
null_reset: bool = True,
|
|
371
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
Send a single NRPN parameter write at a beat position.
|
|
375
|
+
|
|
376
|
+
NRPN (Non-Registered Parameter Number) addresses synth-specific
|
|
377
|
+
parameters that don't fit into the 128 standard CC slots — Sequential,
|
|
378
|
+
Korg, Roland, Elektron and others use it heavily for filter cutoff,
|
|
379
|
+
envelope amounts, oscillator detune, and similar deep parameters.
|
|
380
|
+
Many such parameters need values beyond 0–127 (e.g. 0–1023, 0–254);
|
|
381
|
+
set ``fine=True`` for full 14-bit precision.
|
|
382
|
+
|
|
383
|
+
Emitted on the pattern's MIDI channel. To target a different channel
|
|
384
|
+
(e.g. a per-channel RPN config), define a separate pattern on that
|
|
385
|
+
channel or use ``composition.trigger(channel=…)`` for a one-shot.
|
|
386
|
+
|
|
387
|
+
Parameters:
|
|
388
|
+
parameter: 14-bit NRPN parameter number (0–16383), or a string
|
|
389
|
+
resolved via the pattern's ``nrpn_name_map``.
|
|
390
|
+
value: Parameter value. 0–127 if ``fine=False``; 0–16383 if
|
|
391
|
+
``fine=True``.
|
|
392
|
+
beat: Beat position within the pattern.
|
|
393
|
+
fine: If True, send 14-bit value via Data Entry MSB+LSB
|
|
394
|
+
(CC 6 + CC 38). If False (default), send only Data Entry
|
|
395
|
+
MSB — sufficient for the common 0–127 range.
|
|
396
|
+
null_reset: If True (default), follow with the RPN null sentinel
|
|
397
|
+
to deselect the active parameter and prevent stray later
|
|
398
|
+
CC 6 / 38 messages from hitting it.
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
```python
|
|
402
|
+
# Sequential Take 5 fine-tune (14-bit, range 0–1400)
|
|
403
|
+
p.nrpn(9, 700, fine=True)
|
|
404
|
+
|
|
405
|
+
# Roland JV-1080 reverb level (7-bit)
|
|
406
|
+
p.nrpn(0x0140, 80)
|
|
407
|
+
```
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
param = self._resolve_nrpn(parameter)
|
|
411
|
+
pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
412
|
+
|
|
413
|
+
self._append_param_select(pulse, param, pymididefs.cc.NRPN_MSB, pymididefs.cc.NRPN_LSB)
|
|
414
|
+
self._append_data_entry(pulse, value, fine)
|
|
415
|
+
|
|
416
|
+
if null_reset:
|
|
417
|
+
self._append_null_reset(pulse)
|
|
418
|
+
|
|
419
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
420
|
+
|
|
421
|
+
def rpn (
|
|
422
|
+
self,
|
|
423
|
+
parameter: typing.Union[int, str],
|
|
424
|
+
value: int,
|
|
425
|
+
beat: float = 0.0,
|
|
426
|
+
fine: bool = False,
|
|
427
|
+
null_reset: bool = True,
|
|
428
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
429
|
+
|
|
430
|
+
"""
|
|
431
|
+
Send a single RPN parameter write at a beat position.
|
|
432
|
+
|
|
433
|
+
RPN (Registered Parameter Number) addresses the small standardised
|
|
434
|
+
set of parameters defined by the MIDI specification — pitch bend
|
|
435
|
+
range, master tuning, modulation depth — supported by virtually any
|
|
436
|
+
MIDI synth. String names resolve via ``pymididefs.rpn.RPN_MAP``
|
|
437
|
+
out of the box, no map needed.
|
|
438
|
+
|
|
439
|
+
Standard RPN names: ``pitch_bend_sensitivity``,
|
|
440
|
+
``channel_fine_tuning``, ``channel_coarse_tuning``,
|
|
441
|
+
``tuning_program_select``, ``tuning_bank_select``,
|
|
442
|
+
``modulation_depth_range``.
|
|
443
|
+
|
|
444
|
+
Emitted on the pattern's MIDI channel.
|
|
445
|
+
|
|
446
|
+
Parameters:
|
|
447
|
+
parameter: 14-bit RPN parameter number (0–16383), or one of the
|
|
448
|
+
standard string names above.
|
|
449
|
+
value: Parameter value. 0–127 if ``fine=False``; 0–16383 if
|
|
450
|
+
``fine=True``. Pitch bend sensitivity uses MSB = semitones
|
|
451
|
+
and LSB = cents, so set ``fine=True`` for sub-semitone control.
|
|
452
|
+
beat: Beat position within the pattern.
|
|
453
|
+
fine: If True, send 14-bit value via Data Entry MSB+LSB.
|
|
454
|
+
null_reset: If True (default), follow with the RPN null sentinel.
|
|
455
|
+
|
|
456
|
+
Example:
|
|
457
|
+
```python
|
|
458
|
+
# Set pitch bend range to ±12 semitones
|
|
459
|
+
p.rpn("pitch_bend_sensitivity", 12)
|
|
460
|
+
|
|
461
|
+
# 4 semitones plus 50 cents
|
|
462
|
+
p.rpn("pitch_bend_sensitivity", 4 * 128 + 50, fine=True)
|
|
463
|
+
```
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
param = self._resolve_rpn(parameter)
|
|
467
|
+
pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
468
|
+
|
|
469
|
+
self._append_param_select(pulse, param, pymididefs.cc.RPN_MSB, pymididefs.cc.RPN_LSB)
|
|
470
|
+
self._append_data_entry(pulse, value, fine)
|
|
471
|
+
|
|
472
|
+
if null_reset:
|
|
473
|
+
self._append_null_reset(pulse)
|
|
474
|
+
|
|
475
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
476
|
+
|
|
477
|
+
def nrpn_ramp (
|
|
478
|
+
self,
|
|
479
|
+
parameter: typing.Union[int, str],
|
|
480
|
+
start: int,
|
|
481
|
+
end: int,
|
|
482
|
+
beat_start: float = 0.0,
|
|
483
|
+
beat_end: typing.Optional[float] = None,
|
|
484
|
+
resolution: int = 4,
|
|
485
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear",
|
|
486
|
+
fine: bool = True,
|
|
487
|
+
null_reset: bool = True,
|
|
488
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
489
|
+
|
|
490
|
+
"""
|
|
491
|
+
Interpolate an NRPN value over a beat range.
|
|
492
|
+
|
|
493
|
+
The parameter is selected once at ``beat_start``; subsequent steps
|
|
494
|
+
emit only Data Entry messages. Synths track the most recently
|
|
495
|
+
selected NRPN per the spec, so re-selecting per step would just
|
|
496
|
+
waste bandwidth. If ``null_reset=True`` the RPN null sentinel is
|
|
497
|
+
appended once at ``beat_end``.
|
|
498
|
+
|
|
499
|
+
**Mid-ramp parameter persistence:** between ``beat_start`` and
|
|
500
|
+
``beat_end`` the synth still has this NRPN selected. Avoid issuing
|
|
501
|
+
``p.cc(6, …)`` or ``p.cc(38, …)`` on the same channel during the
|
|
502
|
+
ramp window — they would land on the ramped parameter rather than
|
|
503
|
+
acting as plain data-entry CCs.
|
|
504
|
+
|
|
505
|
+
Bandwidth note: with ``fine=True`` (default) every step emits two
|
|
506
|
+
CCs. Default ``resolution=4`` is one update every four pulses
|
|
507
|
+
(~83 ms at 120 BPM, where one pulse is ~21 ms), which keeps the bus
|
|
508
|
+
lightly loaded. Increase
|
|
509
|
+
``resolution`` (e.g. ``8``) on slow DIN-MIDI links if you hear
|
|
510
|
+
other messages getting delayed.
|
|
511
|
+
|
|
512
|
+
Emitted on the pattern's MIDI channel.
|
|
513
|
+
|
|
514
|
+
Parameters:
|
|
515
|
+
parameter: 14-bit NRPN parameter number, or a string resolved
|
|
516
|
+
via the pattern's ``nrpn_name_map``.
|
|
517
|
+
start: Starting value (0–16383 when ``fine=True``, 0–127 when False).
|
|
518
|
+
end: Ending value.
|
|
519
|
+
beat_start: Beat position to begin the ramp.
|
|
520
|
+
beat_end: Beat position to end the ramp. Defaults to pattern length.
|
|
521
|
+
resolution: Pulses between Data Entry messages (default 4).
|
|
522
|
+
shape: Easing curve — string name or callable [0, 1] → [0, 1].
|
|
523
|
+
fine: If True (default), use full 14-bit Data Entry MSB+LSB.
|
|
524
|
+
null_reset: If True (default), append the null sentinel at the
|
|
525
|
+
end of the ramp (not per step).
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
param = self._resolve_nrpn(parameter)
|
|
529
|
+
self._validate_ramp_endpoints(start, end, fine)
|
|
530
|
+
|
|
531
|
+
if beat_end is None:
|
|
532
|
+
beat_end = self._pattern.length
|
|
533
|
+
|
|
534
|
+
pulse_end = int(beat_end * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
535
|
+
|
|
536
|
+
self._append_param_select(int(beat_start * subsequence.constants.MIDI_QUARTER_NOTE), param, pymididefs.cc.NRPN_MSB, pymididefs.cc.NRPN_LSB)
|
|
537
|
+
|
|
538
|
+
def _event (pulse: int, val: float) -> None:
|
|
539
|
+
# Clamp guards against custom easing callables that overshoot [0, 1].
|
|
540
|
+
if fine:
|
|
541
|
+
value = max(0, min(16383, int(round(val))))
|
|
542
|
+
else:
|
|
543
|
+
value = max(0, min(127, int(round(val))))
|
|
544
|
+
self._append_data_entry(pulse, value, fine)
|
|
545
|
+
|
|
546
|
+
self._ramp_pulses(beat_start, beat_end, float(start), float(end), shape, resolution, _event)
|
|
547
|
+
|
|
548
|
+
if null_reset:
|
|
549
|
+
self._append_null_reset(pulse_end)
|
|
550
|
+
|
|
551
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
552
|
+
|
|
553
|
+
def rpn_ramp (
|
|
554
|
+
self,
|
|
555
|
+
parameter: typing.Union[int, str],
|
|
556
|
+
start: int,
|
|
557
|
+
end: int,
|
|
558
|
+
beat_start: float = 0.0,
|
|
559
|
+
beat_end: typing.Optional[float] = None,
|
|
560
|
+
resolution: int = 4,
|
|
561
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear",
|
|
562
|
+
fine: bool = True,
|
|
563
|
+
null_reset: bool = True,
|
|
564
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
565
|
+
|
|
566
|
+
"""Interpolate an RPN value over a beat range.
|
|
567
|
+
|
|
568
|
+
Identical to :meth:`nrpn_ramp` but uses CC 101 / 100 for parameter
|
|
569
|
+
selection. String names resolve via ``pymididefs.rpn.RPN_MAP``.
|
|
570
|
+
The same mid-ramp persistence note applies: avoid plain ``p.cc(6, …)``
|
|
571
|
+
on this channel during the ramp window.
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
param = self._resolve_rpn(parameter)
|
|
575
|
+
self._validate_ramp_endpoints(start, end, fine)
|
|
576
|
+
|
|
577
|
+
if beat_end is None:
|
|
578
|
+
beat_end = self._pattern.length
|
|
579
|
+
|
|
580
|
+
pulse_end = int(beat_end * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
581
|
+
|
|
582
|
+
self._append_param_select(int(beat_start * subsequence.constants.MIDI_QUARTER_NOTE), param, pymididefs.cc.RPN_MSB, pymididefs.cc.RPN_LSB)
|
|
583
|
+
|
|
584
|
+
def _event (pulse: int, val: float) -> None:
|
|
585
|
+
# Clamp guards against custom easing callables that overshoot [0, 1].
|
|
586
|
+
if fine:
|
|
587
|
+
value = max(0, min(16383, int(round(val))))
|
|
588
|
+
else:
|
|
589
|
+
value = max(0, min(127, int(round(val))))
|
|
590
|
+
self._append_data_entry(pulse, value, fine)
|
|
591
|
+
|
|
592
|
+
self._ramp_pulses(beat_start, beat_end, float(start), float(end), shape, resolution, _event)
|
|
593
|
+
|
|
594
|
+
if null_reset:
|
|
595
|
+
self._append_null_reset(pulse_end)
|
|
596
|
+
|
|
597
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
598
|
+
|
|
599
|
+
# ── Program change and SysEx ─────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
def program_change (
|
|
602
|
+
self,
|
|
603
|
+
program: int,
|
|
604
|
+
beat: float = 0.0,
|
|
605
|
+
bank_msb: typing.Optional[int] = None,
|
|
606
|
+
bank_lsb: typing.Optional[int] = None,
|
|
607
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
608
|
+
|
|
609
|
+
"""Send a Program Change message, optionally preceded by bank select.
|
|
610
|
+
|
|
611
|
+
Switches the instrument patch on this pattern's MIDI channel.
|
|
612
|
+
Program numbers follow the General MIDI numbering (0–127, where
|
|
613
|
+
e.g. 0 = Acoustic Grand Piano, 40 = Violin, 33 = Electric Bass).
|
|
614
|
+
|
|
615
|
+
To select a patch in a specific bank, provide ``bank_msb`` and/or
|
|
616
|
+
``bank_lsb``. The bank select CC messages (CC 0 for MSB, CC 32 for
|
|
617
|
+
LSB) are sent at the same beat position immediately before the
|
|
618
|
+
program change, in the order the synthesizer expects.
|
|
619
|
+
|
|
620
|
+
Parameters:
|
|
621
|
+
program: Program (patch) number (0–127).
|
|
622
|
+
beat: Beat position within the pattern (default 0.0).
|
|
623
|
+
bank_msb: Bank select coarse (CC 0), 0–127. ``None`` = omit.
|
|
624
|
+
bank_lsb: Bank select fine (CC 32), 0–127. ``None`` = omit.
|
|
625
|
+
|
|
626
|
+
Example:
|
|
627
|
+
```python
|
|
628
|
+
@composition.pattern(channel=1, beats=4)
|
|
629
|
+
def strings (p):
|
|
630
|
+
# GM — no bank needed
|
|
631
|
+
p.program_change(48)
|
|
632
|
+
|
|
633
|
+
# Roland JV-1080 bank 1, patch 48
|
|
634
|
+
p.program_change(48, bank_msb=81, bank_lsb=0)
|
|
635
|
+
|
|
636
|
+
# Change patch only at the first bar of each section
|
|
637
|
+
if p.section.bar == 0:
|
|
638
|
+
p.program_change(48, bank_msb=1)
|
|
639
|
+
```
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
643
|
+
|
|
644
|
+
if bank_msb is not None:
|
|
645
|
+
self._pattern.cc_events.append(
|
|
646
|
+
subsequence.pattern.CcEvent(
|
|
647
|
+
pulse = pulse,
|
|
648
|
+
message_type = 'control_change',
|
|
649
|
+
control = 0,
|
|
650
|
+
value = max(0, min(127, int(round(bank_msb)))),
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if bank_lsb is not None:
|
|
655
|
+
self._pattern.cc_events.append(
|
|
656
|
+
subsequence.pattern.CcEvent(
|
|
657
|
+
pulse = pulse,
|
|
658
|
+
message_type = 'control_change',
|
|
659
|
+
control = 32,
|
|
660
|
+
value = max(0, min(127, int(round(bank_lsb)))),
|
|
661
|
+
)
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
self._pattern.cc_events.append(
|
|
665
|
+
subsequence.pattern.CcEvent(
|
|
666
|
+
pulse = pulse,
|
|
667
|
+
message_type = 'program_change',
|
|
668
|
+
value = max(0, min(127, int(round(program)))),
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
672
|
+
|
|
673
|
+
def sysex (self, data: typing.Union[bytes, typing.List[int]], beat: float = 0.0) -> "subsequence.pattern_builder.PatternBuilder":
|
|
674
|
+
|
|
675
|
+
"""
|
|
676
|
+
Send a System Exclusive (SysEx) message at a beat position.
|
|
677
|
+
|
|
678
|
+
SysEx messages allow deep integration with synthesizers and other
|
|
679
|
+
hardware: patch dumps, parameter control, and vendor-specific commands.
|
|
680
|
+
The ``data`` argument should contain only the inner payload bytes,
|
|
681
|
+
without the surrounding ``0xF0`` / ``0xF7`` framing — mido adds those
|
|
682
|
+
automatically.
|
|
683
|
+
|
|
684
|
+
Parameters:
|
|
685
|
+
data: SysEx payload as ``bytes`` or a list of integers (0–127).
|
|
686
|
+
beat: Beat position within the pattern (default 0.0).
|
|
687
|
+
|
|
688
|
+
Example:
|
|
689
|
+
```python
|
|
690
|
+
# GM System On — reset a GM-compatible device to defaults
|
|
691
|
+
p.sysex([0x7E, 0x7F, 0x09, 0x01])
|
|
692
|
+
```
|
|
693
|
+
"""
|
|
694
|
+
|
|
695
|
+
# Validate at build time: MIDI sysex payloads are 7-bit. A byte over
|
|
696
|
+
# 127 would be rejected by mido at dispatch and the message silently
|
|
697
|
+
# dropped every cycle with a misleading "device disconnected" log.
|
|
698
|
+
invalid = [b for b in data if not 0 <= b <= 127]
|
|
699
|
+
|
|
700
|
+
if invalid:
|
|
701
|
+
raise ValueError(
|
|
702
|
+
f"sysex data bytes must be 0-127 (7-bit MIDI data) — got {invalid[:4]}. "
|
|
703
|
+
"Mask computed values (checksums, packed parameters) with & 0x7F first."
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
707
|
+
|
|
708
|
+
self._pattern.cc_events.append(
|
|
709
|
+
subsequence.pattern.CcEvent(
|
|
710
|
+
pulse = pulse,
|
|
711
|
+
message_type = 'sysex',
|
|
712
|
+
data = bytes(data)
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
716
|
+
|
|
717
|
+
# ── OSC messages ─────────────────────────────────────────────────────────
|
|
718
|
+
|
|
719
|
+
def osc (self, address: str, *args: typing.Any, beat: float = 0.0) -> "subsequence.pattern_builder.PatternBuilder":
|
|
720
|
+
|
|
721
|
+
"""
|
|
722
|
+
Send an OSC message at a beat position.
|
|
723
|
+
|
|
724
|
+
Requires ``composition.osc()`` to be called before ``composition.play()``.
|
|
725
|
+
If no OSC server is configured the event is silently dropped.
|
|
726
|
+
|
|
727
|
+
Parameters:
|
|
728
|
+
address: OSC address path (e.g. ``"/mixer/fader/1"``).
|
|
729
|
+
``*args``: OSC arguments — float, int, str, or bytes.
|
|
730
|
+
beat: Beat position within the pattern (default 0.0).
|
|
731
|
+
|
|
732
|
+
Example:
|
|
733
|
+
```python
|
|
734
|
+
# Enable a chorus effect at beat 2
|
|
735
|
+
p.osc("/fx/chorus/enable", 1, beat=2.0)
|
|
736
|
+
|
|
737
|
+
# Set a mixer pan value immediately
|
|
738
|
+
p.osc("/mixer/pan/1", -0.5)
|
|
739
|
+
```
|
|
740
|
+
"""
|
|
741
|
+
|
|
742
|
+
pulse = int(beat * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
743
|
+
|
|
744
|
+
self._pattern.osc_events.append(
|
|
745
|
+
subsequence.pattern.OscEvent(
|
|
746
|
+
pulse = pulse,
|
|
747
|
+
address = address,
|
|
748
|
+
args = args
|
|
749
|
+
)
|
|
750
|
+
)
|
|
751
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
752
|
+
|
|
753
|
+
def osc_ramp (
|
|
754
|
+
self,
|
|
755
|
+
address: str,
|
|
756
|
+
start: float,
|
|
757
|
+
end: float,
|
|
758
|
+
beat_start: float = 0.0,
|
|
759
|
+
beat_end: typing.Optional[float] = None,
|
|
760
|
+
resolution: int = 4,
|
|
761
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear"
|
|
762
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
763
|
+
|
|
764
|
+
"""
|
|
765
|
+
Interpolate an OSC float value over a beat range.
|
|
766
|
+
|
|
767
|
+
Generates one OSC message per ``resolution`` pulses, sending the
|
|
768
|
+
interpolated value to ``address`` at each step. Useful for smoothly
|
|
769
|
+
automating mixer faders, effect parameters, and other continuous controls
|
|
770
|
+
on a remote machine.
|
|
771
|
+
|
|
772
|
+
Requires ``composition.osc()`` to be called before ``composition.play()``.
|
|
773
|
+
If no OSC server is configured the events are silently dropped.
|
|
774
|
+
|
|
775
|
+
Parameters:
|
|
776
|
+
address: OSC address path (e.g. ``"/mixer/fader/1"``).
|
|
777
|
+
start: Starting float value.
|
|
778
|
+
end: Ending float value.
|
|
779
|
+
beat_start: Beat position to begin the ramp (default 0.0).
|
|
780
|
+
beat_end: Beat position to end the ramp. Defaults to pattern length.
|
|
781
|
+
resolution: Pulses between OSC messages (default 4 — approximately
|
|
782
|
+
6 messages per beat at 120 BPM, which is smooth for fader
|
|
783
|
+
automation while keeping UDP traffic light). Use ``resolution=1``
|
|
784
|
+
for pulse-level precision.
|
|
785
|
+
shape: Easing curve — a name string (e.g. ``"ease_in"``) or any
|
|
786
|
+
callable that maps [0, 1] → [0, 1]. Defaults to ``"linear"``.
|
|
787
|
+
See :mod:`subsequence.easing` for available shapes.
|
|
788
|
+
|
|
789
|
+
Example:
|
|
790
|
+
```python
|
|
791
|
+
# Fade a mixer fader up over 4 beats
|
|
792
|
+
p.osc_ramp("/mixer/fader/1", start=0.0, end=1.0)
|
|
793
|
+
|
|
794
|
+
# Ease in a reverb send over the last 2 beats
|
|
795
|
+
p.osc_ramp("/fx/reverb/wet", 0.0, 0.8, beat_start=2, beat_end=4, shape="ease_in")
|
|
796
|
+
```
|
|
797
|
+
"""
|
|
798
|
+
|
|
799
|
+
if beat_end is None:
|
|
800
|
+
beat_end = self._pattern.length
|
|
801
|
+
|
|
802
|
+
def _event (pulse: int, val: float) -> None:
|
|
803
|
+
self._pattern.osc_events.append(
|
|
804
|
+
subsequence.pattern.OscEvent(
|
|
805
|
+
pulse = pulse,
|
|
806
|
+
address = address,
|
|
807
|
+
args = (val,)
|
|
808
|
+
)
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
self._ramp_pulses(beat_start, beat_end, start, end, shape, resolution, _event)
|
|
812
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
813
|
+
|
|
814
|
+
# ── Note-correlated pitch bend ────────────────────────────────────────────
|
|
815
|
+
|
|
816
|
+
def _generate_bend_events (
|
|
817
|
+
self,
|
|
818
|
+
start_value: float,
|
|
819
|
+
end_value: float,
|
|
820
|
+
pulse_start: int,
|
|
821
|
+
pulse_end: int,
|
|
822
|
+
resolution: int,
|
|
823
|
+
shape: typing.Union[str, subsequence.easing.EasingFn],
|
|
824
|
+
) -> None:
|
|
825
|
+
|
|
826
|
+
"""Generate a series of pitchwheel CcEvents between two pulse positions.
|
|
827
|
+
|
|
828
|
+
Used by ``bend()``, ``portamento()``, and ``slide()``. Delegates the
|
|
829
|
+
span/resolution walk (including the emit-the-endpoint rule) to
|
|
830
|
+
:meth:`_ramp_pulse_span` and contributes only the pitchwheel
|
|
831
|
+
conversion — normalised value scaled to 14-bit and clamped — appending
|
|
832
|
+
events directly to ``self._pattern.cc_events``.
|
|
833
|
+
|
|
834
|
+
Parameters:
|
|
835
|
+
start_value: Normalised bend at the start of the ramp (-1.0 to 1.0).
|
|
836
|
+
end_value: Normalised bend at the end of the ramp (-1.0 to 1.0).
|
|
837
|
+
pulse_start: Absolute pulse position to start the ramp.
|
|
838
|
+
pulse_end: Absolute pulse position to end the ramp.
|
|
839
|
+
resolution: Number of pulses between consecutive events.
|
|
840
|
+
shape: Easing curve name or callable.
|
|
841
|
+
"""
|
|
842
|
+
|
|
843
|
+
def _event (pulse: int, val: float) -> None:
|
|
844
|
+
midi_value = max(-8192, min(8191, int(round(val * 8192))))
|
|
845
|
+
self._pattern.cc_events.append(
|
|
846
|
+
subsequence.pattern.CcEvent(
|
|
847
|
+
pulse = pulse,
|
|
848
|
+
message_type = 'pitchwheel',
|
|
849
|
+
value = midi_value,
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
self._ramp_pulse_span(pulse_start, pulse_end, start_value, end_value, shape, resolution, _event)
|
|
854
|
+
|
|
855
|
+
def bend (
|
|
856
|
+
self,
|
|
857
|
+
note: int,
|
|
858
|
+
amount: float,
|
|
859
|
+
start: float = 0.0,
|
|
860
|
+
end: float = 1.0,
|
|
861
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear",
|
|
862
|
+
resolution: int = 1,
|
|
863
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
864
|
+
|
|
865
|
+
"""Bend a specific note by index.
|
|
866
|
+
|
|
867
|
+
Generates a pitch bend ramp that covers a fraction of the target note's
|
|
868
|
+
duration, then resets to 0.0 at the next note's onset. Call this
|
|
869
|
+
*after* ``legato()`` / ``detached()`` / ``duration()`` so that note durations are final.
|
|
870
|
+
|
|
871
|
+
Parameters:
|
|
872
|
+
note: Note index (0 = first, -1 = last, etc.).
|
|
873
|
+
amount: Target bend normalised to -1.0..1.0 (positive = up).
|
|
874
|
+
With a standard ±2-semitone pitch wheel range, 0.5 = 1 semitone.
|
|
875
|
+
start: Fraction of the note's duration at which the ramp begins
|
|
876
|
+
(0.0 = note onset, default).
|
|
877
|
+
end: Fraction of the note's duration at which the ramp ends
|
|
878
|
+
(1.0 = note end, default).
|
|
879
|
+
shape: Easing curve — a name string (e.g. ``"ease_in"``) or any
|
|
880
|
+
callable mapping [0, 1] → [0, 1]. Defaults to ``"linear"``.
|
|
881
|
+
resolution: Pulses between pitch bend messages.
|
|
882
|
+
|
|
883
|
+
Raises:
|
|
884
|
+
IndexError: If *note* is out of range for the current pattern.
|
|
885
|
+
|
|
886
|
+
Example:
|
|
887
|
+
```python
|
|
888
|
+
p.sequence(steps=[0, 4, 8, 12], pitches=midi_notes.E1)
|
|
889
|
+
p.legato(0.95)
|
|
890
|
+
|
|
891
|
+
# Bend the last note up one semitone (with ±2 st range), easing in
|
|
892
|
+
p.bend(note=-1, amount=0.5, shape="ease_in")
|
|
893
|
+
|
|
894
|
+
# Bend the second note down, starting halfway through
|
|
895
|
+
p.bend(note=1, amount=-0.3, start=0.5)
|
|
896
|
+
```
|
|
897
|
+
"""
|
|
898
|
+
|
|
899
|
+
if not self._pattern.steps:
|
|
900
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
901
|
+
|
|
902
|
+
sorted_positions = sorted(self._pattern.steps.keys())
|
|
903
|
+
|
|
904
|
+
# Resolve note index (supports negative indexing)
|
|
905
|
+
position = sorted_positions[note]
|
|
906
|
+
note_idx = note if note >= 0 else len(sorted_positions) + note
|
|
907
|
+
|
|
908
|
+
# Duration: use the longest note at this step
|
|
909
|
+
step = self._pattern.steps[position]
|
|
910
|
+
note_duration = max(n.duration for n in step.notes)
|
|
911
|
+
|
|
912
|
+
# Clamp start/end fractions and compute pulse range for the ramp
|
|
913
|
+
start_clamped = max(0.0, min(1.0, start))
|
|
914
|
+
end_clamped = max(0.0, min(1.0, end))
|
|
915
|
+
bend_start_pulse = position + int(note_duration * start_clamped)
|
|
916
|
+
bend_end_pulse = position + int(note_duration * end_clamped)
|
|
917
|
+
|
|
918
|
+
self._generate_bend_events(0.0, amount, bend_start_pulse, bend_end_pulse, resolution, shape)
|
|
919
|
+
|
|
920
|
+
# Reset bend at the next note's onset. For the last note that is the
|
|
921
|
+
# NEXT cycle's first onset (total + first), not pulse 0 - a bend tail
|
|
922
|
+
# spilling past the cycle end was cancelled mid-flight by a pulse-0
|
|
923
|
+
# reset, leaving the next cycle's first note bent.
|
|
924
|
+
if note_idx < len(sorted_positions) - 1:
|
|
925
|
+
reset_pulse = sorted_positions[note_idx + 1]
|
|
926
|
+
else:
|
|
927
|
+
total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
928
|
+
reset_pulse = total_pulses + sorted_positions[0]
|
|
929
|
+
|
|
930
|
+
reset_midi = max(-8192, min(8191, int(round(0.0 * 8192))))
|
|
931
|
+
self._pattern.cc_events.append(
|
|
932
|
+
subsequence.pattern.CcEvent(
|
|
933
|
+
pulse = reset_pulse,
|
|
934
|
+
message_type = 'pitchwheel',
|
|
935
|
+
value = reset_midi,
|
|
936
|
+
)
|
|
937
|
+
)
|
|
938
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
939
|
+
|
|
940
|
+
def portamento (
|
|
941
|
+
self,
|
|
942
|
+
time: float = 0.15,
|
|
943
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear",
|
|
944
|
+
resolution: int = 1,
|
|
945
|
+
bend_range: typing.Optional[float] = 2.0,
|
|
946
|
+
wrap: bool = True,
|
|
947
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
948
|
+
|
|
949
|
+
"""Glide between all consecutive notes using pitch bend.
|
|
950
|
+
|
|
951
|
+
Generates a pitch bend ramp in the tail of each note, bending toward
|
|
952
|
+
the next note's pitch, then resets at the next note's onset. Call this
|
|
953
|
+
*after* ``legato()`` / ``detached()`` / ``duration()`` so that note durations are final.
|
|
954
|
+
|
|
955
|
+
Most effective on mono instruments where pitch bend is per-channel.
|
|
956
|
+
|
|
957
|
+
Parameters:
|
|
958
|
+
time: Fraction of each note's duration used for the glide
|
|
959
|
+
(default 0.15 — last 15% of the note).
|
|
960
|
+
shape: Easing curve. Defaults to ``"linear"``.
|
|
961
|
+
resolution: Pulses between pitch bend messages.
|
|
962
|
+
bend_range: Instrument's pitch wheel range in semitones
|
|
963
|
+
(default 2.0 — standard ±2 st). Pairs with intervals larger
|
|
964
|
+
than this value are skipped. Pass ``None`` to disable range
|
|
965
|
+
checking and always generate the bend (large intervals are
|
|
966
|
+
clamped to ±1.0).
|
|
967
|
+
wrap: If ``True`` (default), glide from the last note toward the
|
|
968
|
+
first note of the next cycle.
|
|
969
|
+
|
|
970
|
+
Example:
|
|
971
|
+
```python
|
|
972
|
+
p.sequence(steps=[0, 4, 8, 12], pitches=[40, 42, 40, 43])
|
|
973
|
+
p.legato(0.95)
|
|
974
|
+
|
|
975
|
+
# Gentle glide across all note transitions
|
|
976
|
+
p.portamento(time=0.15, shape="ease_in_out")
|
|
977
|
+
|
|
978
|
+
# Wide bend range (synth set to ±12 semitones)
|
|
979
|
+
p.portamento(time=0.2, bend_range=12)
|
|
980
|
+
|
|
981
|
+
# No range limit — bend as far as MIDI allows
|
|
982
|
+
p.portamento(time=0.1, bend_range=None)
|
|
983
|
+
```
|
|
984
|
+
"""
|
|
985
|
+
|
|
986
|
+
if bend_range is not None and bend_range <= 0:
|
|
987
|
+
raise ValueError(
|
|
988
|
+
f"bend_range must be a positive number of semitones (your instrument's "
|
|
989
|
+
f"pitch-wheel range) — got {bend_range}. Pass None to disable range checking."
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
if not self._pattern.steps:
|
|
993
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
994
|
+
|
|
995
|
+
sorted_positions = sorted(self._pattern.steps.keys())
|
|
996
|
+
n = len(sorted_positions)
|
|
997
|
+
|
|
998
|
+
def _lowest_pitch (pos: int) -> int:
|
|
999
|
+
return min(note.pitch for note in self._pattern.steps[pos].notes)
|
|
1000
|
+
|
|
1001
|
+
def _longest_duration (pos: int) -> int:
|
|
1002
|
+
return max(note.duration for note in self._pattern.steps[pos].notes)
|
|
1003
|
+
|
|
1004
|
+
for i in range(n):
|
|
1005
|
+
a_pos = sorted_positions[i]
|
|
1006
|
+
is_last = (i == n - 1)
|
|
1007
|
+
|
|
1008
|
+
if is_last:
|
|
1009
|
+
if not wrap:
|
|
1010
|
+
continue
|
|
1011
|
+
b_pos = sorted_positions[0]
|
|
1012
|
+
else:
|
|
1013
|
+
b_pos = sorted_positions[i + 1]
|
|
1014
|
+
|
|
1015
|
+
interval = _lowest_pitch(b_pos) - _lowest_pitch(a_pos)
|
|
1016
|
+
|
|
1017
|
+
if bend_range is not None and abs(interval) > bend_range:
|
|
1018
|
+
continue
|
|
1019
|
+
|
|
1020
|
+
normaliser = bend_range if bend_range is not None else 2.0
|
|
1021
|
+
amount = max(-1.0, min(1.0, interval / normaliser))
|
|
1022
|
+
|
|
1023
|
+
a_duration = _longest_duration(a_pos)
|
|
1024
|
+
glide_start_pulse = a_pos + int(a_duration * (1.0 - time))
|
|
1025
|
+
glide_end_pulse = a_pos + a_duration
|
|
1026
|
+
|
|
1027
|
+
self._generate_bend_events(0.0, amount, glide_start_pulse, glide_end_pulse, resolution, shape)
|
|
1028
|
+
|
|
1029
|
+
# Reset at the destination note's onset. For the wrap-around pair
|
|
1030
|
+
# that is the NEXT cycle's first onset (total + first), not pulse 0
|
|
1031
|
+
# - a glide spilling past the cycle end was cancelled mid-flight by
|
|
1032
|
+
# the pulse-0 reset, leaving the first note fully bent.
|
|
1033
|
+
if is_last:
|
|
1034
|
+
total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
1035
|
+
reset_pulse = total_pulses + sorted_positions[0]
|
|
1036
|
+
else:
|
|
1037
|
+
reset_pulse = b_pos
|
|
1038
|
+
self._pattern.cc_events.append(
|
|
1039
|
+
subsequence.pattern.CcEvent(
|
|
1040
|
+
pulse = reset_pulse,
|
|
1041
|
+
message_type = 'pitchwheel',
|
|
1042
|
+
value = 0,
|
|
1043
|
+
)
|
|
1044
|
+
)
|
|
1045
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1046
|
+
|
|
1047
|
+
def slide (
|
|
1048
|
+
self,
|
|
1049
|
+
notes: typing.Optional[typing.List[int]] = None,
|
|
1050
|
+
steps: typing.Optional[typing.List[int]] = None,
|
|
1051
|
+
time: float = 0.15,
|
|
1052
|
+
shape: typing.Union[str, subsequence.easing.EasingFn] = "linear",
|
|
1053
|
+
resolution: int = 1,
|
|
1054
|
+
bend_range: typing.Optional[float] = 2.0,
|
|
1055
|
+
wrap: bool = True,
|
|
1056
|
+
extend: bool = True,
|
|
1057
|
+
) -> "subsequence.pattern_builder.PatternBuilder":
|
|
1058
|
+
|
|
1059
|
+
"""TB-303-style selective slide into specific notes.
|
|
1060
|
+
|
|
1061
|
+
Like ``portamento()`` but only applies to flagged destination notes.
|
|
1062
|
+
Specify target notes by index (``notes=[1, 3]``) or by step grid
|
|
1063
|
+
position (``steps=[4, 12]``). If ``extend=True`` (default) the
|
|
1064
|
+
preceding note's duration is extended to meet the slide target, matching
|
|
1065
|
+
the 303's behaviour where slide notes do not retrigger.
|
|
1066
|
+
|
|
1067
|
+
Call this *after* ``legato()`` / ``detached()`` / ``duration()`` so that note durations
|
|
1068
|
+
are final.
|
|
1069
|
+
|
|
1070
|
+
Parameters:
|
|
1071
|
+
notes: List of note indices to slide *into* (0 = first).
|
|
1072
|
+
Supports negative indexing. Mutually exclusive with *steps*.
|
|
1073
|
+
steps: List of step grid indices to slide *into*.
|
|
1074
|
+
Converted to pulse positions using ``self._default_grid``.
|
|
1075
|
+
Mutually exclusive with *notes*.
|
|
1076
|
+
time: Fraction of the preceding note's duration used for the glide.
|
|
1077
|
+
shape: Easing curve. Defaults to ``"linear"``.
|
|
1078
|
+
resolution: Pulses between pitch bend messages.
|
|
1079
|
+
bend_range: Instrument's pitch wheel range in semitones
|
|
1080
|
+
(default 2.0). Pairs with larger intervals are skipped.
|
|
1081
|
+
Pass ``None`` to disable range checking.
|
|
1082
|
+
wrap: If ``True`` (default), include a wrap-around slide from the
|
|
1083
|
+
last note back toward the first.
|
|
1084
|
+
extend: If ``True`` (default), extend the preceding note's duration
|
|
1085
|
+
to reach the slide target's onset — 303-style legato through
|
|
1086
|
+
the glide.
|
|
1087
|
+
|
|
1088
|
+
Raises:
|
|
1089
|
+
ValueError: If neither or both of *notes* and *steps* are provided,
|
|
1090
|
+
or a note index falls outside the pattern's notes.
|
|
1091
|
+
|
|
1092
|
+
Example:
|
|
1093
|
+
```python
|
|
1094
|
+
p.sequence(steps=[0, 4, 8, 12], pitches=[40, 42, 40, 43])
|
|
1095
|
+
p.legato(0.95)
|
|
1096
|
+
|
|
1097
|
+
# Slide into the 2nd and 4th notes
|
|
1098
|
+
p.slide(notes=[1, 3], time=0.2, shape="ease_in")
|
|
1099
|
+
|
|
1100
|
+
# Same using step grid indices
|
|
1101
|
+
p.slide(steps=[4, 12], time=0.2, shape="ease_in")
|
|
1102
|
+
|
|
1103
|
+
# Slide without extending the preceding note
|
|
1104
|
+
p.slide(notes=[1, 3], extend=False)
|
|
1105
|
+
```
|
|
1106
|
+
"""
|
|
1107
|
+
|
|
1108
|
+
if notes is None and steps is None:
|
|
1109
|
+
raise ValueError("slide() requires either 'notes' or 'steps'")
|
|
1110
|
+
|
|
1111
|
+
if notes is not None and steps is not None:
|
|
1112
|
+
raise ValueError("slide() takes notes= or steps=, not both — they name the same slide targets two different ways")
|
|
1113
|
+
|
|
1114
|
+
if bend_range is not None and bend_range <= 0:
|
|
1115
|
+
raise ValueError(
|
|
1116
|
+
f"bend_range must be a positive number of semitones (your instrument's "
|
|
1117
|
+
f"pitch-wheel range) — got {bend_range}. Pass None to disable range checking."
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
if not self._pattern.steps:
|
|
1121
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|
|
1122
|
+
|
|
1123
|
+
sorted_positions = sorted(self._pattern.steps.keys())
|
|
1124
|
+
total_pulses = int(self._pattern.length * subsequence.constants.MIDI_QUARTER_NOTE)
|
|
1125
|
+
n = len(sorted_positions)
|
|
1126
|
+
|
|
1127
|
+
# Resolve flagged pulse positions
|
|
1128
|
+
if notes is not None:
|
|
1129
|
+
flagged: typing.Set[int] = set()
|
|
1130
|
+
for idx in notes:
|
|
1131
|
+
if not -n <= idx < n:
|
|
1132
|
+
raise ValueError(f"slide() note index {idx} is outside this pattern's {n} notes")
|
|
1133
|
+
|
|
1134
|
+
flagged.add(sorted_positions[idx])
|
|
1135
|
+
else:
|
|
1136
|
+
# steps is not None. Resolve each grid step to the SAME pulse the
|
|
1137
|
+
# placement methods use — int(step * (length / grid) * PPQ) — so the
|
|
1138
|
+
# flag lands on the note even when the grid doesn't divide the bar
|
|
1139
|
+
# evenly. Floored uniform spacing (total_pulses // grid) drifts out of
|
|
1140
|
+
# alignment on non-divisor grids, silently flagging nothing.
|
|
1141
|
+
step_beats = self._pattern.length / self._default_grid
|
|
1142
|
+
flagged = set()
|
|
1143
|
+
for s in (steps or []):
|
|
1144
|
+
flagged.add(int(s * step_beats * subsequence.constants.MIDI_QUARTER_NOTE))
|
|
1145
|
+
|
|
1146
|
+
def _lowest_pitch (pos: int) -> int:
|
|
1147
|
+
return min(note.pitch for note in self._pattern.steps[pos].notes)
|
|
1148
|
+
|
|
1149
|
+
def _longest_duration (pos: int) -> int:
|
|
1150
|
+
return max(note.duration for note in self._pattern.steps[pos].notes)
|
|
1151
|
+
|
|
1152
|
+
for i in range(n):
|
|
1153
|
+
a_pos = sorted_positions[i]
|
|
1154
|
+
is_last = (i == n - 1)
|
|
1155
|
+
|
|
1156
|
+
if is_last:
|
|
1157
|
+
if not wrap:
|
|
1158
|
+
continue
|
|
1159
|
+
b_pos = sorted_positions[0]
|
|
1160
|
+
else:
|
|
1161
|
+
b_pos = sorted_positions[i + 1]
|
|
1162
|
+
|
|
1163
|
+
# Only generate glide if the destination is flagged
|
|
1164
|
+
if b_pos not in flagged:
|
|
1165
|
+
continue
|
|
1166
|
+
|
|
1167
|
+
interval = _lowest_pitch(b_pos) - _lowest_pitch(a_pos)
|
|
1168
|
+
|
|
1169
|
+
if bend_range is not None and abs(interval) > bend_range:
|
|
1170
|
+
continue
|
|
1171
|
+
|
|
1172
|
+
normaliser = bend_range if bend_range is not None else 2.0
|
|
1173
|
+
amount = max(-1.0, min(1.0, interval / normaliser))
|
|
1174
|
+
|
|
1175
|
+
# Optionally extend preceding note to meet the target onset (303 style)
|
|
1176
|
+
if extend:
|
|
1177
|
+
if is_last:
|
|
1178
|
+
gap = (total_pulses - a_pos) + sorted_positions[0]
|
|
1179
|
+
else:
|
|
1180
|
+
gap = b_pos - a_pos
|
|
1181
|
+
for note in self._pattern.steps[a_pos].notes:
|
|
1182
|
+
note.duration = gap
|
|
1183
|
+
|
|
1184
|
+
# Read the duration AFTER any extension so the glide occupies the
|
|
1185
|
+
# tail of the note as actually played and lands on the target
|
|
1186
|
+
# onset. (Reading it before the extend block made the bend jump
|
|
1187
|
+
# near the note's start and then hold flat - the opposite of a
|
|
1188
|
+
# slide.)
|
|
1189
|
+
a_duration = _longest_duration(a_pos)
|
|
1190
|
+
|
|
1191
|
+
glide_start_pulse = a_pos + int(a_duration * (1.0 - time))
|
|
1192
|
+
glide_end_pulse = a_pos + a_duration
|
|
1193
|
+
|
|
1194
|
+
self._generate_bend_events(0.0, amount, glide_start_pulse, glide_end_pulse, resolution, shape)
|
|
1195
|
+
|
|
1196
|
+
# Reset at the destination note's onset. For the wrap-around pair
|
|
1197
|
+
# the destination is the NEXT cycle's first onset (total_pulses +
|
|
1198
|
+
# first onset): resetting at pulse 0 fired while a spilled glide
|
|
1199
|
+
# was still in flight, so the destination note played fully bent.
|
|
1200
|
+
reset_pulse = b_pos if not is_last else total_pulses + sorted_positions[0]
|
|
1201
|
+
self._pattern.cc_events.append(
|
|
1202
|
+
subsequence.pattern.CcEvent(
|
|
1203
|
+
pulse = reset_pulse,
|
|
1204
|
+
message_type = 'pitchwheel',
|
|
1205
|
+
value = 0,
|
|
1206
|
+
)
|
|
1207
|
+
)
|
|
1208
|
+
return typing.cast("subsequence.pattern_builder.PatternBuilder", self)
|