| name | chord-lab-with-pytheory |
| description | Build, voice, and analyze individual chords with PyTheory. Use when the user asks about a single chord — its notes/intervals, inversions or drop-2/drop-3/ open voicings, the tritone substitution, what extensions it can take, how tense or dissonant it is, its Forte number / pitch-class set, figured bass, or the voice-leading between two chords. For progressions and keys use the keys-and-harmony skill; for fingerings/tab use the guitar skill. |
| license | MIT |
| allowed-tools | Write, Read, Bash(python3:*), Bash(uv run:*) |
Chord Lab
Everything about a single chord — construction, voicing, and analysis.
Build a chord
from pytheory import Chord
Chord.from_symbol("F#m7b5")
Chord.from_name("Am")
Chord.from_intervals("C", 0, 4, 7)
Chord.from_tones("C", "E", "G")
A Chord has .symbol, .tones (list of Tone), .root, and
.transpose(semitones).
Voicings
inversion, drop2, drop3, and open_voicing return a new Chord. The
chord symbol stays the same — the change is in the tones (order/octave), so
inspect .tones:
c = Chord.from_symbol("Cmaj7")
[str(t) for t in c.tones]
[str(t) for t in c.inversion(1).tones]
[str(t) for t in c.drop2().tones]
[str(t) for t in c.open_voicing().tones]
drop3() exists too. Use these to spread a close voicing for piano/strings/guitar.
Analysis
c = Chord.from_symbol("Cmaj7")
c.intervals
c.pitch_classes
c.forte_number
c.figured_bass
c.extensions()
c.tension
c.dissonance
c.beat_frequencies
Pitch-class-set toolkit
c = Chord.from_symbol("Cmaj7")
c.normal_form
c.prime_form
c.interval_vector
c.complement
Chord.from_symbol("C").is_transposition_of(Chord.from_symbol("G"))
Chord.from_symbol("C").is_set_class_equivalent(Chord.from_symbol("Cm"))
Chord.from_symbol("C").is_subset_of(Chord.from_symbol("Cmaj7"))
a, b = Chord.from_midi_message(0,1,4,6), Chord.from_midi_message(0,1,3,7)
a.is_z_related(b)
Reharmonization & voice leading
Chord.from_symbol("G7").tritone_sub()
Chord.from_symbol("C").negative_harmony("C").identify()
Chord.from_symbol("G7").negative_harmony("C")
from pytheory import reharmonize
for s in reharmonize(Chord.from_symbol("G7"), "C"):
print(s["technique"], "->", s["chord"].identify())
from pytheory import reharmonize_progression
prog = [Chord.from_symbol(s) for s in ("C","Am","Dm","G7","C")]
[c.symbol for c in reharmonize_progression(prog, "C", technique="secondary_dominants")]
for frm, to, semis in Chord.from_symbol("Cmaj7").voice_leading(Chord.from_symbol("Fmaj7")):
print(frm, "->", to, f"({semis:+d} semitones)")
Neo-Riemannian (P/L/R) — chromatic triad moves
The P/L/R transformations move a single voice to flip a major/minor triad
into another — the engine behind Tonnetz harmony and a lot of film-score
chromaticism. Each is its own inverse; together they reach all 24 triads.
C = Chord.from_symbol("C")
C.parallel().identify()
C.relative().identify()
C.leading_tone_exchange().identify()
C.transform("LP").identify()
C.tonnetz_path(Chord.from_symbol("Am"))
C.tonnetz_path(Chord.from_symbol("Abm"))
Part-writing checker (parallels / crossing)
check_voice_leading flags the common-practice no-no's across a sequence of
voicings. Each voicing's tones are read low-to-high as the voices (so a
4-note chord gets bass/tenor/alto/soprano labels):
from pytheory import Chord, check_voice_leading
a = Chord.from_midi_message(48, 55)
b = Chord.from_midi_message(50, 57)
check_voice_leading([a, b])
It catches parallel fifths, parallel octaves, and voice
crossing; clean part-writing returns [].
Chord-scale theory (what to solo with)
from pytheory import Chord, chord_scales, chord_scale_notes, avoid_notes
chord_scales(Chord.from_symbol("G7"))
chord_scales(Chord.from_symbol("Cm7"))
chord_scales(Chord.from_symbol("Em7"), key="C")
[t.name for t in chord_scale_notes(Chord.from_symbol("Cmaj7"))]
[t.name for t in avoid_notes(Chord.from_symbol("Cmaj7"))]
chord_scales ranks scales by fit (quality alone, or the diatonic mode
first when you pass a key); avoid_notes flags scale tones a half-step
above a chord tone.
from pytheory.play import play, save
from pytheory import Chord, Synth
play(Chord.from_symbol("Cmaj7"), t=2000)
save(Chord.from_symbol("Cmaj7"), "chord.wav", t=2000, synth=Synth.TRIANGLE)
Tips
- Voicing methods change
.tones, not .symbol — compare the tone lists.
tension returns a dict; tension["score"] is the scalar.
- To identify a chord from notes or fret positions, that's the guitar skill
(
Fingering.identify()) or Chord(...).identify().