subsequence 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. subsequence/__init__.py +231 -0
  2. subsequence/__main__.py +24 -0
  3. subsequence/assets/web/index.html +345 -0
  4. subsequence/cadences.py +113 -0
  5. subsequence/chord_graphs/__init__.py +100 -0
  6. subsequence/chord_graphs/aeolian_minor.py +158 -0
  7. subsequence/chord_graphs/chromatic_mediant.py +113 -0
  8. subsequence/chord_graphs/diminished.py +97 -0
  9. subsequence/chord_graphs/dorian_minor.py +127 -0
  10. subsequence/chord_graphs/functional_major.py +102 -0
  11. subsequence/chord_graphs/hooktheory_major.py +88 -0
  12. subsequence/chord_graphs/lydian_major.py +130 -0
  13. subsequence/chord_graphs/mixolydian.py +98 -0
  14. subsequence/chord_graphs/phrygian_minor.py +76 -0
  15. subsequence/chord_graphs/suspended.py +109 -0
  16. subsequence/chord_graphs/turnaround_global.py +157 -0
  17. subsequence/chord_graphs/whole_tone.py +77 -0
  18. subsequence/chords.py +419 -0
  19. subsequence/composition.py +6099 -0
  20. subsequence/conductor.py +238 -0
  21. subsequence/constants/__init__.py +24 -0
  22. subsequence/constants/durations.py +37 -0
  23. subsequence/constants/instruments/__init__.py +13 -0
  24. subsequence/constants/instruments/gm_cc.py +46 -0
  25. subsequence/constants/instruments/gm_drums.py +53 -0
  26. subsequence/constants/instruments/gm_instruments.py +32 -0
  27. subsequence/constants/instruments/roland_tr8s.py +320 -0
  28. subsequence/constants/instruments/vermona_drm1_drums.py +87 -0
  29. subsequence/constants/midi_notes.py +29 -0
  30. subsequence/constants/pulses.py +17 -0
  31. subsequence/constants/velocity.py +22 -0
  32. subsequence/definitions.py +232 -0
  33. subsequence/display.py +617 -0
  34. subsequence/easing.py +347 -0
  35. subsequence/event_emitter.py +109 -0
  36. subsequence/form_state.py +665 -0
  37. subsequence/forms.py +257 -0
  38. subsequence/groove.py +323 -0
  39. subsequence/harmonic_rhythm.py +83 -0
  40. subsequence/harmonic_state.py +352 -0
  41. subsequence/harmony.py +197 -0
  42. subsequence/held_notes.py +91 -0
  43. subsequence/helpers/__init__.py +0 -0
  44. subsequence/helpers/network.py +58 -0
  45. subsequence/helpers/wing.py +430 -0
  46. subsequence/intervals.py +436 -0
  47. subsequence/keystroke.py +249 -0
  48. subsequence/link_clock.py +128 -0
  49. subsequence/live_client.py +187 -0
  50. subsequence/live_reloader.py +298 -0
  51. subsequence/live_server.py +161 -0
  52. subsequence/melodic_state.py +483 -0
  53. subsequence/midi.py +97 -0
  54. subsequence/midi_utils.py +329 -0
  55. subsequence/mini_notation.py +164 -0
  56. subsequence/motifs.py +2356 -0
  57. subsequence/osc.py +194 -0
  58. subsequence/pattern.py +363 -0
  59. subsequence/pattern_algorithmic.py +2010 -0
  60. subsequence/pattern_builder.py +2589 -0
  61. subsequence/pattern_midi.py +1208 -0
  62. subsequence/progressions.py +1913 -0
  63. subsequence/py.typed +0 -0
  64. subsequence/roles.py +63 -0
  65. subsequence/sequence_utils.py +3123 -0
  66. subsequence/sequencer.py +2086 -0
  67. subsequence/tuning.py +453 -0
  68. subsequence/voicings.py +151 -0
  69. subsequence/web_ui.py +337 -0
  70. subsequence/weighted_graph.py +156 -0
  71. subsequence-0.6.4.dist-info/METADATA +208 -0
  72. subsequence-0.6.4.dist-info/RECORD +78 -0
  73. subsequence-0.6.4.dist-info/WHEEL +5 -0
  74. subsequence-0.6.4.dist-info/entry_points.txt +2 -0
  75. subsequence-0.6.4.dist-info/licenses/LICENSE +661 -0
  76. subsequence-0.6.4.dist-info/scm_file_list.json +193 -0
  77. subsequence-0.6.4.dist-info/scm_version.json +8 -0
  78. subsequence-0.6.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ )