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
subsequence/chords.py ADDED
@@ -0,0 +1,419 @@
1
+ """Chord definitions and pitch class utilities.
2
+
3
+ This module provides chord quality definitions, pitch class mappings, and the ``Chord`` class
4
+ for representing and manipulating chords.
5
+
6
+ Module-level constants:
7
+
8
+ - ``NOTE_NAME_TO_PC``: Maps note names (e.g., ``"C"``, ``"F#"``, ``"Bb"``) to pitch classes (0-11)
9
+ - ``PC_TO_NOTE_NAME``: Maps pitch classes to note names
10
+ - ``CHORD_INTERVALS``: Maps chord quality names to interval lists (semitones from root)
11
+ - ``CHORD_SUFFIX``: Maps chord quality names to human-readable suffixes (e.g., ``"m"``, ``"7"``)
12
+
13
+ Module-level helpers:
14
+
15
+ - ``key_name_to_pc(key_name)``: Validate a key name and return its pitch class (0–11).
16
+ Raises ``ValueError`` for unknown names. This is the canonical key validation function
17
+ used by ``harmony.py``, ``pattern_builder.snap_to_scale()``, and ``chord_graphs.validate_key_name()``.
18
+
19
+ Chord qualities: ``"major"``, ``"minor"``, ``"diminished"``, ``"augmented"``, ``"dominant_7th"``,
20
+ ``"major_7th"``, ``"minor_7th"``, ``"half_diminished_7th"``, ``"sus2"``, ``"sus4"``
21
+ """
22
+
23
+ import dataclasses
24
+ import typing
25
+
26
+ import subsequence.voicings
27
+
28
+
29
+ NOTE_NAME_TO_PC: typing.Dict[str, int] = {
30
+ "C": 0,
31
+ "C#": 1,
32
+ "Db": 1,
33
+ "D": 2,
34
+ "D#": 3,
35
+ "Eb": 3,
36
+ "E": 4,
37
+ "F": 5,
38
+ "F#": 6,
39
+ "Gb": 6,
40
+ "G": 7,
41
+ "G#": 8,
42
+ "Ab": 8,
43
+ "A": 9,
44
+ "A#": 10,
45
+ "Bb": 10,
46
+ "B": 11,
47
+ }
48
+
49
+ PC_TO_NOTE_NAME: typing.List[str] = [
50
+ "C",
51
+ "C#",
52
+ "D",
53
+ "D#",
54
+ "E",
55
+ "F",
56
+ "F#",
57
+ "G",
58
+ "G#",
59
+ "A",
60
+ "A#",
61
+ "B",
62
+ ]
63
+
64
+
65
+ def key_name_to_pc (key_name: str) -> int:
66
+
67
+ """Validate a key name and return its pitch class (0–11).
68
+
69
+ Parameters:
70
+ key_name: Note name (e.g. ``"C"``, ``"F#"``, ``"Bb"``).
71
+
72
+ Returns:
73
+ Pitch class integer (0–11).
74
+
75
+ Raises:
76
+ ValueError: If the key name is not recognised.
77
+
78
+ Example:
79
+ ```python
80
+ key_name_to_pc("C") # → 0
81
+ key_name_to_pc("F#") # → 6
82
+ key_name_to_pc("Bb") # → 10
83
+ ```
84
+ """
85
+
86
+ if key_name not in NOTE_NAME_TO_PC:
87
+ raise ValueError(
88
+ f"Unknown key name: {key_name!r}. Expected e.g. 'C', 'F#', 'Bb'."
89
+ )
90
+
91
+ return NOTE_NAME_TO_PC[key_name]
92
+
93
+
94
+ CHORD_INTERVALS: typing.Dict[str, typing.List[int]] = {
95
+ "major": [0, 4, 7],
96
+ "minor": [0, 3, 7],
97
+ "diminished": [0, 3, 6],
98
+ "augmented": [0, 4, 8],
99
+ "dominant_7th": [0, 4, 7, 10],
100
+ "major_7th": [0, 4, 7, 11],
101
+ "minor_7th": [0, 3, 7, 10],
102
+ "half_diminished_7th": [0, 3, 6, 10],
103
+ "diminished_7th": [0, 3, 6, 9],
104
+ "sus2": [0, 2, 7],
105
+ "sus4": [0, 5, 7],
106
+ }
107
+
108
+ CHORD_SUFFIX: typing.Dict[str, str] = {
109
+ "major": "",
110
+ "minor": "m",
111
+ "diminished": "dim",
112
+ "augmented": "+",
113
+ "dominant_7th": "7",
114
+ "major_7th": "maj7",
115
+ "minor_7th": "m7",
116
+ "half_diminished_7th": "m7b5",
117
+ "diminished_7th": "dim7",
118
+ "sus2": "sus2",
119
+ "sus4": "sus4",
120
+ }
121
+
122
+
123
+ @dataclasses.dataclass(frozen=True)
124
+ class Chord:
125
+
126
+ """
127
+ Represents a chord as a root pitch class and quality.
128
+ """
129
+
130
+ root_pc: int
131
+ quality: str
132
+
133
+
134
+ def intervals (self) -> typing.List[int]:
135
+
136
+ """
137
+ Return the chord intervals for this chord quality.
138
+ """
139
+
140
+ if self.quality not in CHORD_INTERVALS:
141
+ raise ValueError(f"Unknown chord quality: {self.quality}")
142
+
143
+ return CHORD_INTERVALS[self.quality]
144
+
145
+
146
+
147
+ def tones (self, root: int, inversion: int = 0, count: typing.Optional[int] = None) -> typing.List[int]:
148
+
149
+ """Return MIDI note numbers for chord tones starting from a root.
150
+
151
+ Finds the MIDI note corresponding to the chord's root pitch class that is
152
+ closest to the provided ``root`` argument.
153
+
154
+ Parameters:
155
+ root: MIDI note number (e.g., 60 = middle C) to center the chord around.
156
+ inversion: Chord inversion (0 = root position, 1 = first, 2 = second, ...).
157
+ Wraps around for values >= number of notes.
158
+ count: Number of notes to return. When set, the chord intervals cycle
159
+ into higher octaves until ``count`` notes are produced. When ``None``
160
+ (default), returns the natural chord tones.
161
+
162
+ Returns:
163
+ List of MIDI note numbers for chord tones
164
+
165
+ Example:
166
+ ```python
167
+ chord = Chord(root_pc=0, quality="major") # C major
168
+ chord.tones(root=60) # [60, 64, 67] - root position around C4
169
+ chord.tones(root=62) # [60, 64, 67] - still finds C4 as closest root
170
+ chord.tones(root=70) # [72, 76, 79] - finds C5 as closest root
171
+ ```
172
+ """
173
+
174
+ # Find the MIDI note for self.root_pc that is closest to the requested root.
175
+ # This handles octaves automatically.
176
+ offset = (self.root_pc - root) % 12
177
+ if offset > 6:
178
+ offset -= 12
179
+
180
+ effective_root = root + offset
181
+
182
+ intervals = self.intervals()
183
+
184
+ if inversion != 0:
185
+ intervals = subsequence.voicings.invert_chord(intervals, inversion)
186
+
187
+ if count is not None:
188
+ n = len(intervals)
189
+ return [effective_root + intervals[i % n] + 12 * (i // n) for i in range(count)]
190
+
191
+ return [effective_root + interval for interval in intervals]
192
+
193
+
194
+ def root_note (self, root_midi: int) -> int:
195
+
196
+ """
197
+ Return the MIDI note number for the chord root nearest to *root_midi*.
198
+
199
+ This is equivalent to ``self.tones(root_midi)[0]`` but makes intent
200
+ explicit when you only need the single root pitch.
201
+
202
+ Parameters:
203
+ root_midi: Reference MIDI note number used to find the closest octave
204
+ of this chord's root pitch class.
205
+
206
+ Returns:
207
+ MIDI note number of the chord root.
208
+
209
+ Example:
210
+ ```python
211
+ chord = Chord(root_pc=4, quality="major") # E major
212
+ chord.root_note(60) # → 64 (E4, nearest to C4)
213
+ chord.root_note(69) # → 64 (E4, nearest to A4)
214
+ ```
215
+ """
216
+
217
+ return self.tones(root_midi)[0]
218
+
219
+
220
+ def bass_note (self, root_midi: int, octave_offset: int = -1) -> int:
221
+
222
+ """
223
+ Return the chord root shifted by a number of octaves.
224
+
225
+ Commonly used to produce a bass register note one or two octaves
226
+ below the chord voicing.
227
+
228
+ Parameters:
229
+ root_midi: Reference MIDI note number (passed to :meth:`root_note`).
230
+ octave_offset: Octaves to shift; negative moves down (default ``-1``).
231
+
232
+ Returns:
233
+ MIDI note number of the chord root in the target register.
234
+
235
+ Example:
236
+ ```python
237
+ chord = Chord(root_pc=4, quality="major") # E major
238
+ chord.bass_note(64) # → 52 (E3, one octave down from E4)
239
+ chord.bass_note(64, -2) # → 40 (E2, two octaves down)
240
+ ```
241
+ """
242
+
243
+ return self.root_note(root_midi) + (12 * octave_offset)
244
+
245
+
246
+ def name (self) -> str:
247
+
248
+ """
249
+ Return a human-friendly chord name.
250
+
251
+ A registered quality without a suffix prints as ``root(quality)``
252
+ (e.g. ``"C(quartal)"``) rather than masquerading as a plain major.
253
+ """
254
+
255
+ root_name = PC_TO_NOTE_NAME[self.root_pc % 12]
256
+
257
+ if self.quality not in CHORD_SUFFIX:
258
+ return f"{root_name}({self.quality})"
259
+
260
+ return f"{root_name}{CHORD_SUFFIX[self.quality]}"
261
+
262
+
263
+ # Quality suffixes accepted by parse_chord(), including common alternates. The
264
+ # canonical suffixes (the values of CHORD_SUFFIX) all round-trip, so a chord's
265
+ # own name() always re-parses to the same chord.
266
+ _SUFFIX_TO_QUALITY: typing.Dict[str, str] = {
267
+ "": "major",
268
+ "maj": "major",
269
+ "M": "major",
270
+ "m": "minor",
271
+ "min": "minor",
272
+ "-": "minor",
273
+ "dim": "diminished",
274
+ "o": "diminished",
275
+ "°": "diminished",
276
+ "aug": "augmented",
277
+ "+": "augmented",
278
+ "7": "dominant_7th",
279
+ "dom7": "dominant_7th",
280
+ "maj7": "major_7th",
281
+ "M7": "major_7th",
282
+ "m7": "minor_7th",
283
+ "min7": "minor_7th",
284
+ "-7": "minor_7th",
285
+ "m7b5": "half_diminished_7th",
286
+ "ø": "half_diminished_7th",
287
+ "ø7": "half_diminished_7th",
288
+ "halfdim": "half_diminished_7th",
289
+ "dim7": "diminished_7th",
290
+ "o7": "diminished_7th",
291
+ "°7": "diminished_7th",
292
+ "sus2": "sus2",
293
+ "sus4": "sus4",
294
+ "sus": "sus4",
295
+ }
296
+
297
+ # Snapshots of the shipped tables, taken before any register_chord_quality()
298
+ # call: built-in qualities and suffixes can never be overwritten.
299
+ _BUILTIN_QUALITY_NAMES: typing.FrozenSet[str] = frozenset(CHORD_INTERVALS)
300
+ _BUILTIN_SUFFIXES: typing.FrozenSet[str] = frozenset(_SUFFIX_TO_QUALITY)
301
+
302
+
303
+ def register_chord_quality (
304
+ name: str,
305
+ intervals: typing.List[int],
306
+ suffix: typing.Optional[str] = None,
307
+ ) -> None:
308
+
309
+ """Register a custom chord quality for use everywhere chords are used.
310
+
311
+ The counterpart to :func:`subsequence.intervals.register_scale` — it opens
312
+ the quality table so quartal stacks, clusters, and extended chords become
313
+ first-class symbolic chords: they work in progressions, graphs, voice
314
+ leading, and ``describe()`` output.
315
+
316
+ Built-in qualities (e.g. ``"minor"``) cannot be overwritten. Custom names
317
+ may be re-registered freely — live reload re-runs registration on every
318
+ save, so this must not raise.
319
+
320
+ Parameters:
321
+ name: Quality name (used as ``Chord(root_pc, quality=name)``).
322
+ intervals: Semitone offsets from the root (e.g. ``[0, 5, 10]`` for a
323
+ quartal stack, ``[0, 3, 7, 10, 14]`` for a minor 9th). Must start
324
+ with 0, ascend strictly, and stay within 0–24 (extensions reach
325
+ past the octave).
326
+ suffix: Optional chord-name suffix. When given, ``parse_chord()``
327
+ accepts ``"A" + suffix`` and ``Chord.name()`` prints it — so
328
+ ``register_chord_quality("minor_9th", [0, 3, 7, 10, 14], suffix="m9")``
329
+ makes ``"Am9"`` parse from then on. Must not collide with a
330
+ built-in suffix.
331
+
332
+ Example:
333
+ ```python
334
+ import subsequence
335
+
336
+ subsequence.register_chord_quality("quartal", [0, 5, 10], suffix="q4")
337
+ subsequence.parse_chord("Dq4") # → Chord(root_pc=2, quality="quartal")
338
+ ```
339
+ """
340
+
341
+ if name in _BUILTIN_QUALITY_NAMES:
342
+ raise ValueError(
343
+ f"Cannot overwrite built-in chord quality '{name}'. "
344
+ "Choose a different name for your custom quality."
345
+ )
346
+
347
+ if not intervals:
348
+ raise ValueError("intervals must not be empty")
349
+ if not all(isinstance(i, int) and not isinstance(i, bool) for i in intervals):
350
+ raise ValueError("intervals must be whole numbers (semitone offsets)")
351
+ if intervals[0] != 0:
352
+ raise ValueError("intervals must start with 0 (the root)")
353
+ if any(b <= a for a, b in zip(intervals, intervals[1:])):
354
+ raise ValueError("intervals must be strictly ascending")
355
+ if any(i < 0 or i > 24 for i in intervals):
356
+ raise ValueError("intervals must contain values between 0 and 24")
357
+
358
+ if suffix is not None:
359
+ if suffix in _BUILTIN_SUFFIXES:
360
+ raise ValueError(
361
+ f"Suffix {suffix!r} is a built-in chord suffix and cannot be reused. "
362
+ "Choose a different suffix for your custom quality."
363
+ )
364
+ if not suffix or suffix[0] in "ABCDEFG#b0123456789":
365
+ raise ValueError(
366
+ f"Suffix {suffix!r} would be ambiguous in a chord name — "
367
+ "it must not be empty or start with a note letter, accidental, or digit"
368
+ )
369
+
370
+ # Re-registration: drop any suffix this quality registered previously, so
371
+ # renaming a suffix on live reload does not leave a stale alias behind.
372
+ for old_suffix in [s for s, q in _SUFFIX_TO_QUALITY.items() if q == name and s not in _BUILTIN_SUFFIXES]:
373
+ del _SUFFIX_TO_QUALITY[old_suffix]
374
+
375
+ CHORD_INTERVALS[name] = list(intervals)
376
+
377
+ if suffix is not None:
378
+ CHORD_SUFFIX[name] = suffix
379
+ _SUFFIX_TO_QUALITY[suffix] = name
380
+ else:
381
+ CHORD_SUFFIX.pop(name, None)
382
+
383
+
384
+ def parse_chord (name: str) -> Chord:
385
+
386
+ """Parse a chord name like ``"Cm7"`` or ``"Dbmaj7"`` into a :class:`Chord`.
387
+
388
+ The name is a root note (``A``–``G`` with an optional ``#`` or ``b``) followed
389
+ by a quality suffix: ``""`` major, ``m`` minor, ``dim`` diminished,
390
+ ``+``/``aug`` augmented, ``7`` dominant 7th, ``maj7`` major 7th, ``m7`` minor
391
+ 7th, ``m7b5``/``ø`` half-diminished 7th, ``sus2``, ``sus4``. A few common
392
+ alternates (``min``, ``-``, ``M7``, …) are accepted too.
393
+
394
+ Raises ``ValueError`` for anything it can't read, so a typo surfaces at the
395
+ call site rather than as a silently wrong chord.
396
+
397
+ Example:
398
+ ```python
399
+ parse_chord("Cm7") # → Chord(root_pc=0, quality="minor_7th")
400
+ parse_chord("Dbmaj7") # → Chord(root_pc=1, quality="major_7th")
401
+ parse_chord("F#") # → Chord(root_pc=6, quality="major")
402
+ ```
403
+ """
404
+
405
+ stripped = name.strip()
406
+ if not stripped or stripped[0] not in "ABCDEFG":
407
+ raise ValueError(f"Cannot parse chord name {name!r} — expected a root like 'C', 'F#', 'Bb' then a quality, e.g. 'Cm7'")
408
+
409
+ split = 2 if (len(stripped) > 1 and stripped[1] in "#b") else 1
410
+ root_name = stripped[:split]
411
+ suffix = stripped[split:]
412
+
413
+ if root_name not in NOTE_NAME_TO_PC:
414
+ raise ValueError(f"Cannot parse chord name {name!r} — unknown root {root_name!r}")
415
+ if suffix not in _SUFFIX_TO_QUALITY:
416
+ known = ", ".join(repr(key) for key in sorted(_SUFFIX_TO_QUALITY) if key)
417
+ raise ValueError(f"Cannot parse chord name {name!r} — unknown quality {suffix!r}. Known suffixes: {known}")
418
+
419
+ return Chord(root_pc=NOTE_NAME_TO_PC[root_name], quality=_SUFFIX_TO_QUALITY[suffix])