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,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project definitions loader — a shared name-to-number vocabulary file.
|
|
3
|
+
|
|
4
|
+
A music project can keep one small YAML file (any filename — ``project.yaml``,
|
|
5
|
+
``kit.yaml``) mapping human names to MIDI numbers, shared between Subsequence
|
|
6
|
+
and the Subsample sampler. Both tools read the same file, so renumbering a
|
|
7
|
+
sound or a controller is one edit and both follow on their next reload. The
|
|
8
|
+
file format is the only contract — neither application depends on the other.
|
|
9
|
+
|
|
10
|
+
The file is a flat mapping of sections, each mapping names to whole numbers:
|
|
11
|
+
|
|
12
|
+
notes:
|
|
13
|
+
ride_edge_soft: 53
|
|
14
|
+
dawn_chorus_pheasant: 60
|
|
15
|
+
cc:
|
|
16
|
+
sampler_release: 21
|
|
17
|
+
channels:
|
|
18
|
+
kit: 10
|
|
19
|
+
birds: 3
|
|
20
|
+
programs:
|
|
21
|
+
brushes: 1
|
|
22
|
+
nrpn:
|
|
23
|
+
filter_env_amount: 1042
|
|
24
|
+
|
|
25
|
+
Sections and value ranges (inclusive):
|
|
26
|
+
|
|
27
|
+
- ``notes`` — MIDI note numbers, 0-127.
|
|
28
|
+
- ``cc`` — controller numbers, 0-127.
|
|
29
|
+
- ``channels`` — MIDI channels as musicians count them, 1-16.
|
|
30
|
+
- ``programs`` — program-change numbers as sent on the wire (0-based), 0-127.
|
|
31
|
+
- ``nrpn`` — 14-bit NRPN parameter numbers, 0-16383. Subsequence-specific;
|
|
32
|
+
Subsample ignores this section.
|
|
33
|
+
|
|
34
|
+
Names must match ``[a-z][a-z0-9_]*`` — lowercase letters, digits, underscores;
|
|
35
|
+
no dots, no leading digit. Unknown top-level sections are silently ignored, so
|
|
36
|
+
either tool can grow a new section without breaking the other. An empty file,
|
|
37
|
+
an absent section, and a null section are all valid. Every failure raises
|
|
38
|
+
``ValueError`` naming the file, section, and offending entry — the same checks,
|
|
39
|
+
in the same order, as Subsample applies, so a bad file fails the same way in
|
|
40
|
+
both tools.
|
|
41
|
+
|
|
42
|
+
Caveats worth knowing:
|
|
43
|
+
|
|
44
|
+
- ``channels`` values are always user-facing 1-16 (the cross-tool contract).
|
|
45
|
+
They pass straight into ``channel=`` under the default numbering; if your
|
|
46
|
+
composition sets ``zero_indexed_channels=True``, subtract 1 yourself.
|
|
47
|
+
- YAML silently collapses duplicate keys — the last duplicate wins, and
|
|
48
|
+
neither tool can detect it.
|
|
49
|
+
- Names resolve exactly as written (lowercase) in ``drum_note_map`` /
|
|
50
|
+
``cc_name_map`` lookups. Merging over a stock map, the later dict wins:
|
|
51
|
+
``{**gm_drums.GM_DRUM_MAP, **defs.notes}`` lets your names shadow GM ones.
|
|
52
|
+
- A ``subsequence.load_definitions(...)`` call at the top of a watched file
|
|
53
|
+
re-runs on every live reload, so the definitions re-read naturally.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
```python
|
|
57
|
+
import subsequence
|
|
58
|
+
|
|
59
|
+
defs = subsequence.load_definitions("project.yaml")
|
|
60
|
+
comp = subsequence.Composition(bpm=100)
|
|
61
|
+
|
|
62
|
+
@comp.pattern(channel=defs.channels["birds"], drum_note_map=defs.notes, cc_name_map=defs.cc)
|
|
63
|
+
def birds (b):
|
|
64
|
+
b.hit("dawn_chorus_pheasant", beats=[0, 2.5])
|
|
65
|
+
b.cc("sampler_release", 64)
|
|
66
|
+
```
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
import dataclasses
|
|
70
|
+
import pathlib
|
|
71
|
+
import re
|
|
72
|
+
import typing
|
|
73
|
+
|
|
74
|
+
import yaml
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
CONSUMED_SECTIONS: typing.FrozenSet[str] = frozenset({
|
|
78
|
+
"notes", "cc", "channels", "programs", "nrpn",
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
_SECTION_RANGES: typing.Dict[str, typing.Tuple[int, int]] = {
|
|
82
|
+
"notes": (0, 127),
|
|
83
|
+
"cc": (0, 127),
|
|
84
|
+
"channels": (1, 16),
|
|
85
|
+
"programs": (0, 127),
|
|
86
|
+
"nrpn": (0, 16383),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_NAME_RE = re.compile(r"[a-z][a-z0-9_]*")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclasses.dataclass(frozen=True)
|
|
93
|
+
class Definitions:
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
The name-to-number tables read from a project definitions file.
|
|
97
|
+
|
|
98
|
+
One plain ``dict`` per section, always present — an absent or null section
|
|
99
|
+
is an empty dict. The dicts merge directly into the existing parameters:
|
|
100
|
+
``notes`` into ``drum_note_map=``, ``cc`` into ``cc_name_map=``, ``nrpn``
|
|
101
|
+
into ``nrpn_name_map=``, while ``channels`` values feed ``channel=`` and
|
|
102
|
+
``programs`` values feed ``p.program_change()``.
|
|
103
|
+
|
|
104
|
+
The dataclass is frozen (attributes cannot be reassigned) but the dicts
|
|
105
|
+
themselves are ordinary mutable dicts, so they can be merged and extended
|
|
106
|
+
freely.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
```python
|
|
110
|
+
defs = subsequence.load_definitions("project.yaml")
|
|
111
|
+
defs.channels["birds"] # 3
|
|
112
|
+
defs.notes # {"ride_edge_soft": 53, ...}
|
|
113
|
+
```
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
notes: typing.Dict[str, int] = dataclasses.field(default_factory=dict)
|
|
117
|
+
cc: typing.Dict[str, int] = dataclasses.field(default_factory=dict)
|
|
118
|
+
channels: typing.Dict[str, int] = dataclasses.field(default_factory=dict)
|
|
119
|
+
programs: typing.Dict[str, int] = dataclasses.field(default_factory=dict)
|
|
120
|
+
nrpn: typing.Dict[str, int] = dataclasses.field(default_factory=dict)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def load_definitions (path: typing.Union[str, pathlib.Path]) -> Definitions:
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
Load and validate a project definitions file.
|
|
127
|
+
|
|
128
|
+
Reads the YAML file at ``path`` and returns a :class:`Definitions` whose
|
|
129
|
+
``notes`` / ``cc`` / ``channels`` / ``programs`` / ``nrpn`` dicts merge
|
|
130
|
+
straight into pattern parameters. See the module docstring for the file
|
|
131
|
+
format, the value ranges, and the shared-vocabulary contract with the
|
|
132
|
+
Subsample sampler.
|
|
133
|
+
|
|
134
|
+
Validation is strict inside the sections listed above and lenient outside
|
|
135
|
+
them: unknown top-level sections are ignored, while a bad name, a non-whole
|
|
136
|
+
number (including YAML ``true``/``false``), or an out-of-range value is
|
|
137
|
+
rejected with an error naming the file, section, and entry.
|
|
138
|
+
|
|
139
|
+
Parameters:
|
|
140
|
+
path: The definitions file, as a path string or ``pathlib.Path``.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
A :class:`Definitions` with one name-to-number dict per section.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If the file is missing, unreadable, or not valid YAML; if
|
|
147
|
+
the top level or a consumed section is not a mapping; or if a name
|
|
148
|
+
or value inside a consumed section is invalid. File-system and
|
|
149
|
+
YAML errors are wrapped so this is the only error type raised.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
```python
|
|
153
|
+
defs = subsequence.load_definitions("project.yaml")
|
|
154
|
+
|
|
155
|
+
@comp.pattern(channel=defs.channels["kit"], drum_note_map=defs.notes)
|
|
156
|
+
def kit (p):
|
|
157
|
+
p.hit("ride_edge_soft", beats=[1, 3])
|
|
158
|
+
```
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
p = pathlib.Path(path)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
with p.open(encoding="utf-8") as fh:
|
|
165
|
+
raw = yaml.safe_load(fh)
|
|
166
|
+
except (OSError, yaml.YAMLError) as exc:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"definitions file {p} could not be read: {exc}"
|
|
169
|
+
) from exc
|
|
170
|
+
|
|
171
|
+
if raw is None:
|
|
172
|
+
return Definitions()
|
|
173
|
+
|
|
174
|
+
if not isinstance(raw, dict):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"definitions file {p}: top level must be a mapping of "
|
|
177
|
+
f"sections (notes:, cc:, …), got {type(raw).__name__}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
tables: typing.Dict[str, typing.Dict[str, int]] = {}
|
|
181
|
+
|
|
182
|
+
for section in sorted(CONSUMED_SECTIONS):
|
|
183
|
+
section_raw = raw.get(section)
|
|
184
|
+
|
|
185
|
+
if section_raw is None:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
if not isinstance(section_raw, dict):
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"definitions file {p}: section {section!r} must be a "
|
|
191
|
+
f"mapping of name to number "
|
|
192
|
+
f"(got {type(section_raw).__name__})"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
lo, hi = _SECTION_RANGES[section]
|
|
196
|
+
table: typing.Dict[str, int] = {}
|
|
197
|
+
|
|
198
|
+
for name_raw, value in section_raw.items():
|
|
199
|
+
name = str(name_raw)
|
|
200
|
+
|
|
201
|
+
if not _NAME_RE.fullmatch(name):
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"definitions file {p}: section {section!r}: name "
|
|
204
|
+
f"{name!r} must match [a-z][a-z0-9_]* (lowercase "
|
|
205
|
+
f"letters, digits, underscores — no dots)"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# bool is an int subclass — reject it first so ``x: true`` fails
|
|
209
|
+
# loudly instead of quietly becoming 1.
|
|
210
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"definitions file {p}: section {section!r}: "
|
|
213
|
+
f"{name!r} must be a whole number (got {value!r})"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not lo <= value <= hi:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"definitions file {p}: section {section!r}: "
|
|
219
|
+
f"{name!r} = {value} is outside [{lo}, {hi}]"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
table[name] = value
|
|
223
|
+
|
|
224
|
+
tables[section] = table
|
|
225
|
+
|
|
226
|
+
return Definitions(
|
|
227
|
+
notes = tables.get("notes", {}),
|
|
228
|
+
cc = tables.get("cc", {}),
|
|
229
|
+
channels = tables.get("channels", {}),
|
|
230
|
+
programs = tables.get("programs", {}),
|
|
231
|
+
nrpn = tables.get("nrpn", {}),
|
|
232
|
+
)
|