gingo 1.0.0__cp311-cp311-win32.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.
@@ -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