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
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])
|