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
35 changes: 26 additions & 9 deletions Semantics.Music/Form.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace ktsu.Semantics.Music;
/// <summary>The structural form of a piece: its section letter-pattern and any recognized named form.</summary>
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],
Expand Down Expand Up @@ -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<ChordEvent> 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;
}
27 changes: 26 additions & 1 deletion Semantics.Test/Music/FormTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
Loading