gingo 1.0.0__cp312-cp312-macosx_11_0_arm64.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.
- gingo/__init__.py +106 -0
- gingo/__init__.pyi +476 -0
- gingo/__main__.py +1219 -0
- gingo/_gingo.cpython-312-darwin.so +0 -0
- gingo/audio.py +507 -0
- gingo/py.typed +0 -0
- gingo-1.0.0.dist-info/METADATA +1098 -0
- gingo-1.0.0.dist-info/RECORD +11 -0
- gingo-1.0.0.dist-info/WHEEL +5 -0
- gingo-1.0.0.dist-info/entry_points.txt +3 -0
- gingo-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gingo
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Music theory library β notes, chords, scales, and harmonic fields
|
|
5
|
+
Keywords: music,theory,chord,scale,harmony,interval,harmonic-field
|
|
6
|
+
Author-Email: Saulo Verissimo <sauloverissimo@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: C++
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Education
|
|
23
|
+
Project-URL: Homepage, https://github.com/sauloverissimo/gingo
|
|
24
|
+
Project-URL: Documentation, https://sauloverissimo.github.io/gingo
|
|
25
|
+
Project-URL: Repository, https://github.com/sauloverissimo/gingo
|
|
26
|
+
Project-URL: Issues, https://github.com/sauloverissimo/gingo/issues
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
30
|
+
Provides-Extra: audio
|
|
31
|
+
Requires-Dist: simpleaudio>=1.0; extra == "audio"
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# πͺ Gingo
|
|
35
|
+
|
|
36
|
+
An expressive music theory and rhythm toolkit for Python, powered by a C++17 core.
|
|
37
|
+
|
|
38
|
+
From pitch classes to harmonic trees and rhythmic grids β with audio playback and a friendly CLI.
|
|
39
|
+
|
|
40
|
+
Notes, intervals, chords, scales, and harmonic fields are just the beginning: Gingo also ships with durations, tempo markings (nomes de tempo), time signatures, and sequence playback.
|
|
41
|
+
|
|
42
|
+
**PortuguΓͺs (pt-BR)**: https://sauloverissimo.github.io/gingo/ (guia e referΓͺncia completos)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## About
|
|
47
|
+
|
|
48
|
+
Gingo is a pragmatic library for analysis, composition, and teaching. It prioritizes correctness, ergonomics, and speed, while keeping the API compact and consistent across concepts.
|
|
49
|
+
|
|
50
|
+
**Highlights**
|
|
51
|
+
|
|
52
|
+
- **C++17 core + Python API** β fast and deterministic, with full type hints.
|
|
53
|
+
- **Pitch & harmony** β `Note`, `Interval`, `Chord`, `Scale`, `Field`, and `Tree` (beta) with identification, deduction, and comparison utilities.
|
|
54
|
+
- **Rhythm & time** β `Duration`, `Tempo` (BPM + nomes de tempo), `TimeSignature`, and `Sequence` with note/chord events.
|
|
55
|
+
- **Audio** β `.play()` and `.to_wav()` on musical objects, plus CLI `--play` / `--wav` with waveform and strum controls.
|
|
56
|
+
- **CLI-first exploration** β query and inspect theory concepts without leaving the terminal.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install gingo
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Optional audio playback dependency:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install "gingo[audio]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Requires Python 3.10+. Pre-built binary wheels are available for Linux, macOS, and Windows β no C++17 compiler needed. If no wheel is available for your platform, pip will build from source automatically.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from gingo import (
|
|
82
|
+
Note, Interval, Chord, Scale, Field, Tree, ScaleType,
|
|
83
|
+
Duration, Tempo, TimeSignature, Sequence,
|
|
84
|
+
NoteEvent, ChordEvent, Rest,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Notes
|
|
88
|
+
note = Note("Bb")
|
|
89
|
+
note.natural() # "A#"
|
|
90
|
+
note.semitone() # 10
|
|
91
|
+
note.frequency(4) # 466.16 Hz
|
|
92
|
+
note.play(octave=4) # Listen to Bb4
|
|
93
|
+
|
|
94
|
+
# Intervals
|
|
95
|
+
iv = Interval("5J")
|
|
96
|
+
iv.semitones() # 7
|
|
97
|
+
iv.anglo_saxon() # "P5"
|
|
98
|
+
|
|
99
|
+
# Chords
|
|
100
|
+
chord = Chord("Cm7")
|
|
101
|
+
chord.root() # Note("C")
|
|
102
|
+
chord.type() # "m7"
|
|
103
|
+
chord.notes() # [Note("C"), Note("Eb"), Note("G"), Note("Bb")]
|
|
104
|
+
chord.interval_labels() # ["P1", "3m", "5J", "7m"]
|
|
105
|
+
chord.play() # Listen to Cm7
|
|
106
|
+
|
|
107
|
+
# Identify a chord from notes
|
|
108
|
+
Chord.identify(["C", "E", "G"]) # Chord("CM")
|
|
109
|
+
|
|
110
|
+
# Identify a scale or field from a full note/chord set
|
|
111
|
+
Scale.identify(["C", "D", "E", "F", "G", "A", "B"]) # Scale("C", "major")
|
|
112
|
+
Field.identify(["CM", "Dm", "Em", "FM", "GM", "Am"]) # Field("C", "major")
|
|
113
|
+
|
|
114
|
+
# Deduce likely fields from partial evidence (ranked)
|
|
115
|
+
matches = Field.deduce(["CM", "FM"])
|
|
116
|
+
matches[0].field # Field("C", "major") or Field("F", "major")
|
|
117
|
+
matches[0].score # 1.0
|
|
118
|
+
|
|
119
|
+
# Compare two chords (absolute, context-free)
|
|
120
|
+
r = Chord("CM").compare(Chord("Am"))
|
|
121
|
+
r.common_notes # [Note("C"), Note("E")]
|
|
122
|
+
r.root_distance # 3
|
|
123
|
+
r.transformation # "R" (neo-Riemannian Relative)
|
|
124
|
+
r.transposition # -1 (not related by transposition)
|
|
125
|
+
r.dissonance_a # 0.057... (psychoacoustic roughness)
|
|
126
|
+
r.to_dict() # full dict serialization
|
|
127
|
+
|
|
128
|
+
# Scales
|
|
129
|
+
scale = Scale("C", ScaleType.Major)
|
|
130
|
+
[n.natural() for n in scale.notes()] # ["C", "D", "E", "F", "G", "A", "B"]
|
|
131
|
+
scale.degree(5) # Note("G")
|
|
132
|
+
scale.play() # Listen to C major scale
|
|
133
|
+
|
|
134
|
+
# Harmonic fields
|
|
135
|
+
field = Field("C", ScaleType.Major)
|
|
136
|
+
[c.name() for c in field.chords()]
|
|
137
|
+
# ["CM", "Dm", "Em", "FM", "GM", "Am", "Bdim"]
|
|
138
|
+
|
|
139
|
+
# Compare two chords within a harmonic field (contextual)
|
|
140
|
+
r = field.compare(Chord("CM"), Chord("GM"))
|
|
141
|
+
r.degree_a # 1 (I)
|
|
142
|
+
r.degree_b # 5 (V)
|
|
143
|
+
r.function_a # HarmonicFunction.Tonic
|
|
144
|
+
r.function_b # HarmonicFunction.Dominant
|
|
145
|
+
r.root_motion # "ascending_fifth"
|
|
146
|
+
r.to_dict() # full dict serialization
|
|
147
|
+
|
|
148
|
+
# Harmonic trees (progressions and voice leading)
|
|
149
|
+
tree = Tree("C", ScaleType.Major)
|
|
150
|
+
# Tree is currently beta (content & references are still growing).
|
|
151
|
+
tree.branches() # All available harmonic branches
|
|
152
|
+
tree.paths("I") # All progressions from tonic
|
|
153
|
+
tree.shortest_path("I", "V7") # ["I", "V7"]
|
|
154
|
+
tree.is_valid_progression(["IIm", "V7", "I"]) # True
|
|
155
|
+
tree.function("V7") # HarmonicFunction.Dominant
|
|
156
|
+
tree.to_dot() # Export to Graphviz
|
|
157
|
+
tree.to_mermaid() # Export to Mermaid diagram
|
|
158
|
+
|
|
159
|
+
# Rhythm
|
|
160
|
+
q = Duration("quarter")
|
|
161
|
+
dotted = Duration("eighth", dots=1)
|
|
162
|
+
triplet = Duration("eighth", tuplet=3)
|
|
163
|
+
Tempo("Allegro").bpm() # 140.0
|
|
164
|
+
Tempo(120).marking() # "Allegretto"
|
|
165
|
+
TimeSignature(6, 8).classification() # "compound"
|
|
166
|
+
|
|
167
|
+
# Sequence (events in time)
|
|
168
|
+
seq = Sequence(Tempo(120), TimeSignature(4, 4))
|
|
169
|
+
seq.add(NoteEvent(Note("C"), Duration("quarter"), octave=4))
|
|
170
|
+
seq.add(ChordEvent(Chord("G7"), Duration("half"), octave=4))
|
|
171
|
+
seq.add(Rest(Duration("quarter")))
|
|
172
|
+
seq.total_seconds()
|
|
173
|
+
|
|
174
|
+
# Audio
|
|
175
|
+
Note("C").play()
|
|
176
|
+
Chord("Am7").play(waveform="square")
|
|
177
|
+
Scale("C", "major").to_wav("c_major.wav")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## CLI (quick exploration)
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
gingo note C#
|
|
186
|
+
gingo note C --fifths
|
|
187
|
+
gingo interval 7 --all
|
|
188
|
+
gingo scale "C major" --degree 5 5
|
|
189
|
+
gingo scale "C,D,E,F,G,A,B" --identify
|
|
190
|
+
gingo field "C major" --functions
|
|
191
|
+
gingo field "CM,FM,G7" --identify
|
|
192
|
+
gingo field "CM,FM" --deduce
|
|
193
|
+
gingo compare CM GM --field "C major"
|
|
194
|
+
gingo note C --play --waveform triangle
|
|
195
|
+
gingo chord Am7 --play --strum 0.05
|
|
196
|
+
gingo chord Am7 --wav am7.wav
|
|
197
|
+
gingo duration quarter --tempo 120
|
|
198
|
+
gingo tempo Allegro --all
|
|
199
|
+
gingo timesig 6 8 --tempo 120
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Audio flags:
|
|
203
|
+
|
|
204
|
+
- `--play` outputs to the system audio device
|
|
205
|
+
- `--wav FILE` exports a WAV file
|
|
206
|
+
- `--waveform` (`sine`, `square`, `sawtooth`, `triangle`)
|
|
207
|
+
- `--strum` and `--gap` control timing between chord tones and events
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Detailed Guide
|
|
212
|
+
|
|
213
|
+
### Note
|
|
214
|
+
|
|
215
|
+
The `Note` class is the atomic unit of the library. It represents a single pitch class (C, D, E, F, G, A, B) with optional accidentals.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from gingo import Note
|
|
219
|
+
|
|
220
|
+
# Construction β accepts any common notation
|
|
221
|
+
c = Note("C") # Natural
|
|
222
|
+
bb = Note("Bb") # Flat
|
|
223
|
+
fs = Note("F#") # Sharp
|
|
224
|
+
eb = Note("Eβ") # Unicode flat
|
|
225
|
+
gs = Note("G##") # Double sharp
|
|
226
|
+
|
|
227
|
+
# Core properties
|
|
228
|
+
bb.name() # "Bb" β the original input
|
|
229
|
+
bb.natural() # "A#" β canonical sharp-based form
|
|
230
|
+
bb.sound() # "B" β base letter (no accidentals)
|
|
231
|
+
bb.semitone() # 10 β chromatic position (C=0, C#=1, ..., B=11)
|
|
232
|
+
|
|
233
|
+
# Frequency calculation (A4 = 440 Hz standard tuning)
|
|
234
|
+
Note("A").frequency(4) # 440.0 Hz
|
|
235
|
+
Note("A").frequency(3) # 220.0 Hz
|
|
236
|
+
Note("C").frequency(4) # 261.63 Hz
|
|
237
|
+
Note("A").frequency(5) # 880.0 Hz
|
|
238
|
+
|
|
239
|
+
# Enharmonic equivalence
|
|
240
|
+
Note("Bb").is_enharmonic(Note("A#")) # True
|
|
241
|
+
Note("Db").is_enharmonic(Note("C#")) # True
|
|
242
|
+
Note("C").is_enharmonic(Note("D")) # False
|
|
243
|
+
|
|
244
|
+
# Equality (compares natural forms)
|
|
245
|
+
Note("Bb") == Note("A#") # True β same natural form
|
|
246
|
+
Note("C") == Note("C") # True
|
|
247
|
+
Note("C") != Note("D") # True
|
|
248
|
+
|
|
249
|
+
# Transposition
|
|
250
|
+
Note("C").transpose(7) # Note("G") β up a perfect fifth
|
|
251
|
+
Note("C").transpose(12) # Note("C") β up an octave
|
|
252
|
+
Note("A").transpose(-2) # Note("G") β down a whole step
|
|
253
|
+
Note("E").transpose(1) # Note("F") β up a semitone
|
|
254
|
+
|
|
255
|
+
# Audio playback (requires gingo[audio])
|
|
256
|
+
Note("A").play(octave=4) # A4 (440 Hz)
|
|
257
|
+
Note("C").play(octave=5, waveform="square") # C5 with square wave
|
|
258
|
+
Note("Eb").to_wav("eb.wav", octave=4) # Export to WAV file
|
|
259
|
+
|
|
260
|
+
# Static utilities
|
|
261
|
+
Note.to_natural("Bb") # "A#"
|
|
262
|
+
Note.to_natural("G##") # "A"
|
|
263
|
+
Note.to_natural("Bbb") # "A"
|
|
264
|
+
Note.extract_root("C#m7") # "C#"
|
|
265
|
+
Note.extract_root("Bbdim") # "Bb"
|
|
266
|
+
Note.extract_sound("Gb") # "G"
|
|
267
|
+
Note.extract_type("C#m7") # "m7"
|
|
268
|
+
Note.extract_type("F#m7(b5)") # "m7(b5)"
|
|
269
|
+
Note.extract_type("C") # ""
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Enharmonic Resolution Table
|
|
273
|
+
|
|
274
|
+
Gingo resolves 89 enharmonic spellings to a canonical sharp-based form:
|
|
275
|
+
|
|
276
|
+
| Input | Natural | Category |
|
|
277
|
+
|-------|---------|----------|
|
|
278
|
+
| `Bb` | `A#` | Standard flat |
|
|
279
|
+
| `Db` | `C#` | Standard flat |
|
|
280
|
+
| `Eb` | `D#` | Standard flat |
|
|
281
|
+
| `Gb` | `F#` | Standard flat |
|
|
282
|
+
| `Ab` | `G#` | Standard flat |
|
|
283
|
+
| `E#` | `F` | Special sharp (no sharp exists) |
|
|
284
|
+
| `B#` | `C` | Special sharp (no sharp exists) |
|
|
285
|
+
| `Fb` | `E` | Special flat (no flat exists) |
|
|
286
|
+
| `Cb` | `B` | Special flat (no flat exists) |
|
|
287
|
+
| `G##` | `A` | Double sharp |
|
|
288
|
+
| `C##` | `D` | Double sharp |
|
|
289
|
+
| `E##` | `F#` | Double sharp |
|
|
290
|
+
| `Bbb` | `A` | Double flat |
|
|
291
|
+
| `Abb` | `G` | Double flat |
|
|
292
|
+
| `Bβ` | `A#` | Unicode flat symbol |
|
|
293
|
+
| `Eββ` | `D` | Unicode double flat |
|
|
294
|
+
| `ββG` | `F` | Prefix accidentals |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### Interval
|
|
299
|
+
|
|
300
|
+
The `Interval` class represents the distance between two pitches, covering two full octaves (24 semitones).
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from gingo import Interval
|
|
304
|
+
|
|
305
|
+
# Construction β from label or semitone count
|
|
306
|
+
p1 = Interval("P1") # Perfect unison
|
|
307
|
+
m3 = Interval("3m") # Minor third
|
|
308
|
+
M3 = Interval("3M") # Major third
|
|
309
|
+
p5 = Interval("5J") # Perfect fifth
|
|
310
|
+
m7 = Interval("7m") # Minor seventh
|
|
311
|
+
|
|
312
|
+
# From semitone count
|
|
313
|
+
iv = Interval(7) # Same as Interval("5J")
|
|
314
|
+
|
|
315
|
+
# Properties
|
|
316
|
+
m3.label() # "3m"
|
|
317
|
+
m3.anglo_saxon() # "mi3"
|
|
318
|
+
m3.semitones() # 3
|
|
319
|
+
m3.degree() # 3
|
|
320
|
+
m3.octave() # 1
|
|
321
|
+
|
|
322
|
+
# Second octave intervals
|
|
323
|
+
b9 = Interval("b9")
|
|
324
|
+
b9.semitones() # 13
|
|
325
|
+
b9.octave() # 2
|
|
326
|
+
|
|
327
|
+
# Equality (by semitone distance)
|
|
328
|
+
Interval("P1") == Interval(0) # True
|
|
329
|
+
Interval("5J") == Interval(7) # True
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
#### All 24 Interval Labels
|
|
333
|
+
|
|
334
|
+
| Semitones | Label | Anglo-Saxon | Degree |
|
|
335
|
+
|-----------|-------|-------------|--------|
|
|
336
|
+
| 0 | P1 | P1 | 1 |
|
|
337
|
+
| 1 | 2m | mi2 | 2 |
|
|
338
|
+
| 2 | 2M | ma2 | 2 |
|
|
339
|
+
| 3 | 3m | mi3 | 3 |
|
|
340
|
+
| 4 | 3M | ma3 | 3 |
|
|
341
|
+
| 5 | 4J | P4 | 4 |
|
|
342
|
+
| 6 | d5 | d5 | 5 |
|
|
343
|
+
| 7 | 5J | P5 | 5 |
|
|
344
|
+
| 8 | #5 | mi6 | 6 |
|
|
345
|
+
| 9 | M6 | ma6 | 6 |
|
|
346
|
+
| 10 | 7m | mi7 | 7 |
|
|
347
|
+
| 11 | 7M | ma7 | 7 |
|
|
348
|
+
| 12 | 8J | P8 | 8 |
|
|
349
|
+
| 13 | b9 | mi9 | 9 |
|
|
350
|
+
| 14 | 9 | ma9 | 9 |
|
|
351
|
+
| 15 | #9 | mi10 | 10 |
|
|
352
|
+
| 16 | b11 | ma10 | 10 |
|
|
353
|
+
| 17 | 11 | P11 | 11 |
|
|
354
|
+
| 18 | #11 | d11 | 11 |
|
|
355
|
+
| 19 | 5 | P12 | 12 |
|
|
356
|
+
| 20 | b13 | mi13 | 13 |
|
|
357
|
+
| 21 | 13 | ma13 | 13 |
|
|
358
|
+
| 22 | #13 | mi14 | 14 |
|
|
359
|
+
| 23 | bI | ma14 | 14 |
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
### Chord
|
|
364
|
+
|
|
365
|
+
The `Chord` class represents a musical chord β a root note plus a set of intervals from a database of 42 chord formulas.
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
from gingo import Chord, Note
|
|
369
|
+
|
|
370
|
+
# Construction from name
|
|
371
|
+
cm = Chord("CM") # C major
|
|
372
|
+
dm7 = Chord("Dm7") # D minor seventh
|
|
373
|
+
bb7m = Chord("Bb7M") # Bb major seventh
|
|
374
|
+
fsdim = Chord("F#dim") # F# diminished
|
|
375
|
+
|
|
376
|
+
# Root, type, and name
|
|
377
|
+
cm.root() # Note("C")
|
|
378
|
+
cm.root().natural() # "C"
|
|
379
|
+
cm.type() # "M"
|
|
380
|
+
cm.name() # "CM"
|
|
381
|
+
|
|
382
|
+
# Notes β with correct enharmonic spelling
|
|
383
|
+
[n.name() for n in Chord("CM").notes()]
|
|
384
|
+
# ["C", "E", "G"]
|
|
385
|
+
|
|
386
|
+
[n.name() for n in Chord("Am7").notes()]
|
|
387
|
+
# ["A", "C", "E", "G"]
|
|
388
|
+
|
|
389
|
+
[n.name() for n in Chord("Dbm7").notes()]
|
|
390
|
+
# ["Db", "Fb", "Ab", "Cb"] β proper flat spelling
|
|
391
|
+
|
|
392
|
+
# Notes can also be accessed as natural (sharp-based) canonical form
|
|
393
|
+
[n.natural() for n in Chord("Dbm7").notes()]
|
|
394
|
+
# ["C#", "E", "G#", "B"]
|
|
395
|
+
|
|
396
|
+
# Interval structure
|
|
397
|
+
Chord("Am7").interval_labels()
|
|
398
|
+
# ["P1", "3m", "5J", "7m"]
|
|
399
|
+
|
|
400
|
+
Chord("CM").interval_labels()
|
|
401
|
+
# ["P1", "3M", "5J"]
|
|
402
|
+
|
|
403
|
+
Chord("Bdim").interval_labels()
|
|
404
|
+
# ["P1", "3m", "d5"]
|
|
405
|
+
|
|
406
|
+
# Size
|
|
407
|
+
Chord("CM").size() # 3 (triad)
|
|
408
|
+
Chord("Am7").size() # 4 (seventh chord)
|
|
409
|
+
Chord("G7").size() # 4
|
|
410
|
+
|
|
411
|
+
# Contains β check if a note belongs to the chord
|
|
412
|
+
Chord("CM").contains(Note("E")) # True
|
|
413
|
+
Chord("CM").contains(Note("F")) # False
|
|
414
|
+
|
|
415
|
+
# Identify chord from notes (reverse lookup)
|
|
416
|
+
c = Chord.identify(["C", "E", "G"])
|
|
417
|
+
c.name() # "CM"
|
|
418
|
+
c.type() # "M"
|
|
419
|
+
|
|
420
|
+
c2 = Chord.identify(["D", "F#", "A", "C#", "E"])
|
|
421
|
+
c2.type() # "9"
|
|
422
|
+
|
|
423
|
+
# Equality
|
|
424
|
+
Chord("CM") == Chord("CM") # True
|
|
425
|
+
Chord("CM") != Chord("Cm") # True
|
|
426
|
+
|
|
427
|
+
# Audio playback (requires gingo[audio])
|
|
428
|
+
Chord("Am7").play() # Play Am7 chord
|
|
429
|
+
Chord("G7").play(waveform="sawtooth") # Custom waveform
|
|
430
|
+
Chord("Dm").play(strum=0.05) # Arpeggiated/strummed
|
|
431
|
+
Chord("CM").to_wav("cmajor.wav", octave=4) # Export to WAV file
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### Supported Chord Types (42 formulas)
|
|
435
|
+
|
|
436
|
+
**Triads (7):** M, m, dim, aug, sus2, sus4, 5
|
|
437
|
+
|
|
438
|
+
**Seventh chords (10):** 7, m7, 7M, m7M, dim7, m7(b5), 7(b5), 7(#5), 7M(#5), sus7
|
|
439
|
+
|
|
440
|
+
**Sixth chords (3):** 6, m6, 6(9)
|
|
441
|
+
|
|
442
|
+
**Ninth chords (4):** 9, m9, M9, sus9
|
|
443
|
+
|
|
444
|
+
**Extended chords (6):** 11, m11, m7(11), 13, m13, M13
|
|
445
|
+
|
|
446
|
+
**Altered chords (6):** 7(b9), 7(#9), 7(#11), 13(#11), (b9), (b13)
|
|
447
|
+
|
|
448
|
+
**Add chords (4):** add9, add2, add11, add4
|
|
449
|
+
|
|
450
|
+
**Other (2):** sus, 7+5
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
### Scale
|
|
455
|
+
|
|
456
|
+
The `Scale` class builds a scale from a tonic note and a scale pattern. It supports 10 parent families, mode names, pentatonic filters, and a chainable API.
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
from gingo import Scale, ScaleType, Note
|
|
460
|
+
|
|
461
|
+
# Construction β from enum, string, or mode name
|
|
462
|
+
s1 = Scale("C", ScaleType.Major)
|
|
463
|
+
s2 = Scale("C", "major") # string form
|
|
464
|
+
s3 = Scale("D", "dorian") # mode name β Major, mode 2
|
|
465
|
+
s4 = Scale("E", "phrygian dominant") # mode name β HarmonicMinor, mode 5
|
|
466
|
+
s5 = Scale("C", "altered") # mode name β MelodicMinor, mode 7
|
|
467
|
+
|
|
468
|
+
# Scale identity
|
|
469
|
+
d = Scale("D", "dorian")
|
|
470
|
+
d.parent() # ScaleType.Major
|
|
471
|
+
d.mode_number() # 2
|
|
472
|
+
d.mode_name() # "Dorian"
|
|
473
|
+
d.quality() # "minor"
|
|
474
|
+
d.brightness() # 3
|
|
475
|
+
|
|
476
|
+
# Scale notes (with correct enharmonic spelling)
|
|
477
|
+
[n.name() for n in Scale("C", "major").notes()]
|
|
478
|
+
# ["C", "D", "E", "F", "G", "A", "B"]
|
|
479
|
+
|
|
480
|
+
[n.name() for n in Scale("D", "dorian").notes()]
|
|
481
|
+
# ["D", "E", "F", "G", "A", "B", "C"]
|
|
482
|
+
|
|
483
|
+
[n.name() for n in Scale("Gb", "major").notes()]
|
|
484
|
+
# ["Gb", "Ab", "Bb", "Cb", "Db", "Eb", "F"]
|
|
485
|
+
|
|
486
|
+
# Natural form (canonical sharp-based) also available
|
|
487
|
+
[n.natural() for n in Scale("Gb", "major").notes()]
|
|
488
|
+
# ["F#", "G#", "A#", "B", "C#", "D#", "F"]
|
|
489
|
+
|
|
490
|
+
# Degree access (1-indexed, supports chaining)
|
|
491
|
+
s = Scale("C", "major")
|
|
492
|
+
s.degree(1) # Note("C") β tonic
|
|
493
|
+
s.degree(5) # Note("G") β dominant
|
|
494
|
+
s.degree(5, 5) # Note("D") β V of V
|
|
495
|
+
s.degree(5, 5, 3) # Note("F") β III of V of V
|
|
496
|
+
|
|
497
|
+
# Walk: navigate along the scale
|
|
498
|
+
s.walk(1, 4) # Note("F") β from I, a fourth = IV
|
|
499
|
+
s.walk(5, 5) # Note("D") β from V, a fifth = II
|
|
500
|
+
|
|
501
|
+
# Modes by number or name
|
|
502
|
+
s.mode(2) # D Dorian
|
|
503
|
+
s.mode("lydian") # F Lydian
|
|
504
|
+
|
|
505
|
+
# Pentatonic
|
|
506
|
+
s.pentatonic() # C major pentatonic (5 notes)
|
|
507
|
+
Scale("C", "major pentatonic") # same thing
|
|
508
|
+
Scale("A", "minor pentatonic") # A C D E G
|
|
509
|
+
|
|
510
|
+
# Color notes (what distinguishes this mode from a reference)
|
|
511
|
+
Scale("C", "dorian").colors("ionian") # [Eb, Bb]
|
|
512
|
+
|
|
513
|
+
# Other families
|
|
514
|
+
Scale("C", "whole tone").size() # 6
|
|
515
|
+
Scale("A", "blues").size() # 6
|
|
516
|
+
Scale("C", "chromatic").size() # 12
|
|
517
|
+
Scale("C", "diminished").size() # 8
|
|
518
|
+
|
|
519
|
+
# Audio playback (requires gingo[audio])
|
|
520
|
+
Scale("C", "major").play() # Play C major scale
|
|
521
|
+
Scale("D", "dorian").play(waveform="triangle") # Custom waveform
|
|
522
|
+
Scale("A", "minor").to_wav("a_minor.wav") # Export to WAV file
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### Scale Types (10 parent families)
|
|
526
|
+
|
|
527
|
+
| Type | Notes | Pattern | Description |
|
|
528
|
+
|------|:-----:|---------|-------------|
|
|
529
|
+
| `Major` | 7 | W-W-H-W-W-W-H | Ionian mode, the most common Western scale |
|
|
530
|
+
| `NaturalMinor` | 7 | W-H-W-W-H-W-W | Aeolian mode, relative minor |
|
|
531
|
+
| `HarmonicMinor` | 7 | W-H-W-W-H-A2-H | Raised 7th degree, characteristic V7 chord |
|
|
532
|
+
| `MelodicMinor` | 7 | W-H-W-W-W-W-H | Raised 6th and 7th degrees (ascending) |
|
|
533
|
+
| `HarmonicMajor` | 7 | W-W-H-W-H-A2-H | Major with lowered 6th degree |
|
|
534
|
+
| `Diminished` | 8 | W-H-W-H-W-H-W-H | Symmetric octatonic scale |
|
|
535
|
+
| `WholeTone` | 6 | W-W-W-W-W-W | Symmetric whole-tone scale |
|
|
536
|
+
| `Augmented` | 6 | A2-H-A2-H-A2-H | Symmetric augmented scale |
|
|
537
|
+
| `Blues` | 6 | m3-W-H-H-m3-W | Minor pentatonic + blue note |
|
|
538
|
+
| `Chromatic` | 12 | H-H-H-H-H-H-H-H-H-H-H-H | All 12 pitch classes |
|
|
539
|
+
|
|
540
|
+
W = whole step, H = half step, A2 = augmented second, m3 = minor third
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Field (Harmonic Field)
|
|
545
|
+
|
|
546
|
+
The `Field` class generates the diatonic chords built from each degree of a scale β the harmonic field.
|
|
547
|
+
|
|
548
|
+
```python
|
|
549
|
+
from gingo import Field, ScaleType, HarmonicFunction
|
|
550
|
+
|
|
551
|
+
# Construction
|
|
552
|
+
f = Field("C", ScaleType.Major)
|
|
553
|
+
|
|
554
|
+
# Triads (3-note chords on each degree)
|
|
555
|
+
triads = f.chords()
|
|
556
|
+
[c.name() for c in triads]
|
|
557
|
+
# ["CM", "Dm", "Em", "FM", "GM", "Am", "Bdim"]
|
|
558
|
+
# I ii iii IV V vi viiΒ°
|
|
559
|
+
|
|
560
|
+
# Seventh chords (4-note chords on each degree)
|
|
561
|
+
sevenths = f.sevenths()
|
|
562
|
+
[c.name() for c in sevenths]
|
|
563
|
+
# ["CM7", "Dm7", "Em7", "FM7", "G7", "Am7", "Bm7(b5)"]
|
|
564
|
+
# Imaj7 ii7 iii7 IVmaj7 V7 vi7 vii-7(b5)
|
|
565
|
+
|
|
566
|
+
# Access by degree (1-indexed)
|
|
567
|
+
f.chord(1) # Chord("CM")
|
|
568
|
+
f.chord(5) # Chord("GM")
|
|
569
|
+
f.seventh(5) # Chord("G7")
|
|
570
|
+
|
|
571
|
+
# Harmonic function (Tonic / Subdominant / Dominant)
|
|
572
|
+
f.function(1) # HarmonicFunction.Tonic
|
|
573
|
+
f.function(5) # HarmonicFunction.Dominant
|
|
574
|
+
f.function(5).name # "Dominant"
|
|
575
|
+
f.function(5).short # "D"
|
|
576
|
+
|
|
577
|
+
# Role within function group
|
|
578
|
+
f.role(1) # "primary"
|
|
579
|
+
f.role(6) # "relative of I"
|
|
580
|
+
|
|
581
|
+
# Query by chord name or object
|
|
582
|
+
f.function("FM") # HarmonicFunction.Subdominant
|
|
583
|
+
f.function("F#M") # None (not in the field)
|
|
584
|
+
f.role("Am") # "relative of I"
|
|
585
|
+
|
|
586
|
+
# Applied chords (tonicization)
|
|
587
|
+
f.applied("V7", 2) # Chord("A7") β V7 of degree II
|
|
588
|
+
f.applied("V7", "V") # Chord("D7") β V7 of degree V
|
|
589
|
+
f.applied("IIm7(b5)", 5) # Chord("Am7(b5)")
|
|
590
|
+
f.applied(5, 2) # Chord("A7") β numeric shorthand
|
|
591
|
+
|
|
592
|
+
# Number of degrees
|
|
593
|
+
f.size() # 7
|
|
594
|
+
|
|
595
|
+
# Works with any scale type
|
|
596
|
+
f_minor = Field("A", ScaleType.HarmonicMinor)
|
|
597
|
+
[c.name() for c in f_minor.chords()]
|
|
598
|
+
# Harmonic minor field: Am, Bdim, Caug, Dm, EM, FM, G#dim
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
### Tree (Harmonic Tree / Progressions) β beta
|
|
604
|
+
|
|
605
|
+
The `Tree` class represents harmonic progressions and voice leading paths within a scale's harmonic field. Based on JosΓ© de Alencar's harmonic tree theory.
|
|
606
|
+
|
|
607
|
+
**β οΈ Status: beta** β This feature is under active study and development. Due to limited bibliographic references available, the current implementation may contain errors or incomplete patterns. Use with caution and validate results against your harmonic analysis needs.
|
|
608
|
+
|
|
609
|
+
```python
|
|
610
|
+
from gingo import Tree, ScaleType, HarmonicFunction
|
|
611
|
+
|
|
612
|
+
# Construction
|
|
613
|
+
tree = Tree("C", ScaleType.Major)
|
|
614
|
+
|
|
615
|
+
# List all available harmonic branches
|
|
616
|
+
branches = tree.branches()
|
|
617
|
+
# ["I", "IIm", "IIIm", "IV", "V7", "VIm", "VIIdim", "V7/IV", "IVm", "bVI", "bVII", ...]
|
|
618
|
+
|
|
619
|
+
# Get all possible paths from a branch
|
|
620
|
+
paths = tree.paths("I")
|
|
621
|
+
for path in paths[:3]:
|
|
622
|
+
print(f"{path.id}: {path.branch} β {path.chord.name()}")
|
|
623
|
+
# 0: I β CM
|
|
624
|
+
# 1: IIm / IV β Dm
|
|
625
|
+
# 2: VIm β Am
|
|
626
|
+
|
|
627
|
+
# Path information
|
|
628
|
+
path = paths[1]
|
|
629
|
+
path.branch # "IIm / IV"
|
|
630
|
+
path.chord # Chord object
|
|
631
|
+
path.chord.name() # "Dm"
|
|
632
|
+
path.interval_labels # ["P1", "3m", "5J"]
|
|
633
|
+
path.note_names # ["D", "F", "A"]
|
|
634
|
+
|
|
635
|
+
# Find shortest path between two branches
|
|
636
|
+
path = tree.shortest_path("I", "V7")
|
|
637
|
+
# ["I", "V7"]
|
|
638
|
+
|
|
639
|
+
path = tree.shortest_path("I", "IV")
|
|
640
|
+
# ["I", "VIm", "IV"] or another valid path
|
|
641
|
+
|
|
642
|
+
# Validate a progression
|
|
643
|
+
tree.is_valid_progression(["IIm", "V7", "I"]) # True (II-V-I)
|
|
644
|
+
tree.is_valid_progression(["I", "IV", "V7"]) # True
|
|
645
|
+
tree.is_valid_progression(["I", "INVALID"]) # False
|
|
646
|
+
|
|
647
|
+
# Harmonic function classification
|
|
648
|
+
tree.function("I") # HarmonicFunction.Tonic
|
|
649
|
+
tree.function("IV") # HarmonicFunction.Subdominant
|
|
650
|
+
tree.function("V7") # HarmonicFunction.Dominant
|
|
651
|
+
tree.function("VIm") # HarmonicFunction.Tonic (relative)
|
|
652
|
+
|
|
653
|
+
# Get all branches with a specific function
|
|
654
|
+
tonics = tree.branches_with_function(HarmonicFunction.Tonic)
|
|
655
|
+
# ["I", "VIm", ...]
|
|
656
|
+
|
|
657
|
+
dominants = tree.branches_with_function(HarmonicFunction.Dominant)
|
|
658
|
+
# ["V7", "VIIdim", ...]
|
|
659
|
+
|
|
660
|
+
# Export to visualization formats
|
|
661
|
+
dot = tree.to_dot(show_functions=True)
|
|
662
|
+
# Graphviz DOT format with color-coded functions
|
|
663
|
+
|
|
664
|
+
mermaid = tree.to_mermaid()
|
|
665
|
+
# Mermaid diagram format
|
|
666
|
+
|
|
667
|
+
# Works with minor scales
|
|
668
|
+
tree_minor = Tree("A", ScaleType.NaturalMinor)
|
|
669
|
+
tree_minor.branches()
|
|
670
|
+
# ["Im", "IIdim", "bIII", "IVm", "Vm", "bVI", "bVII", ...]
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## API Reference Summary
|
|
676
|
+
|
|
677
|
+
### Note
|
|
678
|
+
|
|
679
|
+
| Method | Returns | Description |
|
|
680
|
+
|--------|---------|-------------|
|
|
681
|
+
| `Note(name)` | `Note` | Construct from any notation |
|
|
682
|
+
| `.name()` | `str` | Original input name |
|
|
683
|
+
| `.natural()` | `str` | Canonical sharp form |
|
|
684
|
+
| `.sound()` | `str` | Base letter only |
|
|
685
|
+
| `.semitone()` | `int` | Chromatic index 0-11 |
|
|
686
|
+
| `.frequency(octave=4)` | `float` | Concert pitch in Hz |
|
|
687
|
+
| `.is_enharmonic(other)` | `bool` | Same pitch class? |
|
|
688
|
+
| `.transpose(semitones)` | `Note` | Shifted note |
|
|
689
|
+
| `Note.to_natural(name)` | `str` | Static: resolve spelling |
|
|
690
|
+
| `Note.extract_root(name)` | `str` | Static: root from chord name |
|
|
691
|
+
| `Note.extract_sound(name)` | `str` | Static: base letter from name |
|
|
692
|
+
| `Note.extract_type(name)` | `str` | Static: chord type suffix |
|
|
693
|
+
|
|
694
|
+
### Interval
|
|
695
|
+
|
|
696
|
+
| Method | Returns | Description |
|
|
697
|
+
|--------|---------|-------------|
|
|
698
|
+
| `Interval(label)` | `Interval` | From label string |
|
|
699
|
+
| `Interval(semitones)` | `Interval` | From semitone count |
|
|
700
|
+
| `.label()` | `str` | Short label |
|
|
701
|
+
| `.anglo_saxon()` | `str` | Anglo-Saxon formal name |
|
|
702
|
+
| `.semitones()` | `int` | Semitone distance |
|
|
703
|
+
| `.degree()` | `int` | Diatonic degree number |
|
|
704
|
+
| `.octave()` | `int` | Octave (1 or 2) |
|
|
705
|
+
|
|
706
|
+
### Chord
|
|
707
|
+
|
|
708
|
+
| Method | Returns | Description |
|
|
709
|
+
|--------|---------|-------------|
|
|
710
|
+
| `Chord(name)` | `Chord` | From chord name |
|
|
711
|
+
| `.name()` | `str` | Full chord name |
|
|
712
|
+
| `.root()` | `Note` | Root note |
|
|
713
|
+
| `.type()` | `str` | Quality suffix |
|
|
714
|
+
| `.notes()` | `list[Note]` | Chord tones (natural) |
|
|
715
|
+
| `.formal_notes()` | `list[Note]` | Chord tones (diatonic spelling) |
|
|
716
|
+
| `.intervals()` | `list[Interval]` | Interval objects |
|
|
717
|
+
| `.interval_labels()` | `list[str]` | Interval label strings |
|
|
718
|
+
| `.size()` | `int` | Number of notes |
|
|
719
|
+
| `.contains(note)` | `bool` | Note membership test |
|
|
720
|
+
| `.compare(other)` | `ChordComparison` | Detailed comparison (18 dimensions) |
|
|
721
|
+
| `Chord.identify(names)` | `Chord` | Static: reverse lookup |
|
|
722
|
+
|
|
723
|
+
### Scale
|
|
724
|
+
|
|
725
|
+
| Method | Returns | Description |
|
|
726
|
+
|--------|---------|-------------|
|
|
727
|
+
| `Scale(tonic, type)` | `Scale` | From tonic + ScaleType/string/mode name |
|
|
728
|
+
| `.tonic()` | `Note` | Tonic note |
|
|
729
|
+
| `.parent()` | `ScaleType` | Parent family (Major, HarmonicMinor, ...) |
|
|
730
|
+
| `.mode_number()` | `int` | Mode number (1-7) |
|
|
731
|
+
| `.mode_name()` | `str` | Mode name (Ionian, Dorian, ...) |
|
|
732
|
+
| `.quality()` | `str` | Tonal quality ("major" / "minor") |
|
|
733
|
+
| `.brightness()` | `int` | Brightness (1=Locrian, 7=Lydian) |
|
|
734
|
+
| `.is_pentatonic()` | `bool` | Whether pentatonic filter is active |
|
|
735
|
+
| `.type()` | `ScaleType` | Scale type enum (backward compat, = parent) |
|
|
736
|
+
| `.modality()` | `Modality` | Modality enum (backward compat) |
|
|
737
|
+
| `.notes()` | `list[Note]` | Scale notes (natural) |
|
|
738
|
+
| `.formal_notes()` | `list[Note]` | Scale notes (diatonic) |
|
|
739
|
+
| `.degree(*degrees)` | `Note` | Chained degree: `degree(5, 5)` = V of V |
|
|
740
|
+
| `.walk(start, *steps)` | `Note` | Walk: `walk(1, 4)` = IV |
|
|
741
|
+
| `.size()` | `int` | Number of notes |
|
|
742
|
+
| `.contains(note)` | `bool` | Note membership |
|
|
743
|
+
| `.mode(n_or_name)` | `Scale` | Mode by number (int) or name (str) |
|
|
744
|
+
| `.pentatonic()` | `Scale` | Pentatonic version of the scale |
|
|
745
|
+
| `.colors(reference)` | `list[Note]` | Notes differing from a reference mode |
|
|
746
|
+
| `.mask()` | `list[int]` | 24-bit active positions |
|
|
747
|
+
| `Scale.parse_type(name)` | `ScaleType` | Static: string to enum |
|
|
748
|
+
| `Scale.parse_modality(name)` | `Modality` | Static: string to enum |
|
|
749
|
+
| `Scale.identify(notes)` | `Scale` | Static: detect scale from full note set |
|
|
750
|
+
|
|
751
|
+
### Field
|
|
752
|
+
|
|
753
|
+
| Method | Returns | Description |
|
|
754
|
+
|--------|---------|-------------|
|
|
755
|
+
| `Field(tonic, type)` | `Field` | From tonic + ScaleType/string |
|
|
756
|
+
| `.tonic()` | `Note` | Tonic note |
|
|
757
|
+
| `.scale()` | `Scale` | Underlying scale |
|
|
758
|
+
| `.chords()` | `list[Chord]` | Triads per degree |
|
|
759
|
+
| `.sevenths()` | `list[Chord]` | Seventh chords per degree |
|
|
760
|
+
| `.chord(degree)` | `Chord` | Triad at degree N |
|
|
761
|
+
| `.seventh(degree)` | `Chord` | 7th chord at degree N |
|
|
762
|
+
| `.applied(func, target)` | `Chord` | Applied chord (tonicization) |
|
|
763
|
+
| `.function(degree)` | `HarmonicFunction` | Harmonic function (T/S/D) |
|
|
764
|
+
| `.function(chord)` | `HarmonicFunction?` | Function by chord (None if not in field) |
|
|
765
|
+
| `.role(degree)` | `str` | Role: "primary", "relative of I", etc. |
|
|
766
|
+
| `.role(chord)` | `str?` | Role by chord (None if not in field) |
|
|
767
|
+
| `.compare(a, b)` | `FieldComparison` | Contextual comparison (21 dimensions) |
|
|
768
|
+
| `.size()` | `int` | Number of degrees |
|
|
769
|
+
| `Field.identify(items)` | `Field` | Static: detect field from full notes/chords |
|
|
770
|
+
| `Field.deduce(items, limit=10)` | `list[FieldMatch]` | Static: ranked candidates from partial input |
|
|
771
|
+
|
|
772
|
+
### Tree
|
|
773
|
+
|
|
774
|
+
| Method | Returns | Description |
|
|
775
|
+
|--------|---------|-------------|
|
|
776
|
+
| `Tree(tonic, type)` | `Tree` | From tonic + ScaleType/string |
|
|
777
|
+
| `.tonic()` | `Note` | Tonic note |
|
|
778
|
+
| `.type()` | `ScaleType` | Scale type |
|
|
779
|
+
| `.branches()` | `list[str]` | All harmonic branches |
|
|
780
|
+
| `.paths(branch)` | `list[HarmonicPath]` | All paths from a branch |
|
|
781
|
+
| `.shortest_path(from, to)` | `list[str]` | Shortest progression |
|
|
782
|
+
| `.is_valid_progression(branches)` | `bool` | Validate progression |
|
|
783
|
+
| `.function(branch)` | `HarmonicFunction` | Harmonic function (T/S/D) |
|
|
784
|
+
| `.branches_with_function(func)` | `list[str]` | Branches with function |
|
|
785
|
+
| `.to_dot(show_functions=False)` | `str` | Graphviz DOT export |
|
|
786
|
+
| `.to_mermaid()` | `str` | Mermaid diagram export |
|
|
787
|
+
|
|
788
|
+
### HarmonicPath (struct)
|
|
789
|
+
|
|
790
|
+
Returned by `Tree.paths()`. Represents a harmonic progression step.
|
|
791
|
+
|
|
792
|
+
| Field | Type | Description |
|
|
793
|
+
|-------|------|-------------|
|
|
794
|
+
| `.id` | `int` | Path identifier |
|
|
795
|
+
| `.branch` | `str` | Target branch name |
|
|
796
|
+
| `.chord` | `Chord` | Resolved chord |
|
|
797
|
+
| `.interval_labels` | `list[str]` | Chord intervals |
|
|
798
|
+
| `.note_names` | `list[str]` | Chord note names |
|
|
799
|
+
|
|
800
|
+
### ChordComparison (struct)
|
|
801
|
+
|
|
802
|
+
Returned by `Chord.compare()`. Absolute (context-free) comparison of two chords.
|
|
803
|
+
|
|
804
|
+
| Field | Type | Description |
|
|
805
|
+
|-------|------|-------------|
|
|
806
|
+
| `.common_notes` | `list[Note]` | Notes present in both chords |
|
|
807
|
+
| `.exclusive_a` | `list[Note]` | Notes only in chord A |
|
|
808
|
+
| `.exclusive_b` | `list[Note]` | Notes only in chord B |
|
|
809
|
+
| `.root_distance` | `int` | Root distance in semitones (0-6, shortest arc) |
|
|
810
|
+
| `.root_direction` | `int` | Signed root direction (-6 to +6) |
|
|
811
|
+
| `.same_quality` | `bool` | Same chord type (M, m, dim, etc.) |
|
|
812
|
+
| `.same_size` | `bool` | Same number of notes |
|
|
813
|
+
| `.common_intervals` | `list[str]` | Interval labels present in both |
|
|
814
|
+
| `.enharmonic` | `bool` | Same pitch class set |
|
|
815
|
+
| `.subset` | `str` | `""`, `"a_subset_of_b"`, `"b_subset_of_a"`, `"equal"` |
|
|
816
|
+
| `.voice_leading` | `int` | Optimal voice pairing in semitones (Tymoczko 2011). -1 if different sizes |
|
|
817
|
+
| `.transformation` | `str` | Neo-Riemannian transformation (Cohn 2012): `""`, `"P"`, `"L"`, `"R"`, `"RP"`, `"LP"`, `"PL"`, `"PR"`, `"LR"`, `"RL"` (triads only) |
|
|
818
|
+
| `.inversion` | `bool` | Same notes, different root |
|
|
819
|
+
| `.interval_vector_a` | `list[int]` | Interval-class vector (Forte 1973): 6 elements counting ic1-6 for chord A |
|
|
820
|
+
| `.interval_vector_b` | `list[int]` | Interval-class vector (Forte 1973) for chord B |
|
|
821
|
+
| `.same_interval_vector` | `bool` | Same vector = Z-relation candidate (Forte 1973) |
|
|
822
|
+
| `.transposition` | `int` | Transposition index T_n (Lewin 1987): 0-11, or -1 if not related |
|
|
823
|
+
| `.dissonance_a` | `float` | Psychoacoustic roughness (Plomp & Levelt 1965 / Sethares 1998) for chord A |
|
|
824
|
+
| `.dissonance_b` | `float` | Psychoacoustic roughness (Plomp & Levelt 1965 / Sethares 1998) for chord B |
|
|
825
|
+
|
|
826
|
+
| Method | Returns | Description |
|
|
827
|
+
|--------|---------|-------------|
|
|
828
|
+
| `.to_dict()` | `dict` | Serialize all fields to a plain Python dict (Notes as strings) |
|
|
829
|
+
|
|
830
|
+
### FieldComparison (struct)
|
|
831
|
+
|
|
832
|
+
Returned by `Field.compare()`. Contextual comparison within a harmonic field.
|
|
833
|
+
|
|
834
|
+
| Field | Type | Description |
|
|
835
|
+
|-------|------|-------------|
|
|
836
|
+
| `.degree_a`, `.degree_b` | `int?` | Scale degree (None if non-diatonic) |
|
|
837
|
+
| `.function_a`, `.function_b` | `HarmonicFunction?` | Harmonic function |
|
|
838
|
+
| `.role_a`, `.role_b` | `str?` | Role within function group |
|
|
839
|
+
| `.degree_distance` | `int?` | Distance between degrees |
|
|
840
|
+
| `.same_function` | `bool?` | Same harmonic function |
|
|
841
|
+
| `.relative` | `bool` | Relative chord pair |
|
|
842
|
+
| `.progression` | `bool` | Reserved for future use |
|
|
843
|
+
| `.root_motion` | `str` | Diatonic root motion (Kostka & Payne): `""`, `"ascending_fifth"`, `"descending_fifth"`, `"ascending_third"`, `"descending_third"`, `"ascending_step"`, `"descending_step"`, `"tritone"`, `"unison"` |
|
|
844
|
+
| `.secondary_dominant` | `str` | Secondary dominant (Kostka & Payne): `""`, `"a_is_V7_of_b"`, `"b_is_V7_of_a"` |
|
|
845
|
+
| `.applied_diminished` | `str` | Applied diminished vii/x (Gauldin 1997): `""`, `"a_is_viidim_of_b"`, `"b_is_viidim_of_a"` |
|
|
846
|
+
| `.diatonic_a`, `.diatonic_b` | `bool` | Belongs to the field |
|
|
847
|
+
| `.borrowed_a`, `.borrowed_b` | `BorrowedInfo?` | Modal borrowing origin |
|
|
848
|
+
| `.pivot` | `list[PivotInfo]` | Keys where both chords have a degree |
|
|
849
|
+
| `.tritone_sub` | `bool` | Tritone substitution (Kostka & Payne): both dom7, roots 6 st apart |
|
|
850
|
+
| `.chromatic_mediant` | `str` | Chromatic mediant (Cohn 2012): `""`, `"upper"`, `"lower"` |
|
|
851
|
+
| `.foreign_a`, `.foreign_b` | `list[Note]` | Notes outside the scale |
|
|
852
|
+
|
|
853
|
+
| Method | Returns | Description |
|
|
854
|
+
|--------|---------|-------------|
|
|
855
|
+
| `.to_dict()` | `dict` | Serialize all fields to a plain Python dict |
|
|
856
|
+
|
|
857
|
+
### FieldMatch (struct)
|
|
858
|
+
|
|
859
|
+
Returned by `Field.deduce()`. Ranked candidate field match.
|
|
860
|
+
|
|
861
|
+
| Field | Type | Description |
|
|
862
|
+
|-------|------|-------------|
|
|
863
|
+
| `.field` | `Field` | Candidate field |
|
|
864
|
+
| `.score` | `float` | Match ratio (0.0β1.0) |
|
|
865
|
+
| `.matched` | `int` | Number of matched items |
|
|
866
|
+
| `.total` | `int` | Total items in input |
|
|
867
|
+
| `.roles` | `list[str]` | Roles for each item (Roman numerals) |
|
|
868
|
+
|
|
869
|
+
| Method | Returns | Description |
|
|
870
|
+
|--------|---------|-------------|
|
|
871
|
+
| `.to_dict()` | `dict` | Serialize to dict |
|
|
872
|
+
|
|
873
|
+
### BorrowedInfo (struct)
|
|
874
|
+
|
|
875
|
+
| Field | Type | Description |
|
|
876
|
+
|-------|------|-------------|
|
|
877
|
+
| `.scale_type` | `str` | Origin scale type ("NaturalMinor", etc.) |
|
|
878
|
+
| `.degree` | `int` | Degree in that scale |
|
|
879
|
+
| `.function` | `HarmonicFunction` | Function in that scale |
|
|
880
|
+
| `.role` | `str` | Role in that scale |
|
|
881
|
+
|
|
882
|
+
| Method | Returns | Description |
|
|
883
|
+
|--------|---------|-------------|
|
|
884
|
+
| `.to_dict()` | `dict` | Serialize to dict (function as string name) |
|
|
885
|
+
|
|
886
|
+
### PivotInfo (struct)
|
|
887
|
+
|
|
888
|
+
| Field | Type | Description |
|
|
889
|
+
|-------|------|-------------|
|
|
890
|
+
| `.tonic` | `str` | Tonic of the pivot key |
|
|
891
|
+
| `.scale_type` | `str` | Scale type |
|
|
892
|
+
| `.degree_a` | `int` | Degree of chord A in that key |
|
|
893
|
+
| `.degree_b` | `int` | Degree of chord B in that key |
|
|
894
|
+
|
|
895
|
+
| Method | Returns | Description |
|
|
896
|
+
|--------|---------|-------------|
|
|
897
|
+
| `.to_dict()` | `dict` | Serialize to dict |
|
|
898
|
+
|
|
899
|
+
### HarmonicFunction (enum)
|
|
900
|
+
|
|
901
|
+
| Property | Returns | Description |
|
|
902
|
+
|----------|---------|-------------|
|
|
903
|
+
| `.name` | `str` | Full name: "Tonic", "Subdominant", "Dominant" |
|
|
904
|
+
| `.short` | `str` | Abbreviation: "T", "S", "D" |
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## Rhythm & Time
|
|
909
|
+
|
|
910
|
+
Gingo models rhythm with first-class objects that match standard music notation.
|
|
911
|
+
|
|
912
|
+
### Duration
|
|
913
|
+
|
|
914
|
+
Durations can be created by name (e.g., `quarter`, `eighth`) or as rational values. Dots and tuplets are built in.
|
|
915
|
+
|
|
916
|
+
```python
|
|
917
|
+
from gingo import Duration
|
|
918
|
+
|
|
919
|
+
Duration("quarter")
|
|
920
|
+
Duration("eighth", dots=1) # dotted eighth
|
|
921
|
+
Duration("eighth", tuplet=3) # triplet eighth
|
|
922
|
+
Duration(3, 16) # 3/16
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### Tempo (nomos de tempo)
|
|
926
|
+
|
|
927
|
+
Tempo accepts either BPM or traditional tempo markings (nomos/nomes de tempo) such as Allegro or Adagio, and converts between them.
|
|
928
|
+
|
|
929
|
+
```python
|
|
930
|
+
from gingo import Tempo, Duration
|
|
931
|
+
|
|
932
|
+
Tempo(120).marking() # "Allegretto"
|
|
933
|
+
Tempo("Adagio").bpm() # 60
|
|
934
|
+
Tempo("Allegro").seconds(Duration("quarter"))
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
### Time Signature
|
|
938
|
+
|
|
939
|
+
Time signatures provide beats-per-bar, beat unit, classification, and bar duration.
|
|
940
|
+
|
|
941
|
+
```python
|
|
942
|
+
from gingo import TimeSignature, Tempo
|
|
943
|
+
|
|
944
|
+
ts = TimeSignature(6, 8)
|
|
945
|
+
ts.classification() # "compound"
|
|
946
|
+
ts.bar_duration().beats()
|
|
947
|
+
Tempo(120).seconds(ts.bar_duration())
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Sequence & Events
|
|
951
|
+
|
|
952
|
+
Build a timeline of note/chord events with a tempo and time signature. Sequences can be transposed and played back.
|
|
953
|
+
|
|
954
|
+
```python
|
|
955
|
+
from gingo import (
|
|
956
|
+
Sequence, Tempo, TimeSignature, NoteEvent, ChordEvent, Rest,
|
|
957
|
+
Note, Chord, Duration,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
seq = Sequence(Tempo(96), TimeSignature(4, 4))
|
|
961
|
+
seq.add(NoteEvent(Note("C"), Duration("quarter"), octave=4))
|
|
962
|
+
seq.add(ChordEvent(Chord("G7"), Duration("half"), octave=4))
|
|
963
|
+
seq.add(Rest(Duration("quarter")))
|
|
964
|
+
seq.total_seconds()
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
CLI helpers for rhythm:
|
|
968
|
+
|
|
969
|
+
- `gingo duration quarter --tempo 120`
|
|
970
|
+
- `gingo tempo Allegro --all`
|
|
971
|
+
- `gingo timesig 6 8 --tempo 120`
|
|
972
|
+
|
|
973
|
+
---
|
|
974
|
+
|
|
975
|
+
## Audio & Playback
|
|
976
|
+
|
|
977
|
+
Any musical object can be rendered to audio with `.play()` or `.to_wav()` (monophonic synthesis). Playback uses `simpleaudio` when available; install the optional dependency with `pip install gingo[audio]` for the best cross-platform experience.
|
|
978
|
+
|
|
979
|
+
```python
|
|
980
|
+
from gingo import Note, Chord, Scale
|
|
981
|
+
|
|
982
|
+
Note("C").play(waveform="sine")
|
|
983
|
+
Chord("Am7").play(waveform="square", strum=0.04)
|
|
984
|
+
Scale("C", "major").to_wav("c_major.wav", waveform="triangle")
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
CLI audio flags are available on `note`, `chord`, `scale`, and `field`:
|
|
988
|
+
|
|
989
|
+
- `--play` outputs to speakers
|
|
990
|
+
- `--wav FILE` exports a WAV file
|
|
991
|
+
- `--waveform sine|square|sawtooth|triangle`
|
|
992
|
+
- `--strum` and `--gap` for timing feel
|
|
993
|
+
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
## Architecture
|
|
997
|
+
|
|
998
|
+
```
|
|
999
|
+
gingo/
|
|
1000
|
+
βββ cpp/ # C++17 core library
|
|
1001
|
+
β βββ include/gingo/ # Public headers
|
|
1002
|
+
β β βββ note.hpp # Note class
|
|
1003
|
+
β β βββ interval.hpp # Interval class
|
|
1004
|
+
β β βββ chord.hpp # Chord class (42 formulas)
|
|
1005
|
+
β β βββ scale.hpp # Scale class (10 families, modes, pentatonic)
|
|
1006
|
+
β β βββ field.hpp # Harmonic field
|
|
1007
|
+
β β βββ tree.hpp # Harmonic tree (beta)
|
|
1008
|
+
β β βββ duration.hpp # Duration class (rhythm)
|
|
1009
|
+
β β βββ tempo.hpp # Tempo class (BPM + markings)
|
|
1010
|
+
β β βββ time_signature.hpp # TimeSignature class
|
|
1011
|
+
β β βββ event.hpp # NoteEvent, ChordEvent, Rest
|
|
1012
|
+
β β βββ sequence.hpp # Sequence class (timeline)
|
|
1013
|
+
β β βββ gingo.hpp # Umbrella include
|
|
1014
|
+
β β βββ internal/ # Internal infrastructure
|
|
1015
|
+
β β βββ types.hpp # TypeElement, TypeVector, TypeTable
|
|
1016
|
+
β β βββ table.hpp # Lookup table class
|
|
1017
|
+
β β βββ data_ops.hpp # rotate, spread, spin operations
|
|
1018
|
+
β β βββ notation_utils.hpp # Formal notation helpers
|
|
1019
|
+
β β βββ lookup_data.hpp # Singleton with all music data
|
|
1020
|
+
β β βββ lookup_tree.hpp # Singleton with tree data
|
|
1021
|
+
β β βββ mode_data.hpp # Mode metadata
|
|
1022
|
+
β βββ src/ # All implementations
|
|
1023
|
+
βββ bindings/
|
|
1024
|
+
β βββ pybind_module.cpp # pybind11 Python bridge
|
|
1025
|
+
βββ python/gingo/
|
|
1026
|
+
β βββ __init__.py # Public API re-exports
|
|
1027
|
+
β βββ __init__.pyi # Type stubs (PEP 561)
|
|
1028
|
+
β βββ __main__.py # CLI entry point
|
|
1029
|
+
β βββ audio.py # Audio playback (requires simpleaudio)
|
|
1030
|
+
β βββ py.typed # PEP 561 marker
|
|
1031
|
+
βββ tests/
|
|
1032
|
+
β βββ cpp/ # Catch2 test suite
|
|
1033
|
+
β βββ python/ # pytest suite
|
|
1034
|
+
βββ CMakeLists.txt # CMake build system
|
|
1035
|
+
βββ pyproject.toml # scikit-build-core packaging
|
|
1036
|
+
βββ MANIFEST.in # Source distribution manifest
|
|
1037
|
+
βββ .github/workflows/
|
|
1038
|
+
βββ ci.yml # Cross-platform CI
|
|
1039
|
+
βββ publish.yml # PyPI publishing via cibuildwheel
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
**Design decisions:**
|
|
1043
|
+
|
|
1044
|
+
- **C++ core** β All music theory computation runs in compiled C++17 for performance. This is critical for real-time MIDI, FFT, and machine learning workloads.
|
|
1045
|
+
- **pybind11 bridge** β Exposes the C++ types to Python with zero-copy where possible and full type stub support for IDE autocompletion.
|
|
1046
|
+
- **Lazy computation** β Chord notes, scale notes, and formal spellings are computed on first access and cached internally using mutable fields.
|
|
1047
|
+
- **Meyer's singleton** β All lookup data (enharmonic maps, chord formulas, scale masks) is initialized once on first use, with no manual setup required.
|
|
1048
|
+
- **Domain types over generic tables** β Instead of the original generic `Table` data structure, the new API uses dedicated `Note`, `Interval`, `Chord`, `Scale`, and `Field` types with clear, discoverable methods.
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
## Building from Source
|
|
1053
|
+
|
|
1054
|
+
### Python package
|
|
1055
|
+
|
|
1056
|
+
```bash
|
|
1057
|
+
pip install -v .
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
This triggers scikit-build-core, which runs CMake, compiles the C++ core, links the pybind11 module, and installs the Python package.
|
|
1061
|
+
|
|
1062
|
+
### C++ only
|
|
1063
|
+
|
|
1064
|
+
```bash
|
|
1065
|
+
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
|
1066
|
+
cmake --build build
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
### C++ with tests
|
|
1070
|
+
|
|
1071
|
+
```bash
|
|
1072
|
+
cmake -B build -DGINGO_BUILD_TESTS=ON
|
|
1073
|
+
cmake --build build
|
|
1074
|
+
cd build && ctest --output-on-failure
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
### Run Python tests
|
|
1078
|
+
|
|
1079
|
+
```bash
|
|
1080
|
+
pip install -v ".[test]"
|
|
1081
|
+
pytest tests/python -v
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
---
|
|
1085
|
+
|
|
1086
|
+
## Contributing
|
|
1087
|
+
|
|
1088
|
+
1. Fork the repository
|
|
1089
|
+
2. Create a feature branch
|
|
1090
|
+
3. Make your changes
|
|
1091
|
+
4. Run both C++ and Python test suites
|
|
1092
|
+
5. Submit a pull request
|
|
1093
|
+
|
|
1094
|
+
---
|
|
1095
|
+
|
|
1096
|
+
## License
|
|
1097
|
+
|
|
1098
|
+
MIT
|