diff --git a/Semantics.Music/Form.cs b/Semantics.Music/Form.cs
index eb30ec0d..2ff3301f 100644
--- a/Semantics.Music/Form.cs
+++ b/Semantics.Music/Form.cs
@@ -9,6 +9,8 @@ namespace ktsu.Semantics.Music;
/// The structural form of a piece: its section letter-pattern and any recognized named form.
public sealed record Form
{
+ private const int BluesBars = 12;
+
private static readonly int[][] BluesTemplates =
[
[1, 1, 1, 1, 4, 4, 1, 1, 5, 4, 1, 1],
@@ -133,26 +135,41 @@ private static bool IsRondo(string pattern)
return true;
}
+ // A twelve-bar blues is judged bar by bar, not chord by chord: the section must last exactly twelve bars, and each
+ // bar is represented by the chord sounding on its downbeat. A chord held across several bars counts for each of
+ // them, and a chord that starts after a downbeat does not count for that bar.
private static bool IsTwelveBarBlues(Section section, Key key)
{
- IReadOnlyList chords = section.Progression.Chords;
- if (chords.Count != 12)
+ Progression progression = section.Progression;
+ Duration bar = progression.TimeSignature.BarDuration;
+ if (progression.TotalDuration != bar.Multiply(BluesBars))
{
return false;
}
- int[] degrees = new int[12];
- for (int i = 0; i < 12; i++)
+ int[] degrees = new int[BluesBars];
+ int barIndex = 0;
+ Duration end = Duration.Create(0, 1);
+ foreach (ChordEvent chordEvent in progression.Chords)
{
- ScaleDegree degree = key.FunctionOf(chords[i].Chord.Root);
- if (degree.Alteration != 0)
+ end = end.Add(chordEvent.Duration);
+
+ // Every downbeat before this chord ends that an earlier chord has not already claimed falls within this chord.
+ while (barIndex < BluesBars && IsBefore(bar.Multiply(barIndex), end))
{
- return false;
- }
+ ScaleDegree degree = key.FunctionOf(chordEvent.Chord.Root);
+ if (degree.Alteration != 0)
+ {
+ return false;
+ }
- degrees[i] = degree.Degree;
+ degrees[barIndex++] = degree.Degree;
+ }
}
return BluesTemplates.Any(template => template.SequenceEqual(degrees));
}
+
+ private static bool IsBefore(Duration left, Duration right) =>
+ (long)left.Numerator * right.Denominator < (long)right.Numerator * left.Denominator;
}
diff --git a/Semantics.Test/Music/FormTests.cs b/Semantics.Test/Music/FormTests.cs
index a4221874..ffacc9bf 100644
--- a/Semantics.Test/Music/FormTests.cs
+++ b/Semantics.Test/Music/FormTests.cs
@@ -35,7 +35,7 @@ public void Of_RecognizesTernaryAndBinary()
[TestMethod]
public void Of_RecognizesTwelveBarBlues()
{
- Section blues = Section(SectionType.Verse, "C7 | C7 | C7 | C7 | F7 | F7 | C7 | C7 | G7 | F7 | C7 | C7");
+ Section blues = Section(SectionType.Verse, "C7 / / / | C7 / / / | C7 / / / | C7 / / / | F7 / / / | F7 / / / | C7 / / / | C7 / / / | G7 / / / | F7 / / / | C7 / / / | C7 / / /");
Arrangement arrangement = Arrangement.Create(CMajor, [blues]);
Assert.AreEqual(NamedForm.TwelveBarBlues, arrangement.Form.Name);
}
@@ -66,4 +66,29 @@ public void TryParseFailsOnLowercase()
Assert.IsFalse(Form.TryParse("aaba", out Form? result));
Assert.IsNull(result);
}
+
+ [TestMethod]
+ public void Of_RecognizesTwelveBarBlues_WithHeldChords()
+ {
+ Section blues = Section(
+ SectionType.Verse,
+ "C7 / / / / / / / / / / / / / / / | F7 / / / / / / / | C7 / / / / / / / | G7 / / / | F7 / / / | C7 / / / / / / /");
+ Assert.AreEqual(12, blues.Bars);
+ Assert.AreEqual(NamedForm.TwelveBarBlues, Arrangement.Create(CMajor, [blues]).Form.Name);
+ }
+
+ [TestMethod]
+ public void Of_RecognizesTwelveBarBlues_ByDownbeatChord()
+ {
+ Section blues = Section(SectionType.Verse, "C7 / / / | C7 / / / | C7 / / / | C7 / G7 / | F7 / / / | F7 / / / | C7 / / / | C7 / / / | G7 / / / | F7 / / / | C7 / / / | C7 / G7 /");
+ Assert.AreEqual(NamedForm.TwelveBarBlues, Arrangement.Create(CMajor, [blues]).Form.Name);
+ }
+
+ [TestMethod]
+ public void Of_DoesNotCallTwelveQuarterNoteChordsABlues()
+ {
+ Section shortSection = Section(SectionType.Verse, "C7 C7 C7 C7 | F7 F7 C7 C7 | G7 F7 C7 C7");
+ Assert.AreEqual(3, shortSection.Bars);
+ Assert.AreNotEqual(NamedForm.TwelveBarBlues, Arrangement.Create(CMajor, [shortSection]).Form.Name);
+ }
}