What's wrong
Scale.DegreeOf (Semantics.Music/Scale.cs:94-103) spells a pitch class that isn't in the scale against its nearest scale neighbours, preferring the flat spelling on a tie. When the pitch is above the scale's last offset there is no upper neighbour, so the flat candidate wraps around the octave to degree 1:
int flatDegree = upperIndex >= 0 ? upperIndex + 1 : 1;
int flatAlteration = semitone - (upperIndex >= 0 ? offsets[upperIndex] : offsets[0] + 12);
...
return Math.Abs(flatAlteration) <= Math.Abs(sharpAlteration) ? flat : sharp;
Every mode with a flat seventh has this problem: aeolian, dorian, phrygian, mixolydian, locrian, and the blues and pentatonic scales. Semitone 11 is one semitone from both neighbours, so it comes out as degree 1 with alteration −1, a flattened tonic.
Harmonic analysis never reads that pitch as a flattened tonic. It is the raised seventh, the leading tone, and the most common chromatic note in minor-key harmony.
Reproduced
In a scratch net10.0 app against Semantics.Music:
Key am = Key.Parse("A aeolian");
am.FunctionOf(PitchClass.Parse("G#")); // ScaleDegree { Degree = 1, Alteration = -1 }
am.RomanNumeralOf(Chord.Parse("G#dim")); // "bi°"
am.RomanNumeralOf(Chord.Parse("G#dim7")); // "bi°"
Key.Parse("G mixolydian").RomanNumeralOf(Chord.Parse("F#dim")); // "bi°"
am.ChordFromRomanNumeral("#vii°7"); // G#dim7 — the parser accepts the conventional spelling
So the minor progression i – iv – vii°7 – i is labelled i, iv, bi°, i. ChordFromRomanNumeral accepts #vii°, which means the output of RomanNumeralOf and the input ChordFromRomanNumeral expects disagree.
Suggested fix
In DegreeOf, when the flat candidate reaches degree 1 only by wrapping (upperIndex < 0), use the sharp spelling of the last degree instead. The flat-preferring tie-break stays as it is everywhere else, so major-key bII, bIII, bVI and bVII are unchanged.
Acceptance criteria
Key.Parse("A aeolian").FunctionOf(PitchClass.Parse("G#")) returns (7, +1).
Key.Parse("A aeolian").RomanNumeralOf(Chord.Parse("G#dim")) returns "#vii°".
RomanNumeralOf → ChordFromRomanNumeral round-trips for that chord.
- The existing
ScaleTests.DegreeOf_ChromaticPrefersFlatOfUpperDegree still passes.
Related but separate: #289 covers the seventh suffix (° vs °7 / ø7) on this same chord.
What's wrong
Scale.DegreeOf(Semantics.Music/Scale.cs:94-103) spells a pitch class that isn't in the scale against its nearest scale neighbours, preferring the flat spelling on a tie. When the pitch is above the scale's last offset there is no upper neighbour, so the flat candidate wraps around the octave to degree 1:Every mode with a flat seventh has this problem: aeolian, dorian, phrygian, mixolydian, locrian, and the blues and pentatonic scales. Semitone 11 is one semitone from both neighbours, so it comes out as degree 1 with alteration −1, a flattened tonic.
Harmonic analysis never reads that pitch as a flattened tonic. It is the raised seventh, the leading tone, and the most common chromatic note in minor-key harmony.
Reproduced
In a scratch net10.0 app against Semantics.Music:
So the minor progression i – iv – vii°7 – i is labelled
i, iv, bi°, i.ChordFromRomanNumeralaccepts#vii°, which means the output ofRomanNumeralOfand the inputChordFromRomanNumeralexpects disagree.Suggested fix
In
DegreeOf, when the flat candidate reaches degree 1 only by wrapping (upperIndex < 0), use the sharp spelling of the last degree instead. The flat-preferring tie-break stays as it is everywhere else, so major-key bII, bIII, bVI and bVII are unchanged.Acceptance criteria
Key.Parse("A aeolian").FunctionOf(PitchClass.Parse("G#"))returns(7, +1).Key.Parse("A aeolian").RomanNumeralOf(Chord.Parse("G#dim"))returns"#vii°".RomanNumeralOf→ChordFromRomanNumeralround-trips for that chord.ScaleTests.DegreeOf_ChromaticPrefersFlatOfUpperDegreestill passes.Related but separate: #289 covers the seventh suffix (
°vs°7/ø7) on this same chord.