Use whenever dealing with anything requiring jazz music theory or harmony. Covers pitch class arithmetic, chord-scale theory, chord tones/guide tones/tensions, context-aware scale resolution (V7 to minor, tritone subs, ii-V), voice-leading, walking bass construction, and swing timing. Essential for the harmony, exercises, and backing modules.
Reference for the jazz theory concepts used throughout the codebase. Apply this knowledge when implementing or modifying the harmony analyzer, exercise engine, walking bass generator, or any module that reasons about chords, scales, or voice-leading.
All pitch reasoning in this project uses pitch classes (integers 0-11) and modulo-12 arithmetic. No music21 or heavy theory libraries.
C=0 C#/Db=1 D=2 D#/Eb=3 E=4 F=5 F#/Gb=6 G=7 G#/Ab=8 A=9 A#/Bb=10 B=11
Enharmonic equivalents map to the same integer: F# and Gb are both 6.
pitch_class = midi_note % 12
# MIDI 60 (middle C) → 60 % 12 = 0 → C
# MIDI 64 (E4) → 64 % 12 = 4 → E
interval = (note_pc - root_pc) % 12
# From C (0) to E (4): (4 - 0) % 12 = 4 → major 3rd
# From A (9) to C (0): (0 - 9) % 12 = 3 → minor 3rd
To find all MIDI notes belonging to a scale, iterate MIDI 21-108 and check membership:
def get_scale_midi_notes(root_pc: int, intervals: tuple[int, ...],
low: int = 21, high: int = 108) -> list[int]:
return [m for m in range(low, high + 1) if (m - root_pc) % 12 in intervals]
Each scale is a tuple of semitone intervals from the root. These are the scales used in the project:
| Scale | Intervals | Degrees | Notes (from C) |
|---|---|---|---|
| Ionian (major) | (0,2,4,5,7,9,11) | 1 2 3 4 5 6 7 | C D E F G A B |
| Dorian | (0,2,3,5,7,9,10) | 1 2 b3 4 5 6 b7 | C D Eb F G A Bb |
| Phrygian | (0,1,3,5,7,8,10) | 1 b2 b3 4 5 b6 b7 | C Db Eb F G Ab Bb |
| Lydian | (0,2,4,6,7,9,11) | 1 2 3 #4 5 6 7 | C D E F# G A B |
| Mixolydian | (0,2,4,5,7,9,10) | 1 2 3 4 5 6 b7 | C D E F G A Bb |
| Aeolian (natural minor) | (0,2,3,5,7,8,10) | 1 2 b3 4 5 b6 b7 | C D Eb F G Ab Bb |
| Locrian | (0,1,3,5,6,8,10) | 1 b2 b3 4 b5 b6 b7 | C Db Eb F Gb Ab Bb |
| Harmonic minor | (0,2,3,5,7,8,11) | 1 2 b3 4 5 b6 7 | C D Eb F G Ab B |
| Locrian ♮6 | (0,1,3,5,6,9,10) | 1 b2 b3 4 b5 6 b7 | C Db Eb F Gb A Bb |
| Phrygian dominant | (0,1,4,5,7,8,10) | 1 b2 3 4 5 b6 b7 | C Db E F G Ab Bb |
| Melodic minor | (0,2,3,5,7,9,11) | 1 2 b3 4 5 6 7 | C D Eb F G A B |
| Locrian ♮9 | (0,2,3,5,6,8,10) | 1 2 b3 4 b5 b6 b7 | C D Eb F Gb Ab Bb |
| Lydian dominant | (0,2,4,6,7,9,10) | 1 2 3 #4 5 6 b7 | C D E F# G A Bb |
| Altered | (0,1,3,4,6,8,10) | 1 b2 #2 3 #4 b6 b7 | C Db D# E F# Ab Bb |
| Half-whole diminished | (0,1,3,4,6,7,9,10) | 1 b2 #2 3 #4 5 6 b7 | C Db D# E F# G A Bb |
| Whole-half diminished | (0,2,3,5,6,8,9,11) | 1 2 b3 4 b5 b6 6 7 | C D Eb F Gb Ab A B |
| Whole-tone | (0,2,4,6,8,10) | 1 2 3 #4 #5 b7 | C D E F# G# Bb |
Symmetric scales (diminished, whole-tone) have 6 or 8 notes instead of 7. The half-whole and whole-half diminished scales are inversions of each other: half-whole starts with a half step (used over dominant chords), whole-half starts with a whole step (used over diminished chords).
| Mode | Parent Relationship | Used Over |
|---|---|---|
| Harmonic minor | 1st mode | minmaj7 |
| Locrian ♮6 | 2nd mode of harmonic minor | hdim7 (natural 13 = natural 6, idiomatic for ii of minor ii-V-i) |
| Phrygian dominant | 5th mode of harmonic minor | V7 → minor |
Several important jazz scales are modes of the melodic minor scale:
| Mode | Parent Relationship | Used Over |
|---|---|---|
| Melodic minor | 1st mode | minmaj7 (alternative — brighter, no b6) |
| Lydian dominant | 4th mode of melodic minor | Tritone subs, 7(#11) |
| Altered | 7th mode of melodic minor | V7(#9), V7(b9,b13) |
Locrian ♮9 (locrian_nat9) | 6th mode of melodic minor | hdim7 standalone (natural 9 but b5 and b13; used when hdim7 is NOT ii° of a minor ii°-V) |
This is Layer 1 of the harmony analyzer — the fallback when no context rule applies:
| Quality | Default Scale | Reasoning |
|---|---|---|
maj7 | Ionian | Standard major sound |
maj | Ionian | Major triad |
6 | Ionian | Major with added 6th |
maj9 | Ionian | Major 9th |
maj13 | Ionian | Fully voiced major chord, natural 13 confirms Ionian |
maj69 | Ionian | Major 6/9 — no 7th, but 6 and 9 both diatonic to Ionian |
maj7#11 | Lydian | #11 is the defining note of Lydian; often the IV chord in a major context |
7 | Mixolydian | Plain dominant |
9 | Mixolydian | Dominant 9th |
sus4 | Mixolydian | Sus4 functions as a dominant suspension |
sus2 | Mixolydian | Same family as sus4 |
sus | Mixolydian | Generic suspended (implies sus4) |
7sus4 | Mixolydian | Dominant 7th with sus4 — unresolved dominant, same scale as plain 7 |
min7 | Dorian | Default minor color in jazz (brighter than Aeolian due to natural 6) |
min | Dorian | Minor triad |
min6 | Dorian | Minor with natural 6 = Dorian |
min9 | Dorian | Minor 9th |
min11 | Dorian | Minor 11th; natural 11 is diatonic to Dorian |
min13 | Dorian | Minor 13th; natural 13 confirms Dorian (not Aeolian's b6) |
minmaj7 | Harmonic minor | The natural 7 over minor = harmonic minor (raised 7th, b6 included) |
hdim7 | Locrian ♮6 | Half-diminished = m7b5; natural 13 is idiomatic when functioning as ii of minor ii-V-i |
dim7 | Whole-half diminished | Symmetric, each chord tone can be a root |
aug | Whole-tone | Augmented triad is a whole-tone subset |
When a chord has parenthesized extensions, they override the default scale:
| Quality + Extension | Scale | Why |
|---|---|---|
7(b9) | Phrygian dominant | b9 = b13 of harmonic minor V; implies minor resolution |
7(b13) | Phrygian dominant | b13 = b6 of harmonic minor V; same implication as b9 |
7(b9,b13) | Phrygian dominant | Both b9 and b13 = harmonic minor V (explicit confirmation) |
7(b9,13) | Half-whole diminished | Natural 13 with b9 distinguishes HW dim from Phrygian dominant (which has b13) |
7(#9) | Altered | #9 signals altered dominant |
7(b9,#9) | Altered | Both altered 9ths = fully altered dominant |
7(#5) | Whole-tone | #5 = augmented dominant, whole-tone contains #5 |
7(#11) | Lydian dominant | #11 is the defining note of Lydian dominant |
7(b5) | Lydian dominant | b5 is enharmonically #11 (seen in Take the A Train: D:7(b5)) |
7(13) | Mixolydian | Natural 13 confirms standard Mixolydian |
The essential notes that define the chord (root, 3rd, 5th, 7th), expressed as semitone intervals from the root:
| Quality | Root | 3rd | 5th | 7th | Intervals |
|---|---|---|---|---|---|
maj7 | 0 | 4 | 7 | 11 | (0, 4, 7, 11) |
7 | 0 | 4 | 7 | 10 | (0, 4, 7, 10) |
min7 | 0 | 3 | 7 | 10 | (0, 3, 7, 10) |
hdim7 | 0 | 3 | 6 | 10 | (0, 3, 6, 10) |
dim7 | 0 | 3 | 6 | 9 | (0, 3, 6, 9) |
minmaj7 | 0 | 3 | 7 | 11 | (0, 3, 7, 11) |
maj | 0 | 4 | 7 | — | (0, 4, 7) |
min | 0 | 3 | 7 | — | (0, 3, 7) |
aug | 0 | 4 | 8 | — | (0, 4, 8) |
6 | 0 | 4 | 7 | 9 | (0, 4, 7, 9) (6th replaces 7th) |
min6 | 0 | 3 | 7 | 9 | (0, 3, 7, 9) |
The 3rd and 7th of each chord. These two notes:
def get_guide_tones(root_pc: int, quality: str) -> tuple[int, int]:
"""Return (3rd_pc, 7th_pc) for the chord."""
third = (root_pc + CHORD_TONES[quality][1]) % 12 # 3 or 4 semitones
seventh = (root_pc + CHORD_TONES[quality][3]) % 12 # 9, 10, or 11 semitones
return (third, seventh)
For triads without a 7th (maj, min, aug), the guide tone is just the 3rd.
Scale tones that are NOT chord tones. These add color without clashing:
| Quality | Tensions | Intervals |
|---|---|---|
maj7 (Ionian) | 9, 13 | 2, 9 (avoid 4/11 — half step above 3rd) |
7 (Mixolydian) | 9, 13 | 2, 9 |
min7 (Dorian) | 9, 11, 13 | 2, 5, 9 |
hdim7 (Locrian ♮6) | 11, 13 | 5, 9 (avoid b2/b9 — half step above root) |
Avoid notes: Scale tones a half step above a chord tone create dissonance when sustained. In Ionian, the 4th (F over Cmaj7) is an avoid note because it's a half step above the 3rd (E). The projection doesn't distinguish avoid notes — all scale tones are shown — but this matters for the walking bass and exercise target note selection.
These rules override the default scale when the harmonic context is clear. Each rule examines (prev_chord, current_chord, next_chord).
Pattern: X:7 → Y:<minor> where X is a 5th above Y (i.e. (X_root - Y_root) % 12 == 7), and <minor> is any quality starting with min (i.e. min7, min, minmaj7, min6, min9, min11, min13, etc.).
Scale: Phrygian dominant (5th mode of harmonic minor)
Why: The b9 and b13 of Phrygian dominant are the characteristic tones of a V7 in minor. Mixolydian (the default) has a natural 9 and 13, which sound like a major-key dominant.
G:7 → C:min7 → G Phrygian dominant (G Ab B C D Eb F)
= C harmonic minor starting on G
Detection: (current_chord.root_pc - next_chord.root_pc) % 12 == 7 AND next_chord.quality.startswith("min").
Pattern: X:7 → Y where X is a half step above Y (root moves down by 1 semitone).
Scale: Lydian dominant
Why: A tritone sub replaces a V7 with a dominant chord a tritone away. Lydian dominant has a #11, which is the enharmonic equivalent of the original dominant's root — it connects the two keys smoothly.
Db:7 → C:maj7 → Db Lydian dominant (Db Eb F G Ab Bb Cb)
The G (= #11 of Db) is the 5th of C, linking the resolution
Detection: (current_chord.root_pc - next_chord.root_pc) % 12 == 1 AND current_chord.quality == "7" (exact match — excludes 7sus4 slash-chord spellings from tritone sub detection).
Pattern: A chain of chords whose roots each ascend by P4 (5 semitones), ending with a ii-V pair. Common forms:
vi-ii-V: A:min7 → D:min7 → G:7 (roots: 9→2→7, each +5 mod 12)
iii-vi-ii-V: E:min7 → A:min7 → D:min7 → G:7
iii-VI7-ii-V: E:min7 → A:7 → D:min7 → G:7 (A:7 is a secondary dominant V/ii)
Scale: All chords in the chain share the key center of the final resolution target. Each chord gets the diatonic mode for its degree — this overrides the Layer 1 default of Dorian for iii and vi:
| Degree | Example (in C) | Scale | Layer 1 default |
|---|---|---|---|
| iii | E:min7 | Phrygian | |
| vi | A:min7 | Aeolian | |
| ii | D:min7 | Dorian | Dorian (no change) |
| V | G:7 | Mixolydian | Mixolydian (no change) |
Secondary dominants (7 chords within the chain that resolve to a minor chord) get Phrygian dominant via Rule 1.
Why: This is a descending-fifths cycle — the most common harmonic motion in jazz. Recognizing the full chain (not just isolated ii-V pairs) helps identify secondary dominants and confirms the key center for all chords.
Detection: Identify a ii-V pair (a min* chord followed by a 7* chord with roots a P4 apart), then walk backwards. Each preceding chord is part of the chain if (current_chord.root_pc - prev_chord.root_pc) % 12 == 5 AND prev_chord.quality.startswith("min") or prev_chord.quality.startswith("7").
Pattern: W:maj7 → X:min7 → Y:min7 → Z:7 where the I chord's root is a minor 3rd above the vi chord's root, followed by a descending-fifths chain (Rule 3).
C:maj7 → A:min7 → D:min7 → G:7 (I-vi-ii-V in C major)
Scale: Same key-center logic as Rule 3. The I chord confirms the key unambiguously:
| Degree | Example (in C) | Scale | Layer 1 default |
|---|---|---|---|
| I | C:maj7 | Ionian | Ionian (no change) |
| vi | A:min7 | Aeolian | |
| ii | D:min7 | Dorian | Dorian (no change) |
| V | G:7 | Mixolydian | Mixolydian (no change) |
Why: The I-vi-ii-V turnaround is one of the most common progressions in jazz standards. The I chord anchors the key center, making it the strongest confirmation that vi should get Aeolian (not the default Dorian).
Detection: A Rule 3 chain is preceded by a chord where prev_chord.quality.startswith("maj") AND (prev_chord.root_pc - current_chord.root_pc) % 12 == 3. The I → vi root motion is a minor 3rd down (3 semitones), unlike the P4 motion within the chain.
Pattern: X:maj7 where the previous chord or key context suggests X is the IV degree.
Scale: Lydian (instead of default Ionian)
Why: The #4 of Lydian avoids the clash between the natural 4 and the major 3rd of the chord. When a maj7 chord is clearly functioning as a IV chord, Lydian is the idiomatic jazz choice.
Detection: (current_chord.root_pc - prev_chord.root_pc) % 12 == 5 AND both prev_chord.quality.startswith("maj") AND current_chord.quality.startswith("maj").
Pattern: X:hdim7 where the next chord is NOT dominant-function (its default scale ∉ {mixolydian, lydian_dominant, phrygian_dom, altered}).
Scale: Locrian ♮9 (locrian_nat9)
Why: When hdim7 is not resolving to a dominant (not acting as ii° in a minor ii°–V), the natural 9 is idiomatic. Contrast:
hdim7 → dominant → Locrian ♮6 (natural 13, ii° of minor ii°-V-i)hdim7 standalone → Locrian ♮9 (natural 9, 6th mode of melodic minor)Detection: current_chord.quality.startswith("hdim") AND next_chord is None or its default scale is not in DOMINANT_SCALES.
When multiple rules could apply, use this priority:
b9, #9, etc.) — always winThe harmony analyzer pre-computes a guide-tone line across the entire form. At each chord transition, pick the guide tone (3rd or 7th) that is closest in pitch to the previous guide tone.
Dm7 → G7 → Cmaj7
3rd=F 3rd=B 3rd=E
7th=C 7th=F 7th=B
Guide-tone line: F → F → E (7th of G7 = F, same as 3rd of Dm7; then 3rd of Cmaj7 = E, half step down)
Key voice-leading connections in jazz:
This is what makes ii-V-I progressions sound smooth — the guide tones descend by half step.
def compute_guide_tone_line(chords: list[ChordEvent]) -> list[int]:
"""Return one MIDI note per chord: the voice-led guide tone."""
line = []
prev_note = None
for chord in chords:
third_pc, seventh_pc = get_guide_tones(chord.root_pc, chord.quality)
# Find closest octave placement of each candidate to prev_note
candidates = find_nearest_midi(third_pc, prev_note) + find_nearest_midi(seventh_pc, prev_note)
if prev_note is None:
chosen = pick_from_middle_register(candidates) # start in a comfortable range
else:
chosen = min(candidates, key=lambda n: abs(n - prev_note))
line.append(chosen)
prev_note = chosen
return line
A walking bass line plays one note per beat (quarter notes) and outlines the harmony while creating melodic motion.
Strong beats (1 and 3): Chord tones — root, 3rd, 5th, 7th. Beat 1 strongly prefers the root.
Weak beats (2 and 4): Scale tones, chromatic approach notes, or passing tones connecting the strong beats.
Beat 4 → Beat 1 (next chord): The most important connection. Beat 4 should approach the root of the next chord by:
Bass range: MIDI 28 (E1) to MIDI 48 (C3). This matches the range of an upright bass. Notes should generally move by step or small skip (2nds or 3rds), with occasional larger leaps (4ths, 5ths) for variety.
For each bar with chord X:
When a chord lasts only 2 beats:
In swing feel, each beat is divided into two unequal parts instead of two equal eighth notes. The first eighth is longer, the second is shorter — like a triplet with the first two notes tied:
Straight: |-----|-----| (50/50 split)
Swing: |-------|---| (67/33 split, triplet feel)
The swing_ratio parameter controls the split:
0.50 = straight (no swing) — each eighth note is exactly 50% of a beat0.67 = triplet swing (default) — the downbeat eighth is 67% of a beat, the upbeat is 33%0.75 = hard swing — exaggerated shuffle feelSwing is applied during event generation, not during playback. The timeline stays in straight time (chord changes happen on beat boundaries). Only the backing track events (bass notes, drum hits on the "and" of beats) get their timing shifted:
def apply_swing(beat_position: float, swing_ratio: float = 0.67) -> float:
"""Shift offbeat eighth notes forward in time."""
beat_fraction = beat_position % 1.0
if abs(beat_fraction - 0.5) < 0.01: # this is an offbeat ("and")
beat_int = int(beat_position)
return beat_int + swing_ratio # shift the "and" later
return beat_position
SYMBOL = ROOT ":" QUALITY [EXTENSION] [BASS]
ROOT = C | C# | Db | D | D# | Eb | E | F | F# | Gb | G | G# | Ab | A | A# | Bb | B
QUALITY = <string> — matched by prefix family (see Parsing Strategy below)
EXTENSION = "(" ALTERATION {"," ALTERATION} ")"
ALTERATION = b5 | b6 | #5 | b9 | #9 | #11 | b13 | 13
BASS = "/" ROOT
: to separate root from the rest\(([^)]+)\)/[A-G][#b]?$| Prefix | Family | Examples |
|---|---|---|
hdim | half-diminished | hdim7 |
min | minor | min7, min, minmaj7, min6, min(b6) |
maj | major | maj7, maj, maj9, maj7#11 |
dim | diminished | dim7 |
aug | augmented | aug |
sus | suspended | sus4, sus2, sus |
7, 9, 11, 13 | dominant | 7, 9, 7sus4, 13 |
No prefix collisions exist in the current set, but check in the order listed for clarity. Within a family, use the full quality string to look up the exact default scale from Layer 1; fall back to the family's base quality if no exact match.
The parser must handle both sharp and flat spellings. Both F#:min7 and Gb:min7 refer to the same pitch class (6). Normalize to a canonical spelling when computing pitch classes, but preserve the original spelling for display.
Two patterns are recognized and reclassified as 7sus4 rooted on the bass note:
| Pattern | Interval condition | Example | Becomes |
|---|---|---|---|
| Major upper, bass a whole step below | (bass_pc - root_pc) % 12 == 2, quality not "7*" | Ab:maj/Bb | Bb:7sus4 |
| Minor upper, root a P5 above bass | quality.startswith("min") AND (root_pc - bass_pc) % 12 == 7 | E:min/A | A:7sus4 |
The quality not startswith("7") guard on the first pattern prevents mis-detecting E:7/F# as a dominant sus4. After reclassification, all chord tones and scale notes use the bass note as root, Mixolydian scale.
These patterns appear frequently in the lead sheet corpus and inform context-aware resolution:
| Progression | Example | Key Insight |
|---|---|---|
| ii-V-I major | Dm7 → G7 → Cmaj7 | G7 gets Mixolydian |
| ii-V-i minor | D:hdim7 → G:7 → C:min7 | G:7 gets Phrygian dominant |
| I-vi-ii-V | Cmaj7 → Am7 → Dm7 → G7 | Am7=Aeolian, all in C major |
| iii-vi-ii-V | Em7 → Am7 → Dm7 → G7 | Em7=Phrygian, Am7=Aeolian, all in C major |
| Tritone sub | Dm7 → Db7 → Cmaj7 | Db7 gets Lydian dominant |
| Backdoor ii-V | Fm7 → Bb7 → Cmaj7 | bVII7 resolving up to I |
| Rhythm changes bridge | D7 → G7 → C7 → F7 | Circle of dominants, each Mixolydian |
| Minor blues | Cm7 → Fm7 → G7 → Cm7 | G7 gets Phrygian dominant |
| Diminished passing | Cmaj7 → C#dim7 → Dm7 | C#dim7 is chromatic connector |