diff --git a/Semantics.Music/Key.cs b/Semantics.Music/Key.cs index 2daaee3..7fc4383 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 ca22e7a..630f86f 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);