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(); + } +}