An interactive four-part chorale harmonizer. Given a soprano melody and a chord progression, the program generates alto, tenor, and bass voices that obey classical voice-leading rules, then renders the result as a playable score in the browser.
Given a soprano line (as MIDI note numbers) and a sequence of chord symbols, the harmonizer fills in the three lower voices using a beam search followed by iterative rule-enforcement passes. The output respects standard SATB constraints:
- Voice ranges (soprano 60–79, alto 55–74, tenor 48–67, bass 43–60)
- No voice crossing or overlapping
- Chord tone coverage at every beat
- Correct bass note for root position, first inversion, and second inversion chords
- Doubling rules: root doubled in root-position triads, soprano doubled in first inversion, bass/fifth doubled in second inversion, leading tone and seventh never doubled
- No parallel fifths or octaves, no hidden fifths
- No consecutive unisons between any pair of voices
- Seventh resolution (descends by step), leading-tone resolution (rises by half-step)
- Chromatic tone resolution for secondary dominants, secondary leading-tone chords, and modal mixture chords
- Avoidance of augmented second melodic intervals
The result is shown in a text table in the GUI, with any out-of-chord tones flagged. Clicking "Show Score" opens a browser page with a rendered grand-staff score (via VexFlow) and a playable audio transport.
The optimizer makes a best effort but does not guarantee perfect voice leading in all cases. Some progressions may still contain minor rule violations.
harmonizer.py: all music logic: chord definitions, key signatures, beam search (_beam_search_chunk), voice snapping, and the full suite of rule-enforcement fixup passes called byoptimize()main.py: Tkinter GUI: beat editor, key selector, example loader, result display, and the "Show Score" buttonscore_svg.py: HTML/VexFlow score renderer: converts MIDI pitches to VexFlow note strings with correct accidentals for the active key signature, then serves a self-contained HTML page over a local HTTP server and opens it in the browser; includes a Web Audio synthesizer for playbacktest_harmony.py: unittest suite covering coverage, voice ranges, ordering, spacing, bass correctness, doubling rules, parallel motion, resolution, and a library of named progressions tested end-to-end
No third-party packages are required. The GUI requires Tkinter (included with standard Python on macOS and Windows; on Linux install python3-tk). The score viewer requires a browser with JavaScript enabled.
python main.pyOn startup the app loads the first built-in example. Use the Load example dropdown to switch between the 22 included progressions, or edit the beat table directly:
- Key: select any of the 30 supported major and minor keys
- Beat table: each row has a chord symbol and a soprano MIDI note; use + Add Beat / – Remove Beat to resize
- ▶ Harmonize: runs the optimizer and displays the four-voice result
- 🎼 Show Score: opens the score in a browser tab with Play/Stop controls and a tempo slider
The chord vocabulary covers diatonic triads and seventh chords in all inversions, secondary dominants and secondary leading-tone chords, modal mixture/borrowed chords, Neapolitan and augmented-sixth chords, and fully diminished seventh chords in all enharmonic spellings. A few examples:
| Symbol | Meaning (in C) |
|---|---|
I, I6, I64 |
Tonic triad, root / first / second inversion |
V7, V65, V43, V42 |
Dominant seventh, all inversions |
viio6 |
Leading-tone triad, first inversion |
V/V, V7/V |
Secondary dominant of V (D major / D dominant 7th) |
viio7/V |
Secondary leading-tone dim7 of V |
iv, bVI, bVII |
Modal mixture (borrowed from parallel minor) |
N6 |
Neapolitan sixth |
Ger65 |
German augmented sixth |
viiodim7 |
Fully diminished seventh |
optimize() in harmonizer.py runs in two stages:
Beam search splits the progression at phrase boundaries (cadential I64, half cadences) and runs a forward beam search over each segment with width 40, scoring candidates on voice-leading smoothness, doubling quality, spacing, parallel motion penalties, and resolution tendencies.
Rule-enforcement loop runs up to six iterations of twelve sequential fixup passes over the full progression until the voices stop changing:
_ensure_coverage: moves an inner voice to supply any missing chord tone_fix_common_tones: retains common tones between adjacent chords where possible_fix_aug2: eliminates augmented-second melodic leaps_fix_parallels: removes parallel fifths and octaves_fix_hidden_fifths: removes hidden (direct) fifths in outer voices_fix_consec_unisons: prevents consecutive unisons between voice pairs_fix_crossing_and_spacing: corrects voice crossing and spacing violations_fix_third_doubling: avoids doubling the third in major triads_fix_doubling: enforces root/soprano/bass doubling conventions_fix_seventh_resolution: resolves chord sevenths downward by step_fix_chromatic_resolution: resolves leading tones and mixture tones in secondary chords_enforce_ordering: final pass to ensure S ≥ A ≥ T ≥ B
python -m unittest test_harmony -vThe suite runs 81 tests covering individual rule classes and 17 named progressions checked against the full rule set end-to-end.