You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Chord.Voice(octave, inversion) puts the root back in the bass from the 4th inversion of extended chords: C9 inversion 4 gives C5 D5 E5 G5 A#5 #356
Chord.Voice(int octave, int inversion) (Chord.cs#L529-L563) builds inversion k by adding 12 to the first k chord tones and re-sorting. The doc says inversion k voices the chord with the k-th chord tone lowest. That only holds while every tone above the raised ones is within an octave of the root.
ChordTones() returns tensions as 13–21 semitones. Once the root is raised to 12, it sits below the 9th (14), 11th (17) and 13th (21). So when the inversion reaches a tension, the raised root drops back to the bottom.
Inversions 5 and 6 of C13 also put C in the bass. The inversion is silently ignored, and the result is root position an octave up.
Trace for C9: the tones [0,4,7,10,14] become [12,16,19,22,14] after four raises, which sorts to [12,14,…].
There is a related smaller problem. The root is built with Pitch.Parse(Root.Name + octave), so a root above MIDI 127 throws FormatException("Invalid note name 'B9'") (Chord.Parse("B").Voice(9)). An upper tone out of range throws ArgumentOutOfRangeException for midi (Chord.Parse("G").Voice(9)). The same kind of bad input gives two different exceptions.
Suggested fix
Take the target bass as tones[rotation], then raise every tone below it by whole octaves until it sits above that bass, instead of raising only the first rotation tones by one octave. Compute the root from MIDI arithmetic rather than by parsing a string. Throw a single ArgumentOutOfRangeException(nameof(octave)) when the voicing leaves 0..127.
Acceptance criteria
For every chord in the round-trip corpus and every inversion in 0..ChordTones().Count-1, the lowest pitch of Voice(4, inversion) (ignoring a slash bass) has pitch class Root + ChordTones()[inversion].
Voice with an octave that pushes any tone outside MIDI 0..127 throws ArgumentOutOfRangeException.
The existing InversionTransposeTests cover only triads.
Priority: Medium. Voice silently returns root position for inversions of 4 and above on extended chords, and out-of-range octaves throw two different exception types.
Next step: Pick the bass as tones[inversion], then raise every lower tone by whole octaves above it. Build the root from MIDI arithmetic and throw a single ArgumentOutOfRangeException(nameof(octave)). Extend InversionTransposeTests beyond triads over the round-trip corpus.
Activity
matt-edmondson commentedon Oct 5, 2026
Triage
Voicesilently returns root position for inversions of 4 and above on extended chords, and out-of-range octaves throw two different exception types.Semantics.Music/Chord.cs(Voice)tones[inversion], then raise every lower tone by whole octaves above it. Build the root from MIDI arithmetic and throw a singleArgumentOutOfRangeException(nameof(octave)). ExtendInversionTransposeTestsbeyond triads over the round-trip corpus.Generated by Claude Code