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,665 @@
|
|
|
1
|
+
"""Compositional form tracking — section sequences, transitions, and lookahead.
|
|
2
|
+
|
|
3
|
+
Defines :class:`SectionInfo` (immutable per-bar snapshot) and
|
|
4
|
+
:class:`FormState` (the stateful form engine that advances through sections).
|
|
5
|
+
|
|
6
|
+
These are registered on the :class:`~subsequence.composition.Composition`
|
|
7
|
+
via :meth:`~subsequence.composition.Composition.form` and read by pattern
|
|
8
|
+
builders through ``p.section``.
|
|
9
|
+
|
|
10
|
+
A form may be a :class:`~subsequence.forms.Form` value (Sections with
|
|
11
|
+
energy/key payloads), a plain list of ``(name, bars)`` tuples or Sections,
|
|
12
|
+
a generator yielding ``(name, bars)`` pairs, or a weighted-graph dict.
|
|
13
|
+
Everything normalises to :class:`~subsequence.forms.Section` internally —
|
|
14
|
+
the payload travels with the section either way.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import dataclasses
|
|
18
|
+
import logging
|
|
19
|
+
import random
|
|
20
|
+
import typing
|
|
21
|
+
|
|
22
|
+
import subsequence.forms
|
|
23
|
+
import subsequence.weighted_graph
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Form-end behaviours (sequence and generator modes; graphs end via their
|
|
30
|
+
# terminal sections).
|
|
31
|
+
_AT_END_CHOICES: typing.FrozenSet[str] = frozenset({"stop", "hold", "loop"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclasses.dataclass
|
|
35
|
+
class SectionInfo:
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
An immutable snapshot of the current section in the compositional form.
|
|
39
|
+
|
|
40
|
+
Patterns read ``p.section`` to make context-aware decisions, such as increasing
|
|
41
|
+
intensity as a section progresses or playing variation only in certain blocks.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
name: The string name of the section (e.g., "verse").
|
|
45
|
+
bar: The current bar index within this section (0-indexed).
|
|
46
|
+
bars: Total number of bars in this section.
|
|
47
|
+
index: The global index of this section in the form's timeline.
|
|
48
|
+
next_section: The name of the upcoming section (or ``None`` if the
|
|
49
|
+
form will end after this section). This is pre-decided when
|
|
50
|
+
the current section begins, so patterns can plan lead-ins.
|
|
51
|
+
A performer or code can override it with ``composition.form_next()``.
|
|
52
|
+
energy: The section's energy payload (0.5 unless a bound
|
|
53
|
+
:class:`~subsequence.forms.Form` says otherwise; the
|
|
54
|
+
``composition.energy()`` dict overrides it at read time).
|
|
55
|
+
key: The section's key override, or ``None`` (a higher tier — form
|
|
56
|
+
key, then composition key — supplies it).
|
|
57
|
+
scale: The section's scale/mode override, or ``None`` (falls back
|
|
58
|
+
through the form scale to the composition scale).
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
```python
|
|
62
|
+
@composition.pattern(channel=9)
|
|
63
|
+
def drums (p):
|
|
64
|
+
# Always play a basic kick
|
|
65
|
+
p.hit_steps("kick", [0, 8])
|
|
66
|
+
|
|
67
|
+
# Only add snare and hats during the "chorus"
|
|
68
|
+
if p.section and p.section.name == "chorus":
|
|
69
|
+
p.hit_steps("snare", [4, 12])
|
|
70
|
+
|
|
71
|
+
# Use .progress (0.0 to 1.0) to build a riser
|
|
72
|
+
vel = int(60 + 40 * p.section.progress)
|
|
73
|
+
p.hit_steps("hh", list(range(16)), velocity=vel)
|
|
74
|
+
|
|
75
|
+
# Plan a lead-in on the last bar before a different section
|
|
76
|
+
if p.section and p.section.ending:
|
|
77
|
+
p.hit_steps("snare", [0, 2, 4, 6, 8, 10, 12, 14], velocity=100)
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
bar: int
|
|
83
|
+
bars: int
|
|
84
|
+
index: int
|
|
85
|
+
next_section: typing.Optional[str] = None
|
|
86
|
+
energy: float = 0.5
|
|
87
|
+
key: typing.Optional[str] = None
|
|
88
|
+
scale: typing.Optional[str] = None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def progress (self) -> float:
|
|
92
|
+
|
|
93
|
+
"""Return how far through this section we are (0.0 to ~1.0)."""
|
|
94
|
+
|
|
95
|
+
if self.bars <= 0:
|
|
96
|
+
return 0.0
|
|
97
|
+
|
|
98
|
+
return self.bar / self.bars
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def first_bar (self) -> bool:
|
|
102
|
+
|
|
103
|
+
"""Return True if this is the first bar of the section."""
|
|
104
|
+
|
|
105
|
+
return self.bar == 0
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def last_bar (self) -> bool:
|
|
109
|
+
|
|
110
|
+
"""Return True if this is the last bar of the section."""
|
|
111
|
+
|
|
112
|
+
return self.bar == self.bars - 1
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def ending (self) -> bool:
|
|
116
|
+
|
|
117
|
+
"""True on the last bar before a DIFFERENT section.
|
|
118
|
+
|
|
119
|
+
A repeat (verse → verse) is not an ending, and neither is the
|
|
120
|
+
form's end — ``ending`` marks the bars where transition material
|
|
121
|
+
(fills, mutes) belongs.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
self.last_bar
|
|
126
|
+
and self.next_section is not None
|
|
127
|
+
and self.next_section != self.name
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class FormState:
|
|
132
|
+
|
|
133
|
+
"""Track compositional form as a sequence of named sections with bar durations."""
|
|
134
|
+
|
|
135
|
+
def __init__ (
|
|
136
|
+
self,
|
|
137
|
+
sections: typing.Union[
|
|
138
|
+
"subsequence.forms.Form",
|
|
139
|
+
typing.List[typing.Any],
|
|
140
|
+
typing.Iterator[typing.Tuple[str, int]],
|
|
141
|
+
typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]]
|
|
142
|
+
],
|
|
143
|
+
loop: bool = False,
|
|
144
|
+
start: typing.Optional[str] = None,
|
|
145
|
+
rng: typing.Optional[random.Random] = None,
|
|
146
|
+
at_end: str = "stop",
|
|
147
|
+
) -> None:
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
Initialize from a Form, list, iterator, or dict of weighted section transitions.
|
|
151
|
+
|
|
152
|
+
Parameters:
|
|
153
|
+
sections: Form definition. A :class:`~subsequence.forms.Form`
|
|
154
|
+
value, a list of Sections / ``(name, bars)`` tuples, an
|
|
155
|
+
iterator yielding ``(name, bars)`` tuples, or a dictionary
|
|
156
|
+
defining a weighted directed graph for generative progression.
|
|
157
|
+
loop: Sugar for ``at_end="loop"`` (sequence mode).
|
|
158
|
+
start: Name of the starting section when using a graph dict. If omitted,
|
|
159
|
+
it defaults to the first key in the dictionary.
|
|
160
|
+
rng: Optional seeded ``random.Random`` for deterministic graph decisions.
|
|
161
|
+
at_end: What happens when a sequence/generator form runs out —
|
|
162
|
+
``"stop"`` (the form finishes; default), ``"hold"`` (the
|
|
163
|
+
final section repeats until navigated away from), or
|
|
164
|
+
``"loop"`` (start over). Graphs end via their terminal
|
|
165
|
+
sections, so a graph only accepts ``"stop"``.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
if at_end not in _AT_END_CHOICES:
|
|
169
|
+
choices = ", ".join(sorted(_AT_END_CHOICES))
|
|
170
|
+
raise ValueError(f"at_end must be one of {choices}, got {at_end!r}")
|
|
171
|
+
|
|
172
|
+
if loop:
|
|
173
|
+
if at_end not in ("stop", "loop"):
|
|
174
|
+
raise ValueError(f"loop=True conflicts with at_end={at_end!r} — pass one or the other")
|
|
175
|
+
at_end = "loop"
|
|
176
|
+
|
|
177
|
+
self._at_end: str = at_end
|
|
178
|
+
self._current: typing.Optional[subsequence.forms.Section] = None
|
|
179
|
+
self._bar_in_section: int = 0
|
|
180
|
+
self._section_index: int = 0
|
|
181
|
+
self._total_bars: int = 0
|
|
182
|
+
self._finished: bool = False
|
|
183
|
+
|
|
184
|
+
# Graph mode state (only set when sections is a dict).
|
|
185
|
+
self._graph: typing.Optional[subsequence.weighted_graph.WeightedGraph] = None
|
|
186
|
+
self._section_bars: typing.Optional[typing.Dict[str, int]] = None
|
|
187
|
+
self._rng: random.Random = rng or random.Random()
|
|
188
|
+
self._iterator: typing.Optional[typing.Iterator[typing.Tuple[str, int]]] = None
|
|
189
|
+
|
|
190
|
+
# Sequence mode state (list or Form): the whole timeline is known,
|
|
191
|
+
# which is what makes jump_to()/queue_next() navigable.
|
|
192
|
+
self._sequence: typing.Optional[typing.List[subsequence.forms.Section]] = None
|
|
193
|
+
self._position: int = 0
|
|
194
|
+
self._queued_position: typing.Optional[int] = None
|
|
195
|
+
|
|
196
|
+
# Terminal sections (graph mode only): sections with None transitions.
|
|
197
|
+
self._terminal_sections: typing.Set[str] = set()
|
|
198
|
+
|
|
199
|
+
# Next-section lookahead: pre-decided when entering a section so
|
|
200
|
+
# patterns can read p.section.next_section for lead-ins.
|
|
201
|
+
# Overridable at any time via queue_next().
|
|
202
|
+
self._next_section_name: typing.Optional[str] = None
|
|
203
|
+
|
|
204
|
+
# Iterator peek buffer (generator mode only).
|
|
205
|
+
self._peeked: typing.Optional[subsequence.forms.Section] = None
|
|
206
|
+
self._peek_exhausted: bool = False
|
|
207
|
+
|
|
208
|
+
if isinstance(sections, dict):
|
|
209
|
+
# Graph mode: build a WeightedGraph from the dict.
|
|
210
|
+
if at_end != "stop":
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"at_end={at_end!r} applies to sequence forms — a graph form ends "
|
|
213
|
+
"via its terminal sections (give a section None transitions)"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self._graph = subsequence.weighted_graph.WeightedGraph()
|
|
217
|
+
self._section_bars = {}
|
|
218
|
+
|
|
219
|
+
for name, (bars, transitions) in sections.items():
|
|
220
|
+
if bars < 1:
|
|
221
|
+
raise ValueError(f"Section '{name}' must last at least 1 bar, got {bars}")
|
|
222
|
+
|
|
223
|
+
self._section_bars[name] = bars
|
|
224
|
+
if transitions is None:
|
|
225
|
+
self._terminal_sections.add(name)
|
|
226
|
+
else:
|
|
227
|
+
for target, weight in transitions:
|
|
228
|
+
# Validate targets against the whole dict now — a typo
|
|
229
|
+
# would otherwise crash with a bare KeyError at the
|
|
230
|
+
# first section boundary DURING playback, every bar.
|
|
231
|
+
if target not in sections:
|
|
232
|
+
known = ", ".join(sorted(sections))
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Section '{name}' transitions to unknown section "
|
|
235
|
+
f"'{target}'. Known sections: {known}"
|
|
236
|
+
)
|
|
237
|
+
self._graph.add_transition(name, target, weight)
|
|
238
|
+
|
|
239
|
+
start_name = start if start is not None else next(iter(sections))
|
|
240
|
+
|
|
241
|
+
if start_name not in self._section_bars:
|
|
242
|
+
raise ValueError(f"Start section '{start_name}' not found in form definition")
|
|
243
|
+
|
|
244
|
+
self._current = subsequence.forms.Section(name = start_name, bars = self._section_bars[start_name])
|
|
245
|
+
self._pick_next()
|
|
246
|
+
|
|
247
|
+
elif isinstance(sections, (subsequence.forms.Form, list)):
|
|
248
|
+
# Sequence mode: the timeline is a known, navigable list.
|
|
249
|
+
elements = sections.sections if isinstance(sections, subsequence.forms.Form) else sections
|
|
250
|
+
self._sequence = [subsequence.forms._coerce_section(element) for element in elements]
|
|
251
|
+
|
|
252
|
+
if self._sequence:
|
|
253
|
+
self._current = self._sequence[0]
|
|
254
|
+
else:
|
|
255
|
+
self._finished = True
|
|
256
|
+
|
|
257
|
+
self._pick_next()
|
|
258
|
+
|
|
259
|
+
else:
|
|
260
|
+
# Generator/iterator mode: use directly.
|
|
261
|
+
if at_end == "loop":
|
|
262
|
+
raise ValueError(
|
|
263
|
+
'at_end="loop" cannot replay a generator form — pass a list '
|
|
264
|
+
'(or Form) if the form should cycle'
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
self._iterator = sections
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
self._current = subsequence.forms._coerce_section(next(self._iterator))
|
|
271
|
+
except StopIteration:
|
|
272
|
+
self._finished = True
|
|
273
|
+
|
|
274
|
+
self._peek_iterator()
|
|
275
|
+
|
|
276
|
+
def _pick_next (self) -> None:
|
|
277
|
+
|
|
278
|
+
"""Pre-decide the next section so patterns can read it as a lookahead.
|
|
279
|
+
|
|
280
|
+
In graph mode, calls ``choose_next()`` on the weighted graph.
|
|
281
|
+
In sequence mode, reads the timeline (respecting ``at_end``).
|
|
282
|
+
In generator mode, delegates to ``_peek_iterator()``.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
if self._graph is not None:
|
|
286
|
+
assert self._current is not None
|
|
287
|
+
current_name = self._current.name
|
|
288
|
+
|
|
289
|
+
if current_name in self._terminal_sections:
|
|
290
|
+
self._next_section_name = None
|
|
291
|
+
else:
|
|
292
|
+
self._next_section_name = self._graph.choose_next(current_name, self._rng)
|
|
293
|
+
|
|
294
|
+
elif self._sequence is not None:
|
|
295
|
+
if self._queued_position is not None:
|
|
296
|
+
self._next_section_name = self._sequence[self._queued_position].name
|
|
297
|
+
else:
|
|
298
|
+
upcoming = self._sequence_next_position()
|
|
299
|
+
self._next_section_name = self._sequence[upcoming].name if upcoming is not None else (
|
|
300
|
+
self._current.name if self._at_end == "hold" and self._current is not None else None
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
else:
|
|
304
|
+
self._peek_iterator()
|
|
305
|
+
|
|
306
|
+
def _sequence_next_position (self) -> typing.Optional[int]:
|
|
307
|
+
|
|
308
|
+
"""The natural next position in sequence mode, or None at the end.
|
|
309
|
+
|
|
310
|
+
``at_end="loop"`` wraps; ``"hold"`` and ``"stop"`` both return
|
|
311
|
+
None here — hold's repeat is decided at the boundary in
|
|
312
|
+
:meth:`advance` (the position does not move).
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
assert self._sequence is not None
|
|
316
|
+
|
|
317
|
+
# An empty form has no position to move to — even under "loop".
|
|
318
|
+
if not self._sequence:
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
following = self._position + 1
|
|
322
|
+
|
|
323
|
+
if following < len(self._sequence):
|
|
324
|
+
return following
|
|
325
|
+
|
|
326
|
+
if self._at_end == "loop":
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def _peek_iterator (self) -> None:
|
|
332
|
+
|
|
333
|
+
"""Peek the next element from the iterator into a one-element buffer."""
|
|
334
|
+
|
|
335
|
+
if self._peek_exhausted or self._iterator is None:
|
|
336
|
+
self._next_section_name = (
|
|
337
|
+
self._current.name
|
|
338
|
+
if self._at_end == "hold" and self._current is not None and self._peek_exhausted
|
|
339
|
+
else None
|
|
340
|
+
)
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
self._peeked = subsequence.forms._coerce_section(next(self._iterator))
|
|
345
|
+
self._next_section_name = self._peeked.name
|
|
346
|
+
except StopIteration:
|
|
347
|
+
self._peeked = None
|
|
348
|
+
self._peek_exhausted = True
|
|
349
|
+
self._next_section_name = (
|
|
350
|
+
self._current.name if self._at_end == "hold" and self._current is not None else None
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _find_occurrence (self, section_name: str, what: str) -> int:
|
|
354
|
+
|
|
355
|
+
"""Find the next occurrence of a name in the sequence, searching forward and wrapping."""
|
|
356
|
+
|
|
357
|
+
assert self._sequence is not None
|
|
358
|
+
|
|
359
|
+
count = len(self._sequence)
|
|
360
|
+
|
|
361
|
+
for step in range(1, count + 1):
|
|
362
|
+
candidate = (self._position + step) % count
|
|
363
|
+
if self._sequence[candidate].name == section_name:
|
|
364
|
+
return candidate
|
|
365
|
+
|
|
366
|
+
known = ", ".join(sorted({section.name for section in self._sequence}))
|
|
367
|
+
raise ValueError(
|
|
368
|
+
f"{what}: section '{section_name}' not found in form. "
|
|
369
|
+
f"Known sections: {known}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def queue_next (self, section_name: str) -> None:
|
|
373
|
+
|
|
374
|
+
"""Queue a section to play after the current one ends.
|
|
375
|
+
|
|
376
|
+
Overrides the automatically pre-decided next section. The queued
|
|
377
|
+
section takes effect at the natural section boundary — the current
|
|
378
|
+
section plays to completion first. In sequence mode the form
|
|
379
|
+
continues from the queued occurrence onward.
|
|
380
|
+
|
|
381
|
+
Queuing after the form has finished revives it: the queued section
|
|
382
|
+
starts at the next bar and the form continues from there.
|
|
383
|
+
|
|
384
|
+
Available in graph and sequence (list/Form) modes; a generator form
|
|
385
|
+
cannot be navigated.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
section_name: The section to queue.
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
ValueError: If the form is a generator, or the name is unknown.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
if self._sequence is not None:
|
|
395
|
+
self._queued_position = self._find_occurrence(section_name, "queue_next")
|
|
396
|
+
self._next_section_name = section_name
|
|
397
|
+
logger.info(f"Form: next → {section_name}")
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
if self._section_bars is None:
|
|
401
|
+
raise ValueError(
|
|
402
|
+
"queue_next() needs a navigable form (a graph dict, a list, or a Form value) — "
|
|
403
|
+
"a generator form cannot be navigated"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if section_name not in self._section_bars:
|
|
407
|
+
known = ", ".join(sorted(self._section_bars))
|
|
408
|
+
raise ValueError(
|
|
409
|
+
f"Section '{section_name}' not found in form. "
|
|
410
|
+
f"Known sections: {known}"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
self._next_section_name = section_name
|
|
414
|
+
logger.info(f"Form: next → {section_name}")
|
|
415
|
+
|
|
416
|
+
def advance (self) -> bool:
|
|
417
|
+
|
|
418
|
+
"""Advance one bar, transitioning to the next section when needed, returning True if section changed."""
|
|
419
|
+
|
|
420
|
+
if self._finished:
|
|
421
|
+
|
|
422
|
+
# A queued section revives a finished form at the next bar —
|
|
423
|
+
# queue_next() after the end is a command to play again, not a
|
|
424
|
+
# call to be silently ignored (sequence mode used to trip the
|
|
425
|
+
# not-finished invariant; graph mode ignored it).
|
|
426
|
+
if self._sequence is not None and self._queued_position is not None:
|
|
427
|
+
self._position = self._queued_position
|
|
428
|
+
self._queued_position = None
|
|
429
|
+
self._current = self._sequence[self._position]
|
|
430
|
+
elif self._graph is not None and self._next_section_name is not None:
|
|
431
|
+
assert self._section_bars is not None
|
|
432
|
+
self._current = subsequence.forms.Section(
|
|
433
|
+
name = self._next_section_name,
|
|
434
|
+
bars = self._section_bars[self._next_section_name],
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
self._finished = False
|
|
440
|
+
self._section_index += 1
|
|
441
|
+
self._bar_in_section = 0
|
|
442
|
+
self._pick_next()
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
self._bar_in_section += 1
|
|
446
|
+
self._total_bars += 1
|
|
447
|
+
|
|
448
|
+
assert self._current is not None, "Form state invariant: current should not be None when not finished"
|
|
449
|
+
|
|
450
|
+
if self._bar_in_section >= self._current.bars:
|
|
451
|
+
|
|
452
|
+
if self._graph is not None:
|
|
453
|
+
# Graph mode: consume the pre-decided (or queued) next section.
|
|
454
|
+
if self._next_section_name is None:
|
|
455
|
+
# Terminal section — form ends.
|
|
456
|
+
self._finished = True
|
|
457
|
+
self._current = None
|
|
458
|
+
return True
|
|
459
|
+
|
|
460
|
+
assert self._section_bars is not None
|
|
461
|
+
next_name = self._next_section_name
|
|
462
|
+
self._current = subsequence.forms.Section(name = next_name, bars = self._section_bars[next_name])
|
|
463
|
+
self._section_index += 1
|
|
464
|
+
self._bar_in_section = 0
|
|
465
|
+
self._pick_next()
|
|
466
|
+
return True
|
|
467
|
+
|
|
468
|
+
elif self._sequence is not None:
|
|
469
|
+
# Sequence mode: a queued jump wins; otherwise the timeline
|
|
470
|
+
# (with at_end deciding what happens past the last section).
|
|
471
|
+
if self._queued_position is not None:
|
|
472
|
+
self._position = self._queued_position
|
|
473
|
+
self._queued_position = None
|
|
474
|
+
else:
|
|
475
|
+
following = self._sequence_next_position()
|
|
476
|
+
|
|
477
|
+
if following is None:
|
|
478
|
+
if self._at_end == "hold":
|
|
479
|
+
# The final section repeats (a re-entry: the index
|
|
480
|
+
# bumps so bound material restarts correctly).
|
|
481
|
+
self._section_index += 1
|
|
482
|
+
self._bar_in_section = 0
|
|
483
|
+
self._pick_next()
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
self._finished = True
|
|
487
|
+
self._current = None
|
|
488
|
+
return True
|
|
489
|
+
|
|
490
|
+
self._position = following
|
|
491
|
+
|
|
492
|
+
self._current = self._sequence[self._position]
|
|
493
|
+
self._section_index += 1
|
|
494
|
+
self._bar_in_section = 0
|
|
495
|
+
self._pick_next()
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
else:
|
|
499
|
+
# Generator mode: consume from the peek buffer.
|
|
500
|
+
if self._peeked is not None:
|
|
501
|
+
self._current = self._peeked
|
|
502
|
+
self._peeked = None
|
|
503
|
+
self._section_index += 1
|
|
504
|
+
self._bar_in_section = 0
|
|
505
|
+
self._peek_iterator()
|
|
506
|
+
return True
|
|
507
|
+
elif self._at_end == "hold":
|
|
508
|
+
self._section_index += 1
|
|
509
|
+
self._bar_in_section = 0
|
|
510
|
+
return True
|
|
511
|
+
else:
|
|
512
|
+
self._finished = True
|
|
513
|
+
self._current = None
|
|
514
|
+
return True
|
|
515
|
+
|
|
516
|
+
return False
|
|
517
|
+
|
|
518
|
+
def get_section_info (self) -> typing.Optional[SectionInfo]:
|
|
519
|
+
|
|
520
|
+
"""Return current section info, or None if the form is exhausted."""
|
|
521
|
+
|
|
522
|
+
if self._finished or self._current is None:
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
return SectionInfo(
|
|
526
|
+
name = self._current.name,
|
|
527
|
+
bar = self._bar_in_section,
|
|
528
|
+
bars = self._current.bars,
|
|
529
|
+
index = self._section_index,
|
|
530
|
+
next_section = self._next_section_name,
|
|
531
|
+
energy = self._current.energy,
|
|
532
|
+
key = self._current.key,
|
|
533
|
+
scale = self._current.scale,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def section_info_at_bar (self, bar: int) -> typing.Optional[SectionInfo]:
|
|
537
|
+
|
|
538
|
+
"""Return the section covering a 1-based GLOBAL bar, or ``None``.
|
|
539
|
+
|
|
540
|
+
Available for **sequence** forms only (lists and ``Form`` values — the
|
|
541
|
+
whole timeline is known, so a bar maps to a section by accumulating
|
|
542
|
+
``Section.bars``). Graph and generator forms have no fixed layout
|
|
543
|
+
ahead of the playhead, so they return ``None`` (callers fall back to
|
|
544
|
+
the playhead section). A looping form wraps; a finite form past its
|
|
545
|
+
end returns ``None``.
|
|
546
|
+
|
|
547
|
+
Used to key a relative ``pin_chord`` to the section that *owns* the
|
|
548
|
+
pinned bar rather than the section at the playhead — they differ when
|
|
549
|
+
the harmonic clock's lookahead projects a pin into a later, possibly
|
|
550
|
+
differently-keyed, section. This is a layout lookup (linear from bar
|
|
551
|
+
1); live ``form_jump`` is not reflected, which is acceptable for
|
|
552
|
+
pre-set pins (the playhead path stays authoritative for live moves).
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
if self._sequence is None or bar < 1:
|
|
556
|
+
return None
|
|
557
|
+
|
|
558
|
+
total = sum(section.bars for section in self._sequence)
|
|
559
|
+
|
|
560
|
+
if total <= 0:
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
index0 = bar - 1
|
|
564
|
+
|
|
565
|
+
if index0 >= total:
|
|
566
|
+
if self._at_end == "loop":
|
|
567
|
+
index0 = index0 % total
|
|
568
|
+
else:
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
cursor = 0
|
|
572
|
+
|
|
573
|
+
for position, section in enumerate(self._sequence):
|
|
574
|
+
if cursor <= index0 < cursor + section.bars:
|
|
575
|
+
# Mirror the playhead path (_sequence_next_position/_pick_next)
|
|
576
|
+
# at the end of the timeline: a looping form's last section
|
|
577
|
+
# leads back to the first, a holding form's repeats itself —
|
|
578
|
+
# so .ending and next_section agree between the two sources.
|
|
579
|
+
if position + 1 < len(self._sequence):
|
|
580
|
+
following: typing.Optional[str] = self._sequence[position + 1].name
|
|
581
|
+
elif self._at_end == "loop":
|
|
582
|
+
following = self._sequence[0].name
|
|
583
|
+
elif self._at_end == "hold":
|
|
584
|
+
following = section.name
|
|
585
|
+
else:
|
|
586
|
+
following = None
|
|
587
|
+
|
|
588
|
+
return SectionInfo(
|
|
589
|
+
name = section.name,
|
|
590
|
+
bar = index0 - cursor,
|
|
591
|
+
bars = section.bars,
|
|
592
|
+
index = position,
|
|
593
|
+
next_section = following,
|
|
594
|
+
energy = section.energy,
|
|
595
|
+
key = section.key,
|
|
596
|
+
scale = section.scale,
|
|
597
|
+
)
|
|
598
|
+
cursor += section.bars
|
|
599
|
+
|
|
600
|
+
return None
|
|
601
|
+
|
|
602
|
+
@property
|
|
603
|
+
def total_bars (self) -> int:
|
|
604
|
+
|
|
605
|
+
"""Return the global bar count since the form started."""
|
|
606
|
+
|
|
607
|
+
return self._total_bars
|
|
608
|
+
|
|
609
|
+
def jump_to (self, section_name: str) -> None:
|
|
610
|
+
|
|
611
|
+
"""Force the form to a named section immediately.
|
|
612
|
+
|
|
613
|
+
Available in **graph mode** (dict forms) and **sequence mode**
|
|
614
|
+
(list/Form forms — the jump lands on the next occurrence of the
|
|
615
|
+
name, searching forward and wrapping, and the form continues from
|
|
616
|
+
there). A generator form cannot be navigated.
|
|
617
|
+
|
|
618
|
+
The section restarts from bar 0. The musical effect is not heard
|
|
619
|
+
until the *next pattern rebuild cycle*, because already-queued MIDI
|
|
620
|
+
notes are unaffected. This is the same natural quantization that
|
|
621
|
+
applies to all ``composition.data`` writes and
|
|
622
|
+
``composition.tweak()`` calls.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
section_name: Name of the section to jump to. Must exist in the
|
|
626
|
+
form definition passed to ``composition.form()``.
|
|
627
|
+
|
|
628
|
+
Raises:
|
|
629
|
+
ValueError: If the form is a generator, or the name is unknown.
|
|
630
|
+
|
|
631
|
+
Example::
|
|
632
|
+
|
|
633
|
+
composition.form_jump("chorus") # via Composition helper
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
if self._sequence is not None:
|
|
637
|
+
self._position = self._find_occurrence(section_name, "jump_to")
|
|
638
|
+
self._current = self._sequence[self._position]
|
|
639
|
+
self._bar_in_section = 0
|
|
640
|
+
self._section_index += 1
|
|
641
|
+
self._finished = False
|
|
642
|
+
self._queued_position = None
|
|
643
|
+
self._pick_next()
|
|
644
|
+
logger.info(f"Form: jump → {section_name}")
|
|
645
|
+
return
|
|
646
|
+
|
|
647
|
+
if self._section_bars is None:
|
|
648
|
+
raise ValueError(
|
|
649
|
+
"jump_to() needs a navigable form (a graph dict, a list, or a Form value) — "
|
|
650
|
+
"a generator form cannot be navigated"
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if section_name not in self._section_bars:
|
|
654
|
+
known = ", ".join(sorted(self._section_bars))
|
|
655
|
+
raise ValueError(
|
|
656
|
+
f"Section '{section_name}' not found in form. "
|
|
657
|
+
f"Known sections: {known}"
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
self._current = subsequence.forms.Section(name = section_name, bars = self._section_bars[section_name])
|
|
661
|
+
self._bar_in_section = 0
|
|
662
|
+
self._section_index += 1
|
|
663
|
+
self._finished = False
|
|
664
|
+
self._pick_next()
|
|
665
|
+
logger.info(f"Form: jump → {section_name}")
|