Skip to content

Commit 1f973f7

Browse files
authored
Smarter composition strategy (#33)
1 parent 650bcd1 commit 1f973f7

File tree

7 files changed

+201
-20
lines changed

7 files changed

+201
-20
lines changed
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
namespace BaroquenMelody.Library.Composition.Configurations;
1+
using BaroquenMelody.Library.Composition.Enums;
2+
3+
namespace BaroquenMelody.Library.Composition.Configurations;
24

35
/// <summary>
46
/// The composition configuration.
57
/// </summary>
68
/// <param name="VoiceConfigurations"> The voice configurations to be used in the composition. </param>
7-
internal record CompositionConfiguration(
8-
ISet<VoiceConfiguration> VoiceConfigurations
9-
);
9+
internal record CompositionConfiguration(ISet<VoiceConfiguration> VoiceConfigurations)
10+
{
11+
public bool IsPitchInVoiceRange(Voice voice, byte pitch) => pitch >= GetMinPitch(voice) && pitch <= GetMaxPitch(voice);
12+
13+
private byte GetMinPitch(Voice voice) => VoiceConfigurations.First(x => x.Voice == voice).MinPitch;
14+
15+
private byte GetMaxPitch(Voice voice) => VoiceConfigurations.First(x => x.Voice == voice).MaxPitch;
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
using BaroquenMelody.Library.Composition.Choices;
22
using BaroquenMelody.Library.Composition.Contexts;
3+
using BaroquenMelody.Library.Composition.Enums;
34

45
namespace BaroquenMelody.Library.Composition;
56

67
/// <summary>
78
/// Represents a note in a composition.
89
/// </summary>
910
/// <param name="Pitch"> The pitch of the note. </param>
11+
/// <param name="Voice"> The voice of the note. </param>
1012
/// <param name="NoteContext"> The previous note context from which this note was generated. </param>
1113
/// <param name="NoteChoice"> The note choice which was used to generate this note. </param>
1214
internal sealed record Note(
1315
byte Pitch,
16+
Voice Voice,
1417
NoteContext NoteContext,
1518
NoteChoice NoteChoice
1619
);

src/BaroquenMelody.Library/Composition/Strategies/CompositionStrategy.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using BaroquenMelody.Library.Composition.Choices;
2+
using BaroquenMelody.Library.Composition.Configurations;
23
using BaroquenMelody.Library.Composition.Contexts;
4+
using BaroquenMelody.Library.Extensions;
35
using BaroquenMelody.Library.Random;
46
using System.Collections;
57
using System.Numerics;
@@ -17,25 +19,40 @@ internal sealed class CompositionStrategy : ICompositionStrategy
1719

1820
private readonly IDictionary<BigInteger, BitArray> _chordContextToChordChoiceMap;
1921

22+
private readonly CompositionConfiguration _compositionConfiguration;
23+
2024
public CompositionStrategy(
2125
IChordChoiceRepository chordChoiceRepository,
2226
IChordContextRepository chordContextRepository,
2327
IRandomTrueIndexSelector randomTrueIndexSelector,
24-
IDictionary<BigInteger, BitArray> chordContextToChordChoiceMap)
28+
IDictionary<BigInteger, BitArray> chordContextToChordChoiceMap,
29+
CompositionConfiguration compositionConfiguration)
2530
{
2631
_chordChoiceRepository = chordChoiceRepository;
2732
_chordContextRepository = chordContextRepository;
2833
_randomTrueIndexSelector = randomTrueIndexSelector;
2934
_chordContextToChordChoiceMap = chordContextToChordChoiceMap;
35+
_compositionConfiguration = compositionConfiguration;
3036
}
3137

3238
public ChordChoice GetNextChordChoice(ChordContext chordContext)
3339
{
3440
var chordContextIndex = _chordContextRepository.GetChordContextIndex(chordContext);
3541
var chordChoiceIndices = _chordContextToChordChoiceMap[chordContextIndex];
36-
var chordChoiceIndex = _randomTrueIndexSelector.SelectRandomTrueIndex(chordChoiceIndices);
3742

38-
return _chordChoiceRepository.GetChordChoice(chordChoiceIndex);
43+
while (true)
44+
{
45+
var chordChoiceIndex = _randomTrueIndexSelector.SelectRandomTrueIndex(chordChoiceIndices);
46+
var chordChoice = _chordChoiceRepository.GetChordChoice(chordChoiceIndex);
47+
var chord = chordContext.ApplyChordChoice(chordChoice);
48+
49+
if (chord.Notes.All(note => _compositionConfiguration.IsPitchInVoiceRange(note.Voice, note.Pitch)))
50+
{
51+
return chordChoice;
52+
}
53+
54+
InvalidateChordChoice(chordContext, chordChoice);
55+
}
3956
}
4057

4158
public void InvalidateChordChoice(ChordContext chordContext, ChordChoice chordChoice)

src/BaroquenMelody.Library/Extensions/NoteContextExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static Note ApplyNoteChoice(this NoteContext noteContext, NoteChoice note
2626

2727
return new Note(
2828
(byte)pitch,
29+
noteChoice.Voice,
2930
noteContext,
3031
noteChoice
3132
);

tests/BaroquenMelody.Library.Tests/Composition/Strategies/CompositionStrategyTests.cs

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using BaroquenMelody.Library.Composition.Choices;
2+
using BaroquenMelody.Library.Composition.Configurations;
23
using BaroquenMelody.Library.Composition.Contexts;
34
using BaroquenMelody.Library.Composition.Enums;
45
using BaroquenMelody.Library.Composition.Strategies;
@@ -14,6 +15,22 @@ namespace BaroquenMelody.Library.Tests.Composition.Strategies;
1415
[TestFixture]
1516
internal sealed class CompositionStrategyTests
1617
{
18+
private const byte MinSopranoPitch = 60;
19+
20+
private const byte MaxSopranoPitch = 72;
21+
22+
private const byte MinAltoPitch = 48;
23+
24+
private const byte MaxAltoPitch = 60;
25+
26+
private const byte MinTenorPitch = 36;
27+
28+
private const byte MaxTenorPitch = 48;
29+
30+
private const byte MinBassPitch = 24;
31+
32+
private const byte MaxBassPitch = 36;
33+
1734
private static readonly BigInteger MockChordContextCount = 5;
1835

1936
private static readonly BigInteger MockChordChoiceCount = 5;
@@ -28,6 +45,8 @@ internal sealed class CompositionStrategyTests
2845

2946
private CompositionStrategy _compositionStrategy = default!;
3047

48+
private CompositionConfiguration _compositionConfiguration = default!;
49+
3150
[SetUp]
3251
public void Setup()
3352
{
@@ -39,11 +58,22 @@ public void Setup()
3958
_mockChordChoiceRepository.Count.Returns(MockChordChoiceCount);
4059
_mockChordContextRepository.Count.Returns(MockChordContextCount);
4160

61+
_compositionConfiguration = new CompositionConfiguration(
62+
new HashSet<VoiceConfiguration>
63+
{
64+
new(Voice.Soprano, MinSopranoPitch, MaxSopranoPitch),
65+
new(Voice.Alto, MinAltoPitch, MaxAltoPitch),
66+
new(Voice.Tenor, MinTenorPitch, MaxTenorPitch),
67+
new(Voice.Bass, MinBassPitch, MaxBassPitch)
68+
}
69+
);
70+
4271
_compositionStrategy = new CompositionStrategy(
4372
_mockChordChoiceRepository,
4473
_mockChordContextRepository,
4574
_mockRandomTrueIndexSelector,
46-
_mockChordContextToChordChoiceMap
75+
_mockChordContextToChordChoiceMap,
76+
_compositionConfiguration
4777
);
4878
}
4979

@@ -54,20 +84,20 @@ public void GetNextChordChoice_returns_random_chord_choice()
5484
var chordContext = new ChordContext(
5585
new List<NoteContext>
5686
{
57-
new(Voice.Soprano, 25, NoteMotion.Ascending, NoteSpan.Step),
58-
new(Voice.Alto, 25, NoteMotion.Ascending, NoteSpan.Step),
59-
new(Voice.Tenor, 25, NoteMotion.Ascending, NoteSpan.Step),
60-
new(Voice.Bass, 25, NoteMotion.Ascending, NoteSpan.Step)
87+
new(Voice.Soprano, MaxSopranoPitch - 2, NoteMotion.Ascending, NoteSpan.Step),
88+
new(Voice.Alto, MinAltoPitch + 2, NoteMotion.Ascending, NoteSpan.Step),
89+
new(Voice.Tenor, MaxTenorPitch - 2, NoteMotion.Ascending, NoteSpan.Step),
90+
new(Voice.Bass, MinBassPitch + 2, NoteMotion.Ascending, NoteSpan.Step)
6191
}
6292
);
6393

6494
var chordChoice = new ChordChoice(
6595
new List<NoteChoice>
6696
{
67-
new(Voice.Soprano, NoteMotion.Ascending, 5),
68-
new(Voice.Alto, NoteMotion.Ascending, 5),
69-
new(Voice.Tenor, NoteMotion.Ascending, 5),
70-
new(Voice.Bass, NoteMotion.Ascending, 5)
97+
new(Voice.Soprano, NoteMotion.Ascending, 1),
98+
new(Voice.Alto, NoteMotion.Ascending, 1),
99+
new(Voice.Tenor, NoteMotion.Ascending, 1),
100+
new(Voice.Bass, NoteMotion.Ascending, 1)
71101
}
72102
);
73103

@@ -97,6 +127,84 @@ public void GetNextChordChoice_returns_random_chord_choice()
97127
);
98128
}
99129

130+
[Test]
131+
public void GetNextChordChoice_invalidates_choices_out_of_voice_range_and_returns_random_chord_choice()
132+
{
133+
// arrange
134+
var chordContext = new ChordContext(
135+
new List<NoteContext>
136+
{
137+
new(Voice.Soprano, MaxSopranoPitch - 2, NoteMotion.Ascending, NoteSpan.Step),
138+
new(Voice.Alto, MinAltoPitch + 2, NoteMotion.Ascending, NoteSpan.Step),
139+
new(Voice.Tenor, MaxTenorPitch - 2, NoteMotion.Ascending, NoteSpan.Step),
140+
new(Voice.Bass, MinBassPitch + 2, NoteMotion.Ascending, NoteSpan.Step)
141+
}
142+
);
143+
144+
var invalidChordChoiceA = new ChordChoice(
145+
new List<NoteChoice>
146+
{
147+
new(Voice.Soprano, NoteMotion.Ascending, 5),
148+
new(Voice.Alto, NoteMotion.Descending, 5),
149+
new(Voice.Tenor, NoteMotion.Ascending, 5),
150+
new(Voice.Bass, NoteMotion.Descending, 5)
151+
}
152+
);
153+
154+
var invalidChordChoiceB = new ChordChoice(
155+
new List<NoteChoice>
156+
{
157+
new(Voice.Soprano, NoteMotion.Ascending, 6),
158+
new(Voice.Alto, NoteMotion.Descending, 6),
159+
new(Voice.Tenor, NoteMotion.Ascending, 6),
160+
new(Voice.Bass, NoteMotion.Descending, 6)
161+
}
162+
);
163+
164+
var validChordChoice = new ChordChoice(
165+
new List<NoteChoice>
166+
{
167+
new(Voice.Soprano, NoteMotion.Ascending, 1),
168+
new(Voice.Alto, NoteMotion.Ascending, 1),
169+
new(Voice.Tenor, NoteMotion.Ascending, 1),
170+
new(Voice.Bass, NoteMotion.Ascending, 1)
171+
}
172+
);
173+
174+
const int chordContextIndex = 3;
175+
const int invalidChordChoiceIndexA = 2;
176+
const int invalidChordChoiceIndexB = 3;
177+
const int validChordChoiceIndex = 4;
178+
179+
var bitArray = new BitArray(new[] { true, true, true, true, true });
180+
181+
_mockChordContextRepository.GetChordContextIndex(chordContext).Returns(chordContextIndex);
182+
_mockChordContextToChordChoiceMap[chordContextIndex].Returns(bitArray);
183+
184+
_mockRandomTrueIndexSelector.SelectRandomTrueIndex(bitArray).Returns(
185+
invalidChordChoiceIndexA,
186+
invalidChordChoiceIndexB,
187+
validChordChoiceIndex
188+
);
189+
190+
_mockChordChoiceRepository.GetChordChoice(invalidChordChoiceIndexA).Returns(invalidChordChoiceA);
191+
_mockChordChoiceRepository.GetChordChoice(invalidChordChoiceIndexB).Returns(invalidChordChoiceB);
192+
_mockChordChoiceRepository.GetChordChoice(validChordChoiceIndex).Returns(validChordChoice);
193+
194+
_mockChordChoiceRepository.GetChordChoiceIndex(invalidChordChoiceA).Returns(invalidChordChoiceIndexA);
195+
_mockChordChoiceRepository.GetChordChoiceIndex(invalidChordChoiceB).Returns(invalidChordChoiceIndexB);
196+
197+
// act
198+
var result = _compositionStrategy.GetNextChordChoice(chordContext);
199+
200+
// assert
201+
result.Should().BeEquivalentTo(validChordChoice);
202+
203+
bitArray[invalidChordChoiceIndexA].Should().BeFalse();
204+
bitArray[invalidChordChoiceIndexB].Should().BeFalse();
205+
bitArray[validChordChoiceIndex].Should().BeTrue();
206+
}
207+
100208
[Test]
101209
public void InvalidateChordChoice_sets_bit_to_false()
102210
{
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using BaroquenMelody.Library.Composition.Configurations;
2+
using BaroquenMelody.Library.Composition.Enums;
3+
using FluentAssertions;
4+
using NUnit.Framework;
5+
6+
namespace BaroquenMelody.Library.Tests.Configuration;
7+
8+
[TestFixture]
9+
internal sealed class CompositionConfigurationTests
10+
{
11+
private const byte MinSopranoPitch = 60;
12+
13+
private const byte MaxSopranoPitch = 72;
14+
15+
private CompositionConfiguration _compositionConfiguration = null!;
16+
17+
[SetUp]
18+
public void SetUp()
19+
{
20+
_compositionConfiguration = new CompositionConfiguration(new HashSet<VoiceConfiguration>
21+
{
22+
new(Voice.Soprano, MinSopranoPitch, MaxSopranoPitch),
23+
new(Voice.Alto, 48, 60),
24+
new(Voice.Tenor, 36, 48),
25+
new(Voice.Bass, 24, 36)
26+
});
27+
}
28+
29+
[Test]
30+
[TestCase(Voice.Soprano, MaxSopranoPitch + 1, false)]
31+
[TestCase(Voice.Soprano, MinSopranoPitch - 1, false)]
32+
[TestCase(Voice.Soprano, MaxSopranoPitch, true)]
33+
[TestCase(Voice.Soprano, MinSopranoPitch, true)]
34+
[TestCase(Voice.Soprano, MaxSopranoPitch - 1, true)]
35+
[TestCase(Voice.Soprano, MinSopranoPitch + 1, true)]
36+
public void IsPitchInVoiceRange_returns_expected_result(
37+
Voice voice,
38+
byte pitch,
39+
bool expectedPitchIsInVoiceRange)
40+
{
41+
var isPitchInVoiceRange = _compositionConfiguration.IsPitchInVoiceRange(voice, pitch);
42+
43+
isPitchInVoiceRange.Should().Be(expectedPitchIsInVoiceRange);
44+
}
45+
}

tests/BaroquenMelody.Library.Tests/Extensions/ChordContextExtensionsTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ public void ApplyChordChoice_ShouldApplyNoteChoicesToChord()
3333

3434
var expectedNotes = new HashSet<Note>
3535
{
36-
new(62, chordContext[Voice.Soprano], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Soprano)),
37-
new(54, chordContext[Voice.Alto], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Alto)),
38-
new(50, chordContext[Voice.Tenor], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Tenor)),
39-
new(48, chordContext[Voice.Bass], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Bass))
36+
new(62, Voice.Soprano, chordContext[Voice.Soprano], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Soprano)),
37+
new(54, Voice.Alto, chordContext[Voice.Alto], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Alto)),
38+
new(50, Voice.Tenor, chordContext[Voice.Tenor], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Tenor)),
39+
new(48, Voice.Bass, chordContext[Voice.Bass], chordChoice.NoteChoices.First(noteChoice => noteChoice.Voice == Voice.Bass))
4040
};
4141

4242
// act

0 commit comments

Comments
 (0)