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);