Concepts¶
The model behind tonal_py is small and consistent. This page walks through
the foundations so the API stops feeling like trivia.
Pitch and pitch coordinates¶
A pitch is a tuple of four ints:
| Field | Meaning |
|---|---|
step |
0=C, 1=D, 2=E, 3=F, 4=G, 5=A, 6=B |
alt |
Alterations: -2=bb, -1=b, 0=natural, 1=#, 2=## |
oct |
Octave (None when this is a pitch class) |
dir |
Interval direction: 1 ascending, -1 descending. None for notes. |
>>> from tonal_py import Pitch
>>> p = Pitch(step=0, alt=0, oct=4) # C4
>>> p.step, p.alt, p.oct
(0, 0, 4)
The clever bit is coordinates on the circle of fifths:
>>> from tonal_py.core import coordinates
>>> coordinates(Pitch(step=0, alt=0, oct=4)) # C4
(0, 4)
>>> coordinates(Pitch(step=4, alt=0, oct=4)) # G4 (one fifth up)
(1, 4)
>>> coordinates(Pitch(step=3, alt=1, oct=4)) # F#4 (six fifths up)
(6, 3)
Why this matters: transposition becomes vector addition. Adding the coordinates of an interval to the coordinates of a note gives you the transposed note's coordinates — no string parsing in the inner loop.
This is the JS implementation faithfully ported, and it's why every
transposition operation in tonal_py is fast and exact (no floating point,
no enharmonic ambiguity).
Chroma¶
A chroma is a pitch class number 0-11 (C=0, C#=1, D=2, ...). It's octave-agnostic.
>>> from tonal_py import Note
>>> Note.chroma("C4")
0
>>> Note.chroma("D5")
2
>>> Note.chroma("Bb3")
10
Chroma is the basis for pitch class sets (see below) and for enharmonic equivalence (Db and C# both have chroma 1).
Pitch class sets (Pcset)¶
A pitch class set is the set of chromas present in a collection of notes, represented compactly as a 12-character binary string:
>>> from tonal_py import Pcset
>>> Pcset.chroma(["C", "E", "G"]) # C major triad
'100010010000'
>>> Pcset.chroma(["C", "D", "E", "F", "G", "A", "B"]) # C major scale
'101011010101'
This representation lets us compare scales and chords with simple bitwise
operations. The set_num field stores the chroma as a decimal int 0-4095:
>>> p = Pcset.get(["C", "E", "G"])
>>> p.set_num
2192
>>> bin(p.set_num)[2:].zfill(12) # back to chroma
'100010010000'
Subset/superset queries are bitwise AND/OR under the hood:
>>> in_cmaj = Pcset.is_subset_of(["C", "E", "G"])
>>> in_cmaj(["C", "E"]) # CE ⊂ CEG
True
>>> in_cmaj(["C", "E", "G", "B"]) # CEGB ⊄ CEG (extra B)
False
This is also how Chord.detect, Scale.detect, Chord.chord_scales, and
Scale.scale_chords work: convert input to a chroma, then look up matching
entries in the chord/scale dictionary.
Intervals¶
An interval has a number (1-15+) and a quality (P, M, m, A, d, AA, dd, etc.):
>>> from tonal_py import Interval
>>> i = Interval.get("M3")
>>> i.num, i.q, i.semitones
(3, 'M', 4)
>>> i.coord # circle-of-fifths coords (fifths, octaves)
(4, -2)
>>> i.dir # 1 = ascending
1
Tonal supports both naming orders ("3M" and "M3") for the same interval — and the parser silently chooses one canonical name:
Quality rules differ by step:
- Perfectable intervals (1, 4, 5, 8, 11, 12): qualities
P,A,d,AA,dd... - Majorable intervals (2, 3, 6, 7, 9, 10, 13, 14): qualities
M,m,A,d,AA,dd...
That's why Interval.get("3P") returns an empty interval — 3rds aren't
perfectable.
Modes¶
There are 7 diatonic modes, each a rotation of the major scale:
>>> from tonal_py import Mode
>>> Mode.names()
['ionian', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'aeolian', 'locrian']
>>> Mode.get("dorian").alt # 2 sharps from ionian on the circle of fifths
2
The alt field is the mode's distance from ionian on the circle of fifths.
That's how Mode.distance and Mode.relative_tonic work — pure fifths math.
Keys¶
A key is a tonic plus a quality (major or minor) plus everything that follows: scale notes, diatonic chords, harmonic functions, secondary dominants, substitute dominants.
>>> from tonal_py import Key
>>> ck = Key.major_key("C")
>>> list(ck.scale)
['C', 'D', 'E', 'F', 'G', 'A', 'B']
>>> list(ck.chords)
['Cmaj7', 'Dm7', 'Em7', 'Fmaj7', 'G7', 'Am7', 'Bm7b5']
>>> list(ck.chords_harmonic_function)
['T', 'SD', 'T', 'SD', 'D', 'T', 'D']
Functions are abbreviated T (tonic), SD (sub-dominant), D
(dominant). Minor keys also expose natural, harmonic, and melodic
sub-scales each with their own chord and function arrays.
Roman numerals¶
Roman numerals connect chord names to scale degrees in a key:
>>> from tonal_py import RomanNumeral
>>> RomanNumeral.get("V").interval
'5P'
>>> RomanNumeral.get("bVII").interval
'7m'
>>> RomanNumeral.get("iv").major
False
The case (uppercase vs lowercase) encodes major vs minor. The optional
chord-type suffix is preserved for use by Progression:
How the layers fit together¶
Pitch (step, alt, oct, dir)
↓
coord ↔ Pitch transformations (circle of fifths)
↓
Note Interval (pitch-note + pitch-interval parsers)
↓ ↓
note() / interval() ← Note.get() / Interval.get()
↓ ↓
Pcset (chroma + binary set ops)
↓
ChordType / ScaleType (data tables)
↓
Chord / Scale / Mode / Key / RomanNumeral (high-level APIs)
↓
Voicing / Progression / VoiceLeading (higher-level applications)
Read the API Reference once you have the model. Each namespace's page is generated from its source docstrings.