From 9c4fff3ec29a4e1529829e014c57c7c7b46ddcbe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 00:30:43 +0000 Subject: [PATCH] =?UTF-8?q?[patch]=20Spell=20fully=20and=20half-diminished?= =?UTF-8?q?=20sevenths=20as=20vii=C2=B07=20and=20vii=C3=B87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RomanNumeralOf gave a fully diminished seventh no suffix after °, so Bdim7 read "vii°", the same as the triad. A half-diminished seventh read "vii°7", which ChordFromRomanNumeral parses back as fully diminished. QualitySuffix now emits "°7" for a diminished seventh and "ø7" for a diminished triad under a minor seventh, and ChordFromRomanNumeral reads ø as half-diminished. Fixes ktsu-dev/Semantics#289 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FRrKUmpjRrYrUchAi6wGqR --- Semantics.Music/Key.cs | 23 ++++++++++++------ .../Music/RomanNumeralParseTests.cs | 24 ++++++++++++++++++- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Semantics.Music/Key.cs b/Semantics.Music/Key.cs index 2daaee3f..7fc4383e 100644 --- a/Semantics.Music/Key.cs +++ b/Semantics.Music/Key.cs @@ -145,7 +145,7 @@ public string RomanNumeralOf(Chord chord) /// Parses a roman-numeral function relative to this key back into a concrete chord. /// - /// A roman numeral such as "Imaj7", "ii7", "V7", "bII", or "vii°7": an optional accidental prefix + /// A roman numeral such as "Imaj7", "ii7", "V7", "bII", "vii°7", or "viiø7": an optional accidental prefix /// (b/♭ or #/♯), the degree numeral (upper-case major, lower-case minor), then a quality suffix. /// /// The chord rooted at the resolved scale degree. @@ -179,11 +179,16 @@ public Chord ChordFromRomanNumeral(string numeral) PitchClass root = PitchClass.Create(pitchClasses[degree - 1].Value + alteration); - // Normalise the suffix into a chord-symbol body, mapping °/+ onto dim/aug and - // supplying a leading "m" for lower-case (minor) numerals. + // Normalise the suffix into a chord-symbol body, mapping °/+ onto dim/aug, passing ø + // through as half-diminished, and supplying a leading "m" for lower-case (minor) numerals. string suffix = text[index..]; string quality; - if (suffix.StartsWith('°')) + if (suffix.StartsWith('ø')) + { + quality = "ø"; + suffix = suffix[1..]; + } + else if (suffix.StartsWith('°')) { quality = "dim"; suffix = suffix[1..]; @@ -234,14 +239,18 @@ private static string QualitySuffix(Chord chord) string seventh = chord.Seventh switch { SeventhType.Major => "maj7", - SeventhType.Dominant => "7", - SeventhType.Diminished => chord.Quality == ChordQuality.Diminished ? "" : "7", + SeventhType.Dominant or SeventhType.Diminished => "7", _ => "", }; + // A diminished triad under a minor seventh is half-diminished (viiø7), which has to be told + // apart from the fully diminished seventh (vii°7): ChordFromRomanNumeral reads ° as dim, so + // spelling both with ° would turn a half-diminished chord into a fully diminished one. + bool halfDiminished = chord.Quality == ChordQuality.Diminished && chord.Seventh == SeventhType.Dominant; + string quality = chord.Quality switch { - ChordQuality.Diminished => "°", + ChordQuality.Diminished => halfDiminished ? "ø" : "°", ChordQuality.Augmented => "+", ChordQuality.Sus2 => "sus2", ChordQuality.Sus4 => "sus4", diff --git a/Semantics.Test/Music/RomanNumeralParseTests.cs b/Semantics.Test/Music/RomanNumeralParseTests.cs index ca22e7a2..630f86fb 100644 --- a/Semantics.Test/Music/RomanNumeralParseTests.cs +++ b/Semantics.Test/Music/RomanNumeralParseTests.cs @@ -45,6 +45,28 @@ public void Parse_LeadingToneDiminishedSeventh() Assert.AreEqual(SeventhType.Diminished, vii.Seventh); } + [TestMethod] + public void Parse_LeadingToneHalfDiminishedSeventh() + { + Chord vii = CMajor.ChordFromRomanNumeral("viiø7"); + Assert.AreEqual(11, vii.Root.Value); // B + Assert.AreEqual(Chord.Parse("Bm7b5"), vii); + } + + [TestMethod] + public void RomanNumeralOf_KeepsTheSeventhOfAFullyDiminishedSeventh() + { + Assert.AreEqual("vii°", CMajor.RomanNumeralOf(Chord.Parse("Bdim"))); + Assert.AreEqual("vii°7", CMajor.RomanNumeralOf(Chord.Parse("Bdim7"))); + } + + [TestMethod] + public void RomanNumeralOf_SpellsAHalfDiminishedSeventhWithØ() + { + Assert.AreEqual("viiø7", CMajor.RomanNumeralOf(Chord.Parse("Bm7b5"))); + Assert.AreEqual(Chord.Parse("Bm7b5"), CMajor.ChordFromRomanNumeral(CMajor.RomanNumeralOf(Chord.Parse("Bm7b5")))); + } + [TestMethod] public void Parse_FlatSixInMinor() { @@ -56,7 +78,7 @@ public void Parse_FlatSixInMinor() [TestMethod] public void Parse_IsInverseOfRomanNumeralOf() { - string[] numerals = ["Imaj7", "ii7", "iii", "IV", "V7", "vi", "vii°"]; + string[] numerals = ["Imaj7", "ii7", "iii", "IV", "V7", "vi", "vii°", "vii°7", "viiø7"]; foreach (string numeral in numerals) { Chord chord = CMajor.ChordFromRomanNumeral(numeral);