diff --git a/Semantics.Music/Chord.cs b/Semantics.Music/Chord.cs index 1f37c03..fb74390 100644 --- a/Semantics.Music/Chord.cs +++ b/Semantics.Music/Chord.cs @@ -312,14 +312,19 @@ private static SeventhType DetermineSeventh(string body, ChordQuality quality) bool hasSeven = body.Contains('7'); bool hasMaj7 = body.Contains("maj", StringComparison.Ordinal) || body.Contains("M7", StringComparison.Ordinal) || body.Contains('Δ'); - if (quality == ChordQuality.Diminished && body.Contains("dim", StringComparison.Ordinal) && hasSeven) + // The major seventh is tested first because one body can carry both spellings: "dimmaj7" + // is the diminished-major seventh, a chord distinct from "dim7", and it is what the + // formatter emits for Diminished + Major. Testing the diminished branch first swallowed + // it, since "dimmaj7" contains "dim" and a '7' exactly as "dim7" does. "dim7" itself is + // unaffected: it carries no "maj". + if (hasMaj7) { - return SeventhType.Diminished; + return SeventhType.Major; } - if (hasMaj7) + if (quality == ChordQuality.Diminished && body.Contains("dim", StringComparison.Ordinal) && hasSeven) { - return SeventhType.Major; + return SeventhType.Diminished; } return hasSeven ? SeventhType.Dominant : SeventhType.None; diff --git a/Semantics.Test/Music/ChordRoundTripTests.cs b/Semantics.Test/Music/ChordRoundTripTests.cs index fb990dd..6b81264 100644 --- a/Semantics.Test/Music/ChordRoundTripTests.cs +++ b/Semantics.Test/Music/ChordRoundTripTests.cs @@ -28,6 +28,24 @@ public void CanonicalOutputRoundTrips() } } + [TestMethod] + public void DiminishedMajorSeventhRoundTrips() + { + // The corpus above cannot cover this one. It starts from a symbol, and "Cdimmaj7" parsed + // wrongly as a diminished seventh still formats and re-parses consistently as "Cdim7" — + // a stable round trip of the wrong chord. The chord object is the only honest starting + // point, since it is what the formatter is being asked to be the inverse of. + Chord chord = new() { Quality = ChordQuality.Diminished, Seventh = SeventhType.Major }; + + Assert.AreEqual("Cdimmaj7", chord.ToString()); + + Chord reparsed = Chord.Parse(chord.ToString()); + + Assert.AreEqual(SeventhType.Major, reparsed.Seventh); + Assert.AreEqual(ChordQuality.Diminished, reparsed.Quality); + Assert.AreEqual(chord, reparsed); + } + [TestMethod] public void TryParseReturnsFalseOnEmpty() { diff --git a/Semantics.Test/Music/ChordTests.cs b/Semantics.Test/Music/ChordTests.cs index edc323f..e83466f 100644 --- a/Semantics.Test/Music/ChordTests.cs +++ b/Semantics.Test/Music/ChordTests.cs @@ -42,6 +42,33 @@ public void Parse_MinorMajorSeventh_IsBothMinorAndMaj7() Assert.AreEqual(SeventhType.Major, c.Seventh); } + [TestMethod] + public void Parse_DiminishedMajorSeventh_IsDiminishedTriadWithMajorSeventh() + { + Chord c = Chord.Parse("Cdimmaj7"); + Assert.AreEqual(ChordQuality.Diminished, c.Quality); + Assert.AreEqual(SeventhType.Major, c.Seventh); + } + + [TestMethod] + public void Parse_DiminishedSeventh_StillHasTheDiminishedSeventh() + { + // The guard on the reordered checks: "dim7" carries no "maj", so it must not be swept + // into the major-seventh branch that now runs first. + Chord c = Chord.Parse("Cdim7"); + Assert.AreEqual(ChordQuality.Diminished, c.Quality); + Assert.AreEqual(SeventhType.Diminished, c.Seventh); + } + + [TestMethod] + public void ChordTones_DiminishedMajorSeventh_HasTheMajorSeventhNotTheDoubleFlatSeventh() + { + // The audible half of the bug: a misparse puts the bb7 (9) where the major 7th (11) + // belongs, so Voice() and playback are a semitone out on that tone. + int[] actual = [.. Chord.Parse("Cdimmaj7").ChordTones()]; + Assert.AreSequenceEqual([0, 3, 6, 11], actual, "Cdimmaj7 should be a diminished triad with a major seventh."); + } + [TestMethod] public void Parse_HalfDiminished_IsDiminishedTriadWithDominantSeventh() {