From 650bcd1264c62a04cd08b47b77a25ad95569f258 Mon Sep 17 00:00:00 2001
From: Will Baldoumas <45316999+wbaldoumas@users.noreply.github.com>
Date: Sat, 4 Nov 2023 16:10:01 -0700
Subject: [PATCH] Notes, Chords, and Extensions (#32)
---
.editorconfig | 3 ++
.../Composition/Chord.cs | 16 ++++++
.../Composition/Contexts/ChordContext.cs | 6 ++-
.../Composition/Note.cs | 16 ++++++
.../Extensions/ChordContextExtensions.cs | 32 ++++++++++++
.../Extensions/NoteContextExtensions.cs | 33 ++++++++++++
.../Extensions/ChordContextExtensionsTests.cs | 50 +++++++++++++++++++
.../Extensions/NoteContextExtensionsTests.cs | 49 ++++++++++++++++++
8 files changed, 204 insertions(+), 1 deletion(-)
create mode 100644 src/BaroquenMelody.Library/Composition/Chord.cs
create mode 100644 src/BaroquenMelody.Library/Composition/Note.cs
create mode 100644 src/BaroquenMelody.Library/Extensions/ChordContextExtensions.cs
create mode 100644 src/BaroquenMelody.Library/Extensions/NoteContextExtensions.cs
create mode 100644 tests/BaroquenMelody.Library.Tests/Extensions/ChordContextExtensionsTests.cs
create mode 100644 tests/BaroquenMelody.Library.Tests/Extensions/NoteContextExtensionsTests.cs
diff --git a/.editorconfig b/.editorconfig
index 90e8a16b..6adf7c7c 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -47,6 +47,9 @@ dotnet_diagnostic.MA0025.severity = none
# a constructor should not follow a property
dotnet_diagnostic.SA1201.severity = none
+# force constructor documentation text
+dotnet_diagnostic.SA1642.severity = none
+
[*.csproj]
indent_style = space
indent_size = 2
diff --git a/src/BaroquenMelody.Library/Composition/Chord.cs b/src/BaroquenMelody.Library/Composition/Chord.cs
new file mode 100644
index 00000000..0400d9e1
--- /dev/null
+++ b/src/BaroquenMelody.Library/Composition/Chord.cs
@@ -0,0 +1,16 @@
+using BaroquenMelody.Library.Composition.Choices;
+using BaroquenMelody.Library.Composition.Contexts;
+
+namespace BaroquenMelody.Library.Composition;
+
+///
+/// Represents a chord in a composition.
+///
+/// The notes which make up the chord.
+/// The previous chord context from which this chord was generated.
+/// The chord choice which was used to generate this chord.
+internal sealed record Chord(
+ ISet Notes,
+ ChordContext ChordContext,
+ ChordChoice ChordChoice
+);
diff --git a/src/BaroquenMelody.Library/Composition/Contexts/ChordContext.cs b/src/BaroquenMelody.Library/Composition/Contexts/ChordContext.cs
index 4ac89f8c..9d8de6b1 100644
--- a/src/BaroquenMelody.Library/Composition/Contexts/ChordContext.cs
+++ b/src/BaroquenMelody.Library/Composition/Contexts/ChordContext.cs
@@ -1,4 +1,6 @@
-namespace BaroquenMelody.Library.Composition.Contexts;
+using BaroquenMelody.Library.Composition.Enums;
+
+namespace BaroquenMelody.Library.Composition.Contexts;
///
/// Represents the note contexts for the voices in a given chord used to arrive at the current chord.
@@ -16,6 +18,8 @@ public IList NoteContexts
init { _noteContexts = value.OrderBy(noteContext => noteContext.Voice).ToList(); }
}
+ public NoteContext this[Voice voice] => NoteContexts.Single(noteContext => noteContext.Voice == voice);
+
public bool Equals(ChordContext? other)
{
if (other is null)
diff --git a/src/BaroquenMelody.Library/Composition/Note.cs b/src/BaroquenMelody.Library/Composition/Note.cs
new file mode 100644
index 00000000..3e3fbeff
--- /dev/null
+++ b/src/BaroquenMelody.Library/Composition/Note.cs
@@ -0,0 +1,16 @@
+using BaroquenMelody.Library.Composition.Choices;
+using BaroquenMelody.Library.Composition.Contexts;
+
+namespace BaroquenMelody.Library.Composition;
+
+///
+/// Represents a note in a composition.
+///
+/// The pitch of the note.
+/// The previous note context from which this note was generated.
+/// The note choice which was used to generate this note.
+internal sealed record Note(
+ byte Pitch,
+ NoteContext NoteContext,
+ NoteChoice NoteChoice
+);
diff --git a/src/BaroquenMelody.Library/Extensions/ChordContextExtensions.cs b/src/BaroquenMelody.Library/Extensions/ChordContextExtensions.cs
new file mode 100644
index 00000000..ab02e423
--- /dev/null
+++ b/src/BaroquenMelody.Library/Extensions/ChordContextExtensions.cs
@@ -0,0 +1,32 @@
+using BaroquenMelody.Library.Composition;
+using BaroquenMelody.Library.Composition.Choices;
+using BaroquenMelody.Library.Composition.Contexts;
+
+namespace BaroquenMelody.Library.Extensions;
+
+internal static class ChordContextExtensions
+{
+ ///
+ /// Applies the given to the given to generate the next chord.
+ ///
+ /// The chord context.
+ /// The chord choice.
+ /// The next chord.
+ public static Chord ApplyChordChoice(this ChordContext chordContext, ChordChoice chordChoice)
+ {
+ var notes = new HashSet();
+
+ foreach (var noteChoice in chordChoice.NoteChoices)
+ {
+ var noteContext = chordContext[noteChoice.Voice];
+
+ notes.Add(noteContext.ApplyNoteChoice(noteChoice));
+ }
+
+ return new Chord(
+ notes,
+ chordContext,
+ chordChoice
+ );
+ }
+}
diff --git a/src/BaroquenMelody.Library/Extensions/NoteContextExtensions.cs b/src/BaroquenMelody.Library/Extensions/NoteContextExtensions.cs
new file mode 100644
index 00000000..10386ba6
--- /dev/null
+++ b/src/BaroquenMelody.Library/Extensions/NoteContextExtensions.cs
@@ -0,0 +1,33 @@
+using BaroquenMelody.Library.Composition;
+using BaroquenMelody.Library.Composition.Choices;
+using BaroquenMelody.Library.Composition.Contexts;
+using BaroquenMelody.Library.Composition.Enums;
+
+namespace BaroquenMelody.Library.Extensions;
+
+internal static class NoteContextExtensions
+{
+ ///
+ /// Applies the given to the given to generate the next note.
+ ///
+ /// The note context.
+ /// The note choice.
+ /// The next note.
+ /// Thrown when the given has an invalid .
+ public static Note ApplyNoteChoice(this NoteContext noteContext, NoteChoice noteChoice)
+ {
+ var pitch = noteChoice.Motion switch
+ {
+ NoteMotion.Ascending => noteContext.Pitch + noteChoice.PitchChange,
+ NoteMotion.Descending => noteContext.Pitch - noteChoice.PitchChange,
+ NoteMotion.Oblique => noteContext.Pitch,
+ _ => throw new ArgumentOutOfRangeException(nameof(noteChoice))
+ };
+
+ return new Note(
+ (byte)pitch,
+ noteContext,
+ noteChoice
+ );
+ }
+}
diff --git a/tests/BaroquenMelody.Library.Tests/Extensions/ChordContextExtensionsTests.cs b/tests/BaroquenMelody.Library.Tests/Extensions/ChordContextExtensionsTests.cs
new file mode 100644
index 00000000..72d2fe3a
--- /dev/null
+++ b/tests/BaroquenMelody.Library.Tests/Extensions/ChordContextExtensionsTests.cs
@@ -0,0 +1,50 @@
+using BaroquenMelody.Library.Composition;
+using BaroquenMelody.Library.Composition.Choices;
+using BaroquenMelody.Library.Composition.Contexts;
+using BaroquenMelody.Library.Composition.Enums;
+using BaroquenMelody.Library.Extensions;
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace BaroquenMelody.Library.Tests.Extensions;
+
+[TestFixture]
+internal sealed class ChordContextExtensionsTests
+{
+ [Test]
+ public void ApplyChordChoice_ShouldApplyNoteChoicesToChord()
+ {
+ // arrange
+ var chordContext = new ChordContext(new[]
+ {
+ new NoteContext(Voice.Soprano, 60, NoteMotion.Oblique, NoteSpan.Leap),
+ new NoteContext(Voice.Alto, 55, NoteMotion.Oblique, NoteSpan.Leap),
+ new NoteContext(Voice.Tenor, 50, NoteMotion.Oblique, NoteSpan.Leap),
+ new NoteContext(Voice.Bass, 45, NoteMotion.Oblique, NoteSpan.Leap)
+ });
+
+ var chordChoice = new ChordChoice(new HashSet
+ {
+ new(Voice.Soprano, NoteMotion.Ascending, 2),
+ new(Voice.Alto, NoteMotion.Descending, 1),
+ new(Voice.Tenor, NoteMotion.Oblique, 0),
+ new(Voice.Bass, NoteMotion.Ascending, 3)
+ });
+
+ var expectedNotes = new HashSet
+ {
+ new(62, chordContext[Voice.Soprano], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Soprano)),
+ new(54, chordContext[Voice.Alto], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Alto)),
+ new(50, chordContext[Voice.Tenor], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Tenor)),
+ new(48, chordContext[Voice.Bass], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Bass))
+ };
+
+ // act
+ var resultChord = chordContext.ApplyChordChoice(chordChoice);
+
+ // assert
+ resultChord.Notes.Should().BeEquivalentTo(expectedNotes);
+ resultChord.ChordContext.Should().Be(chordContext);
+ resultChord.ChordChoice.Should().Be(chordChoice);
+ }
+}
diff --git a/tests/BaroquenMelody.Library.Tests/Extensions/NoteContextExtensionsTests.cs b/tests/BaroquenMelody.Library.Tests/Extensions/NoteContextExtensionsTests.cs
new file mode 100644
index 00000000..b420c293
--- /dev/null
+++ b/tests/BaroquenMelody.Library.Tests/Extensions/NoteContextExtensionsTests.cs
@@ -0,0 +1,49 @@
+using BaroquenMelody.Library.Composition.Choices;
+using BaroquenMelody.Library.Composition.Contexts;
+using BaroquenMelody.Library.Composition.Enums;
+using BaroquenMelody.Library.Extensions;
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace BaroquenMelody.Library.Tests.Extensions;
+
+[TestFixture]
+internal sealed class NoteContextExtensionsTests
+{
+ [Test]
+ [TestCase(60, 2, NoteMotion.Ascending, 62)]
+ [TestCase(60, 2, NoteMotion.Descending, 58)]
+ [TestCase(60, 0, NoteMotion.Oblique, 60)]
+ public void ApplyNoteChoice_ShouldCalculateCorrectPitch(
+ byte startPitch,
+ byte pitchChange,
+ NoteMotion noteMotion,
+ byte expectedPitch)
+ {
+ // arrange
+ var noteContext = new NoteContext(Voice.Soprano, startPitch, NoteMotion.Oblique, NoteSpan.None);
+ var noteChoice = new NoteChoice(Voice.Soprano, noteMotion, pitchChange);
+
+ // act
+ var resultNote = noteContext.ApplyNoteChoice(noteChoice);
+
+ // assert
+ resultNote.Pitch.Should().Be(expectedPitch);
+ resultNote.NoteContext.Should().BeEquivalentTo(noteContext);
+ resultNote.NoteChoice.Should().BeEquivalentTo(noteChoice);
+ }
+
+ [Test]
+ public void ApplyNoteChoice_WithUnsupportedMotion_ShouldThrowArgumentOutOfRangeException()
+ {
+ // arrange
+ var noteContext = new NoteContext(Voice.Soprano, 60, NoteMotion.Oblique, NoteSpan.None);
+ var noteChoice = new NoteChoice(Voice.Soprano, (NoteMotion)55, 5);
+
+ // act
+ var act = () => noteContext.ApplyNoteChoice(noteChoice);
+
+ // assert
+ act.Should().Throw();
+ }
+}