Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions Semantics.Music/Key.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public string RomanNumeralOf(Chord chord)

/// <summary>Parses a roman-numeral function relative to this key back into a concrete chord.</summary>
/// <param name="numeral">
/// 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.
/// </param>
/// <returns>The chord rooted at the resolved scale degree.</returns>
Expand Down Expand Up @@ -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..];
Expand Down Expand Up @@ -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",
Expand Down
24 changes: 23 additions & 1 deletion Semantics.Test/Music/RomanNumeralParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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);
Expand Down
Loading