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
13 changes: 9 additions & 4 deletions Semantics.Music/Chord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions Semantics.Test/Music/ChordRoundTripTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
27 changes: 27 additions & 0 deletions Semantics.Test/Music/ChordTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading