Skip to content

Commit

Permalink
Notes, Chords, and Extensions (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
wbaldoumas authored Nov 4, 2023
1 parent eaaf27b commit 650bcd1
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/BaroquenMelody.Library/Composition/Chord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using BaroquenMelody.Library.Composition.Choices;
using BaroquenMelody.Library.Composition.Contexts;

namespace BaroquenMelody.Library.Composition;

/// <summary>
/// Represents a chord in a composition.
/// </summary>
/// <param name="Notes"> The notes which make up the chord. </param>
/// <param name="ChordContext"> The previous chord context from which this chord was generated. </param>
/// <param name="ChordChoice"> The chord choice which was used to generate this chord. </param>
internal sealed record Chord(
ISet<Note> Notes,
ChordContext ChordContext,
ChordChoice ChordChoice
);
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace BaroquenMelody.Library.Composition.Contexts;
using BaroquenMelody.Library.Composition.Enums;

namespace BaroquenMelody.Library.Composition.Contexts;

/// <summary>
/// Represents the note contexts for the voices in a given chord used to arrive at the current chord.
Expand All @@ -16,6 +18,8 @@ public IList<NoteContext> 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)
Expand Down
16 changes: 16 additions & 0 deletions src/BaroquenMelody.Library/Composition/Note.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using BaroquenMelody.Library.Composition.Choices;
using BaroquenMelody.Library.Composition.Contexts;

namespace BaroquenMelody.Library.Composition;

/// <summary>
/// Represents a note in a composition.
/// </summary>
/// <param name="Pitch"> The pitch of the note. </param>
/// <param name="NoteContext"> The previous note context from which this note was generated. </param>
/// <param name="NoteChoice"> The note choice which was used to generate this note. </param>
internal sealed record Note(
byte Pitch,
NoteContext NoteContext,
NoteChoice NoteChoice
);
32 changes: 32 additions & 0 deletions src/BaroquenMelody.Library/Extensions/ChordContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Applies the given <see cref="ChordChoice"/> to the given <see cref="ChordContext"/> to generate the next chord.
/// </summary>
/// <param name="chordContext"> The chord context. </param>
/// <param name="chordChoice"> The chord choice. </param>
/// <returns> The next chord. </returns>
public static Chord ApplyChordChoice(this ChordContext chordContext, ChordChoice chordChoice)
{
var notes = new HashSet<Note>();

foreach (var noteChoice in chordChoice.NoteChoices)
{
var noteContext = chordContext[noteChoice.Voice];

notes.Add(noteContext.ApplyNoteChoice(noteChoice));
}

return new Chord(
notes,
chordContext,
chordChoice
);
}
}
33 changes: 33 additions & 0 deletions src/BaroquenMelody.Library/Extensions/NoteContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Applies the given <see cref="NoteChoice"/> to the given <see cref="NoteContext"/> to generate the next note.
/// </summary>
/// <param name="noteContext"> The note context. </param>
/// <param name="noteChoice"> The note choice. </param>
/// <returns> The next note. </returns>
/// <exception cref="ArgumentOutOfRangeException"> Thrown when the given <see cref="NoteChoice"/> has an invalid <see cref="NoteMotion"/>. </exception>
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
);
}
}
Original file line number Diff line number Diff line change
@@ -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<NoteChoice>
{
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<Note>
{
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);
}
}
Original file line number Diff line number Diff line change
@@ -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<ArgumentOutOfRangeException>();
}
}

0 comments on commit 650bcd1

Please sign in to comment.