Lesson 18: Voice leading
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Lesson 17 asked where the capo goes and how the strings are tuned. Once the chords are chosen, the next question is how to move from one to the next. Voice leading answers it: each note of a chord is a voice, and each voice moves to a note of the next chord, as little as possible. The router sends “voice leading from C to F” to skill.voiceleading, which runs VoiceLeadingSkill, registered between the capo and the tunings (GaPlugin.cs lines 85-88). It was built on 2026-05-14, like them, to close a “dealbreaker” of GA’s backlog, and it needs no model: it tries every pairing of the two chords’ notes and keeps the one that moves least (VoiceLeadingSkill.cs lines 6-23).
All GA links point to commit a826864, the course’s pin, unless they name another. VoiceLeadingSkill.cs is the same on GA’s main at f4f5b4a (2026-10-01), so the program asks it at the pin only. The output comes from:
dotnet run --project code/ga-ai/GaAi -c Release -- l18Which prompts reach the skill
Section titled “Which prompts reach the skill”CanHandle answers false, with the comment “semantic-routing only” (line 49). On main, when the router can’t embed a question, it gives the question to the first intent, in registration order, whose skill’s CanHandle accepts it (SemanticIntentRouter.cs line 321, OrchestratorSkillIntent.cs line 29). The program starts the chatbot’s host as in lesson 15 and asks every skill’s CanHandle the skill’s 10 example prompts:
== Without embeddings: the first skill, in registration order, whose CanHandle accepts each example promptprompt first skill that accepts itvoice leading from C to F nonesmooth voice leading C to Am nonebest voicing from G7 to Cmaj7 skill.chordvoicingshow do I voice lead Dm7 to G7 nonevoice leading C major to G major nonesmoothest voicing from Em to A7 skill.chordvoicingsvoice leading Fmaj7 to Bm7b5 nonewhat's the smoothest voicing from D to A nonevoice lead C7 to F nonebest way to move from G7 to C noneskill intents 32; CanHandle of skill.voiceleading accepts 0 of its 10 example prompts- Without embeddings, the skill never answers, though it needs no model. Two of its examples go to the voicings skill of lesson 16, because they say “voicing”: that skill answers fingerings for one chord, not the move between two.
- On
main, a refusal can be markedDeclined, “so a caller may route it to another handler” (GuitarAlchemistAgentBase.cslines 347-352 onmain). The skill’s two refusals aren’t (lines 280-294).
How the skill answers
Section titled “How the skill answers”The skill reads two chord symbols around “to”, “→”, “->” or “>” (lines 51-57):
// Two-chord pattern: <chord A> to/→/-> <chord B>. The chord token allows // root + optional accidental + optional quality keyword + optional digit // + optional flat/sharp-with-digit modifiers (b5, #9, b9...) + optional // ° symbol for diminished. private static readonly Regex TwoChordPattern = new(@"\b(?<a>[A-Ga-g][b#♭♯]?(?:maj|min|m|dim|aug|sus|add|dom)?\d*(?:[b#♭♯]\d+)*°?)\s+(?:to|→|->|>)\s+(?<b>[A-Ga-g][b#♭♯]?(?:maj|min|m|dim|aug|sus|add|dom)?\d*(?:[b#♭♯]\d+)*°?)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);It turns each symbol into pitch classes, then pairs the notes of the two chords. When one chord has fewer notes, it repeats that chord’s root until both have as many (lines 101-110, Pad, lines 185-194):
// For an exhaustive minimum-cost matching, we permute the SHORTER chord // against subsets of the larger. To keep the explanation crisp, we pad // the smaller chord by repeating its root so |A| = |B| for assignment // — this is the standard voice-leading framing when voice counts // differ (e.g. triad → 7th chord adds a voice that gets the new tone). var n = Math.Max(chordA.Length, chordB.Length); var paddedA = Pad(chordA, n); var paddedB = Pad(chordB, n);
var (bestPerm, bestCost) = FindBestAssignment(paddedA, paddedB);FindBestAssignment tries every permutation and keeps the first with the least total, each voice moving by the shorter way, at most 6 semitones (lines 139-183). The answer gives the total, a table of the voices, and a closing sentence: “This is the optimal pitch-class assignment — every other voicing of … requires more total semitone movement” (line 131).
The program reads the total and the table of each answer. It names the chords the table moves from and to, and compares the total with the least motion: as many voices as the larger chord, every note of both chords sounded, and any note of the smaller chord doubled. It also counts the pairings, in the skill’s own frame with the root doubled, that reach the skill’s total.
== VoiceLeadingSkill's example prompts: the chords read, the total motion, and the least motionprompt asks reads total least best pairingsvoice leading from C to F C → F C → F 3 3 1smooth voice leading C to Am C → Am C → Am 2 2 1best voicing from G7 to Cmaj7 G7 → Cmaj7 G7 → Cmaj7 3 3 1how do I voice lead Dm7 to G7 Dm7 → G7 Dm7 → G7 3 3 1voice leading C major to G major C → G declined - - -smoothest voicing from Em to A7 Em → A7 Em → A7 5 4 1voice leading Fmaj7 to Bm7b5 Fmaj7 → Bm7b5 Fmaj7 → Bm7b5 3 3 1what's the smoothest voicing from D to A D → A D → A 3 3 1voice lead C7 to F C7 → F C7 → F 4 4 1best way to move from G7 to C G7 → C G7 → C 4 4 1- Eight examples get the least motion. Between C and F, the voice on C stays, E goes up to F and G up to A: 3 semitones.
- “smoothest voicing from Em to A7” gets 5 semitones, where 4 are enough. Em has three notes and A7 four, so the skill doubles E. Doubling G or B instead moves 4 semitones, and the answer still says that every other voicing moves more.
- “voice leading C major to G major” is declined. A chord must be followed by spaces and “to”, so the word “major” after C breaks the expression.
The least motion, chord by chord
Section titled “The least motion, chord by chord”The program asks “voice leading C… to …” for the 16 qualities the skill builds, on C and on each of the 12 roots, and groups the answers by the number of notes of the two chords:
== "voice leading C<quality> to <root><quality>": the 16 qualities the skill builds, on C and on each of the 12 rootsvoices prompts read right least motion more than the least most extra several best pairings3 → 3 432 432 432 0 0 543 → 4 504 504 266 238 5 693 → 5 216 216 34 182 7 674 → 3 504 504 266 238 5 694 → 4 588 588 588 0 0 564 → 5 252 252 79 173 5 885 → 3 216 216 34 182 7 675 → 4 252 252 79 173 5 885 → 5 108 108 108 0 0 49prompts 3072: closing on "every other voicing … requires more total semitone movement" 3072, more motion than the least 1186, several best pairings 607by how much more: 1 semitone 512, 2 semitones 294, 3 semitones 240, 4 semitones 94, 5 semitones 24, 6 semitones 20, 7 semitones 2the first answers with the most extra motion: Csus2 to Em9: 11 semitones, least 4 Cm9 to Absus2: 11 semitones, least 4 Cm to Emaj9: 10 semitones, least 4 Cm to Em9: 10 semitones, least 4 Cm to Fm9: 9 semitones, least 3the first answers with the most best pairings: Cmaj9 to Em9: 7 pairings Cm9 to Abmaj9: 7 pairings Csus2 to Em9: 5 pairings- Between two chords of the same size, the total is always the least. Every permutation is tried, and no doubling is needed.
- When the sizes differ, the root is the wrong note to double in 1186 answers. The new voice must reach the note the smaller chord lacks, and starting it from the root can cost up to 7 semitones more than starting it from another note. From Csus2 to Em9, the two extra voices start on C: 11 semitones, where 4 are enough. The skill’s comment calls root doubling “the standard voice-leading framing”, and four-part harmony does double the root of a triad first. But then the answer is the best move with the root doubled, not the least motion.
- The closing sentence is in all 3072 answers. It’s wrong in the 1186 answers where another doubling moves less, and in the 607 where another pairing in the skill’s own frame moves just as little.
The chord symbols
Section titled “The chord symbols”BuildChord lowercases the quality, then looks it up in a table of 16 qualities (lines 224-252):
private static int[]? BuildChord(int root, string quality) { var q = quality.ToLowerInvariant().Trim();
// Tone choices: 1, b3, 3, 4, b5, 5, #5, 6, b7, 7, b9, 9, #9, 11, #11, 13 int[]? intervals = q switch { "" or "maj" or "major" => [0, 4, 7], // major triad "m" or "min" or "minor" or "-" => [0, 3, 7], // minor triad "dim" or "°" or "o" => [0, 3, 6], // dim triad "aug" or "+" => [0, 4, 8], // aug triad "7" => [0, 4, 7, 10], // dom 7 "m7" or "min7" or "-7" => [0, 3, 7, 10], // min 7 "maj7" or "major7" or "M7" or "Δ7" or "Δ" => [0, 4, 7, 11], // maj 7 "m7b5" or "ø" or "ø7" or "half-dim" or "min7b5" => [0, 3, 6, 10], // half-dim "dim7" or "°7" or "o7" => [0, 3, 6, 9], // dim 7 "sus2" => [0, 2, 7], "sus4" or "sus" => [0, 5, 7], "6" => [0, 4, 7, 9], // maj 6 "m6" or "min6" => [0, 3, 7, 9], // min 6 "9" => [0, 4, 7, 10, 2], // dom 9 "maj9" => [0, 4, 7, 11, 2], "m9" or "min9" => [0, 3, 7, 10, 2], // Unknown quality — return null so the caller emits CannotParse // instead of pretending the chord is a major triad. _ => null, };The program writes each symbol on C, first as the first chord, then as the last:
== Each chord symbol on C, as the first chord ("voice leading C<symbol> to F") and as the last ("voice leading F to C<symbol>")written means first lastC C right rightCmaj C right rightC major C declined rightCm Cm right rightCmin Cm right rightC minor Cm declined reads CC- Cm declined reads CCdim Cdim right rightC° Cdim right reads CCo Cdim declined declinedCaug Caug right rightC+ Caug declined reads CCsus2 Csus2 right rightCsus4 Csus4 right rightCsus Csus4 right rightC7 C7 right rightCdom7 C7 declined declinedCm7 Cm7 right rightCmin7 Cm7 right rightC-7 Cm7 declined reads CCmaj7 Cmaj7 right rightCM7 Cmaj7 reads Cm7 reads Cm7CΔ7 Cmaj7 declined declinedCΔ Cmaj7 declined declinedCmajor7 Cmaj7 declined declinedCm7b5 Cm7b5 right rightCmin7b5 Cm7b5 right rightCø Cm7b5 declined declinedCø7 Cm7b5 declined declinedCdim7 Cdim7 right rightC°7 Cdim7 declined reads CdimCo7 Cdim7 declined declinedC6 C6 right rightCm6 Cm6 right rightCmin6 Cm6 right rightC9 C9 right rightCmaj9 Cmaj9 right rightCm9 Cm9 right rightCmin9 Cm9 right rightCadd9 Cadd9 declined declinedC7sus4 C7sus4 declined declinedC7b9 C7b9 declined declinedC7#9 C7#9 declined declinedCmMaj7 CmMaj7 declined declinedC11 C11 declined declinedC13 C13 declined declinedsymbols 46: read right as the first chord 24, as the last 24- The table lists more symbols than the expression lets through. After the root, the expression takes one word among
maj,min,m,dim,aug,sus,addanddom, digits, accidentals followed by digits, and a final°.Δ,ø,+,-,o, “major7” and “dom7” never reach the table, or reach it in a form it doesn’t list. As the first chord, they’re all declined. As the last,+and-stop the expression, which keeps the letter: “voice leading F to C+” gets C major. The others are declined. - Words after the last chord are ignored. “voice leading F to C minor” gets C major, and “voice leading F to C major” is right by chance.
- CM7 is read as C minor seventh. The table lists
M7, but the quality is lowercased first, som7matches before it. “voice leading CM7 to FM7” moves Cm7 to Fm7. C minor seventh has E♭ and B♭ where C major seventh has E and B. - A final
°is dropped. The expression ends with\b, a word boundary, and°isn’t a word character: “voice leading F to C°” gets C major. As the first chord,°is read, but “C°7” is declined, since°may only end the symbol. - Seven common chords are declined: add9, 7sus4, 7♭9, 7♯9, mMaj7, 11 and 13. The comment says it’s deliberate: a review of 2026-05-14 found that unknown qualities were answered as major triads (lines 216-223).
A root with an accidental
Section titled “A root with an accidental”The program writes the ten roots with an accidental, in ASCII and with ♯ and ♭, as the first chord and as the last:
== A root with an accidental, first ("voice leading <root> to C") and last ("voice leading C to <root>", "… to <root>m")notation first last last, m the root first, spelled the last, misread asASCII # 5 of 5 0 of 5 5 of 5 C#, D#, F#, G#, A# C, D, F, G, A♯ 5 of 5 0 of 5 5 of 5 C#, D#, F#, G#, A# C, D, F, G, AASCII b 5 of 5 5 of 5 5 of 5 Db, Eb, Gb, Ab, A# -♭ 5 of 5 0 of 5 5 of 5 Db, Eb, Gb, Ab, Bb D, E, G, A, B- A sharp, or a flat written
♭, is dropped at the end of the question. The same\bfails after#,♯and♭, and the expression keeps the letter: “voice leading from C to F#” gets C → F. The answer’s first line names the chord read, so a careful reader can see it.bis a letter, so “Db” is read whole, and withmafter the accidental the boundary falls after them. The capo and tunings skills of lesson 17 have the same fault. - The skill’s own refusal suggests B°, “Try a chord-symbol like C, Am, G7, Cmaj7, Dm7, F#m7b5, or B°”, which is read only as the first chord.
The spelling of the answers
Section titled “The spelling of the answers”The skill spells the notes with sharps, unless one of the two symbols has a flat or the question says “flat” (lines 85-86, lines 261-266):
private static bool TokenHasFlats(string token) => token.IndexOf('b', StringComparison.OrdinalIgnoreCase) > 0 // 'b' at position 0 is the note B, not a flat || token.IndexOf('♭') >= 0;
private static string Spell(int pc, bool preferFlats) => preferFlats ? FlatNames[((pc % 12) + 12) % 12] : SharpNames[((pc % 12) + 12) % 12];The program asks ii7 to V7 and V7 to Imaj7 in the 12 major keys, and V7 to i in the 12 minor keys, and lists the notes the skill spells otherwise than their chord does:
== Spelling: ii7 to V7 and V7 to Imaj7 in the 12 major keys, the notes spelled otherwise than in their chordkey ii7 to V7 V7 to Imaj7C as spelled as spelledG as spelled as spelledD as spelled as spelledA as spelled as spelledE as spelled as spelledB as spelled as spelledF# F for E# F for E#F A# for Bb A# for BbBb D# for Eb, A# for Bb A# for Bb, D# for EbEb G# for Ab, A# for Bb, D# for Eb as spelledAb as spelled as spelledDb as spelled as spelled== Spelling: V7 to i in the 12 minor keyskey V7 to iCm D# for EbGm A# for BbDm as spelledAm as spelledEm as spelledBm as spelledF#m F for E#C#m C for B#G#m G for F##Fm A# for Bb, G# for AbBbm A# for Bb, D# for Eb, C# for DbEbm as spelledprompts 36: every note spelled as in its chord 22- A B♭ root written in ASCII doesn’t count as a flat.
TokenHasFlatslooks for abignoring case, which finds the rootBof “Bb” at position 0, and the test wants a position above 0. So “F7 to Bbmaj7” gives A♯ and D♯, and “Bb” as the first chord is spelled A♯. WrittenB♭, it counts. - Without a flat in the question, a flat key gets sharps. D7 to Gm gives A♯ for B♭: the skill doesn’t know the key, only the two symbols.
- Twelve names can’t spell every chord. The skill has one name per pitch class. In F♯ major, E♯ is written F. The B♯ of G♯7 is written C, and the double-sharp F of D♯7 is written G.
Other phrasings
Section titled “Other phrasings”== Other phrasingsprompt asks reads verdicthow do I get from G7 to a C chord G7 → C G7 → A wrong chordvoice leading Dm7 to G7 to Cmaj7 Dm7 → G7 → Cmaj7 Dm7 → G7 reads 2 of 3 chordsvoice leading CM7 to FM7 Cmaj7 → Fmaj7 Cm7 → Fm7 wrong chordvoice leading G7 → C G7 → C G7 → C rightvoice leading G7 - C G7 → C declined declinedvoice leading from G7 into C G7 → C declined declinedvoice leading from C to F# C → F# C → F wrong chordvoice leading from G to B° G → Bdim G → B wrong chordsmooth voice leading from C major to A minor C → Am declined declinedhow do I voice lead Bb to Eb Bb → Eb Bb → Eb right- The article “a” is read as the chord A. The expression ignores case, so in “how do I get from G7 to a C chord” the chord after “to” is “a”: the skill moves G7 to A major.
- A progression of three chords gets only its first move. The expression reads one pair, and the answer doesn’t say that it stopped.
- “-” and “into” aren’t read as “to”, and chords written as words are declined.
Where the course stops
Section titled “Where the course stops”- The router and the model aren’t run. Which intent the router picks for these questions in production needs the embeddings (to verify).
- The least motion is the course’s test: every note of both chords sounded, as many voices as the larger chord. A textbook may also drop the fifth of a seventh chord, which would move even less, and it places the voices in registers. The skill and the course both work on pitch classes, so a “voicing” here has no octave and no fret.
- Another path in GA moves real voicings. The MCP tool
ga_voice_leading_pairpairs playable voicings of the OPTIC-K index with “a greedy sorted-pitch matching”, “not a formal Hungarian-optimal assignment” (CompositionTools.cslines 179-186). The course doesn’t run it.
Reported upstream
Section titled “Reported upstream”- Reported after this lesson was written, in GA issue #789: the root doubling and the claim of an optimal answer, the chord symbols, the accidentals, the spelling of the notes, the phrasings and
CanHandle.
Exercises
Section titled “Exercises”- “voice leading from C to F#” gets C → F. Give two ways to write the question so that the skill moves C to F♯ major.
- The skill moves Em to A7 by 5 semitones. Which note of Em must be doubled to move only 4, and how?
- Why does “voice leading F7 to Bbmaj7” spell A♯ and D♯, while “voice leading F7 to B♭maj7” spells B♭ and E♭? Give a third way to get flats.
- “voice leading CM7 to FM7” and “voice leading Cmaj7 to Fmaj7” both get 4 semitones. How does the answer show that the first moves other chords?
Solutions
- Write the flat,
Gb:bis a letter, so the boundary holds after it, and “voice leading from C to Gb” moves C to G♭ by 6 semitones, spelled with flats. Or add a quality: “voice leading from C to F#maj” ends on thej, and gets the same 6 semitones, spelled with sharps. Checked by running the pinned skill outside the course’s expected output. - G or B. Doubling B: E stays, G stays, B goes down to A and the other B up to C♯, 0 + 0 + 2 + 2. Doubling G: E stays, G stays, the other G goes up to A, and B up to C♯, 0 + 0 + 2 + 2. The skill doubles E, which must go down 3 semitones to C♯ while B goes down 2 to A. Checked by running the pinned skill outside the course’s expected output.
TokenHasFlatslooks forbignoring case and finds theBof “Bbmaj7” at position 0, which it takes for the note B;♭is found directly. With “flat” anywhere in the question, for example “voice leading F7 to Bbmaj7 in flats”, the answer is spelled with flats. Checked by running the pinned skill outside the course’s expected output.- By its notes: the table of the first gives D♯ and A♯, the E♭ and B♭ of Cm7, where Cmaj7 has E and B. The first line also echoes “CM7 → FM7”, which hides the misreading. Checked by running the pinned skill outside the course’s expected output.
Key takeaways
Section titled “Key takeaways”- An optimal search is optimal only over what it searches: trying every permutation finds the best pairing, not the best doubling.
- A claim of optimality in an answer is a claim the program can check, answer by answer, against a slower exhaustive search.
- A table of symbols promises only what the expression in front of it lets through.
\bafter#,♯,♭or°fails, and the expression keeps the letter: the same fault as lessons 7, 9 and 17.- A case-insensitive search for the flat
bfinds the note B.
Sources
Section titled “Sources”- GuitarAlchemist/ga at
a826864:Common/GA.Business.ML/Agents/Skills/VoiceLeadingSkill.cs,Common/GA.Business.Core.Orchestration/Plugins/GaPlugin.cs,GaMcpServer/Tools/CompositionTools.cs. - GuitarAlchemist/ga at
f4f5b4a: the same skill,Common/GA.Business.ML/Agents/GuitarAlchemistAgentBase.cs,Common/GA.Business.ML/Agents/Intents/SemanticIntentRouter.cs,Common/GA.Business.Core.Orchestration/Intents/OrchestratorSkillIntent.cs. - The course’s program:
code/ga-ai/GaAi/Lesson18.cs.