mirror of https://github.com/naudio/NAudio.git
Browse Source
Drum machine demo now can change tempo (still needs some work to reposition correctly in pattern after tempo change). Created some unit tests for the pattern sequencer.
pull/1/head
Drum machine demo now can change tempo (still needs some work to reposition correctly in pattern after tempo change). Created some unit tests for the pattern sequencer.
pull/1/head

7 changed files with 395 additions and 77 deletions
-
40NAudioWpfDemo/DrumMachineDemo/DrumKit.cs
-
4NAudioWpfDemo/DrumMachineDemo/DrumMachineDemoViewModel.cs
-
61NAudioWpfDemo/DrumMachineDemo/DrumPatternSampleProvider.cs
-
107NAudioWpfDemo/DrumMachineDemo/PatternSequencer.cs
-
242NAudioWpfDemo/DrumMachineDemo/PatternSequencerTests.cs
-
14NAudioWpfDemo/NAudioWpfDemo.csproj
-
4NAudioWpfDemo/packages.config
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using NAudio.Wave; |
|||
using NAudio.Wave.SampleProviders; |
|||
|
|||
namespace NAudioWpfDemo.DrumMachineDemo |
|||
{ |
|||
class DrumKit |
|||
{ |
|||
private List<SampleSource> sampleSources; |
|||
private WaveFormat waveFormat; |
|||
|
|||
public DrumKit() |
|||
{ |
|||
SampleSource kickSample = SampleSource.CreateFromWaveFile("Samples\\kick-trimmed.wav"); |
|||
SampleSource snareSample = SampleSource.CreateFromWaveFile("Samples\\snare-trimmed.wav"); |
|||
SampleSource closedHatsSample = SampleSource.CreateFromWaveFile("Samples\\closed-hat-trimmed.wav"); |
|||
SampleSource openHatsSample = SampleSource.CreateFromWaveFile("Samples\\open-hat-trimmed.wav"); |
|||
sampleSources = new List<SampleSource>(); |
|||
|
|||
sampleSources.Add(kickSample); |
|||
sampleSources.Add(snareSample); |
|||
sampleSources.Add(closedHatsSample); |
|||
sampleSources.Add(openHatsSample); |
|||
this.waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(openHatsSample.SampleWaveFormat.SampleRate, openHatsSample.SampleWaveFormat.Channels); |
|||
} |
|||
|
|||
public virtual WaveFormat WaveFormat |
|||
{ |
|||
get { return waveFormat; } |
|||
} |
|||
|
|||
public MusicSampleProvider GetSampleProvider(int note) |
|||
{ |
|||
return new MusicSampleProvider(this.sampleSources[note]); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,61 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using NAudio.Wave; |
|||
using NAudio.Midi; |
|||
using NAudio.Wave.SampleProviders; |
|||
|
|||
namespace NAudioWpfDemo.DrumMachineDemo |
|||
{ |
|||
class DrumPatternSampleProvider : ISampleProvider |
|||
{ |
|||
private MixingSampleProvider mixer; |
|||
private WaveFormat waveFormat; |
|||
private PatternSequencer sequencer; |
|||
|
|||
public DrumPatternSampleProvider(DrumPattern pattern) |
|||
{ |
|||
var kit = new DrumKit(); |
|||
this.sequencer = new PatternSequencer(pattern, kit); |
|||
this.waveFormat = kit.WaveFormat; |
|||
mixer = new MixingSampleProvider(waveFormat); |
|||
} |
|||
|
|||
public int Tempo |
|||
{ |
|||
get |
|||
{ |
|||
return sequencer.Tempo; |
|||
} |
|||
set |
|||
{ |
|||
sequencer.Tempo = value; |
|||
} |
|||
} |
|||
|
|||
public WaveFormat WaveFormat |
|||
{ |
|||
get { return waveFormat; } |
|||
} |
|||
|
|||
public int Read(float[] buffer, int offset, int count) |
|||
{ |
|||
|
|||
foreach (var mixerInput in sequencer.GetNextMixerInputs(count)) |
|||
{ |
|||
//mixerInput = new MonoToStereoSampleProvider(mixerInput);
|
|||
mixer.AddMixerInput(mixerInput); |
|||
} |
|||
|
|||
// now we just need to read from the mixer
|
|||
var samplesRead = mixer.Read(buffer, offset, count); |
|||
if (samplesRead < count) |
|||
{ |
|||
Array.Clear(buffer, offset + samplesRead, count - samplesRead); |
|||
samplesRead = count; |
|||
} |
|||
return samplesRead; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,242 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using NUnit.Framework; |
|||
using System.Diagnostics; |
|||
using NAudio.Wave; |
|||
|
|||
namespace NAudioWpfDemo.DrumMachineDemo |
|||
{ |
|||
[TestFixture] |
|||
public class PatternSequencerTests |
|||
{ |
|||
class TestKit : DrumKit |
|||
{ |
|||
private WaveFormat wf; |
|||
public TestKit() |
|||
{ |
|||
} |
|||
|
|||
public TestKit(int sampleRate) |
|||
{ |
|||
this.wf = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 1); |
|||
} |
|||
|
|||
public override WaveFormat WaveFormat |
|||
{ |
|||
get |
|||
{ |
|||
if (this.wf != null) |
|||
{ |
|||
return wf; |
|||
} |
|||
return base.WaveFormat; |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Test] |
|||
public void Pattern_Sequencer_Should_Return_No_Mixer_Inputs_For_An_Empty_Pattern() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
var sequencer = new PatternSequencer(pattern, new TestKit()); |
|||
var mixerInputs = sequencer.GetNextMixerInputs(100); |
|||
Assert.AreEqual(0, mixerInputs.Count()); |
|||
} |
|||
|
|||
[Test] |
|||
public void Pattern_Sequencer_Should_Return_A_Non_Delayed_Mixer_Input_For_A_Beat_At_Position_Zero() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 0] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit()); |
|||
var mixerInputs = sequencer.GetNextMixerInputs(100); |
|||
|
|||
Assert.AreEqual(1, mixerInputs.Count()); |
|||
Assert.AreEqual(0, mixerInputs.First().DelayBy); |
|||
} |
|||
|
|||
[Test] |
|||
public void Pattern_Sequencer_Should_Set_DelayBy_On_Mixer_Inputs_That_Are_Not_At_The_Start() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 1] = 127; |
|||
|
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(2); |
|||
|
|||
Assert.AreEqual(1, mixerInputs.Count()); |
|||
Assert.AreEqual(1, mixerInputs.First().DelayBy); |
|||
} |
|||
|
|||
private int CalculateSampleRateForTempo(int tempo, int samplesPerStep = 1) |
|||
{ |
|||
int stepsPerBeat = 4; |
|||
int stepsPerMinute = tempo * stepsPerBeat; |
|||
int stepsPerSecond = stepsPerMinute / 60; |
|||
return stepsPerSecond * samplesPerStep; // an imaginary low sample rate where there is one sample per beat
|
|||
} |
|||
|
|||
[Test] |
|||
public void Pattern_Sequencer_Should_Not_Return_Mixer_Inputs_For_Steps_That_Are_Outside_The_Requested_Range() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 2] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(2); |
|||
|
|||
Assert.AreEqual(0, mixerInputs.Count()); |
|||
} |
|||
|
|||
[Test] |
|||
public void Pattern_Sequencer_Should_Loop_Around_After_Reaching_The_End_Of_The_Pattern() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 2] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(32); // twice through
|
|||
|
|||
Assert.AreEqual(2, mixerInputs.Count()); |
|||
} |
|||
|
|||
[Test] |
|||
public void Pattern_Sequencer_Should_Carry_On_From_Where_It_Left_Off_On_Second_Call() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 1] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
// first read gets nothing
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(1); |
|||
Assert.AreEqual(0, mixerInputs.Count(), "First read"); |
|||
|
|||
// second read gets something
|
|||
mixerInputs = sequencer.GetNextMixerInputs(1); |
|||
Assert.AreEqual(1, mixerInputs.Count(), "Second Read"); |
|||
} |
|||
|
|||
[Test] |
|||
public void DelayBy_Values_Are_Relative_To_Current_Position_On_Subsequent_Calls() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 6] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
// first read gets nothing
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(3); |
|||
Assert.AreEqual(0, mixerInputs.Count(), "First read"); |
|||
|
|||
// second read gets something
|
|||
mixerInputs = sequencer.GetNextMixerInputs(4); |
|||
Assert.AreEqual(1, mixerInputs.Count(), "Second Read"); |
|||
Assert.AreEqual(3, mixerInputs.First().DelayBy, "DelayBy"); |
|||
} |
|||
|
|||
[Test] |
|||
public void Multiple_DelayBy_Values_Are_All_Relative_To_Current_Position_Before_Calling_GetNextMixerInputs() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 6] = 127; |
|||
pattern[0, 7] = 127; |
|||
pattern[0, 8] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
// first read gets nothing
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(3); |
|||
Assert.AreEqual(0, mixerInputs.Count, "First read"); |
|||
|
|||
// second read gets something
|
|||
mixerInputs = sequencer.GetNextMixerInputs(10); |
|||
Assert.AreEqual(3, mixerInputs.Count, "Second Read"); |
|||
Assert.AreEqual(3, mixerInputs[0].DelayBy, "Inputs[0].DelayBy"); |
|||
Assert.AreEqual(4, mixerInputs[1].DelayBy, "Inputs[1].DelayBy"); |
|||
Assert.AreEqual(5, mixerInputs[2].DelayBy, "Inputs[2].DelayBy"); |
|||
} |
|||
|
|||
[Test] |
|||
public void DelayBy_Values_Should_Be_Correct_On_Wraparound() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 0] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
// read 12 of the 16 steps
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(12); |
|||
Assert.AreEqual(1, mixerInputs.Count, "First read"); |
|||
|
|||
// read 12 more - will wrap around
|
|||
mixerInputs = sequencer.GetNextMixerInputs(12); |
|||
Assert.AreEqual(1, mixerInputs.Count, "Second Read"); |
|||
Assert.AreEqual(4, mixerInputs[0].DelayBy, "Inputs[0].DelayBy"); |
|||
} |
|||
|
|||
[Test] |
|||
public void DelayBy_Values_Should_Be_Correct_On_Subsequent_Read_After_Wraparound() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
pattern[0, 0] = 127; |
|||
pattern[0, 10] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
|
|||
// read 12 of the 16 steps (ends at pos 12)
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(12); |
|||
Assert.AreEqual(2, mixerInputs.Count, "First read"); |
|||
|
|||
// read 12 more - will wrap around (ends at pos 8)
|
|||
mixerInputs = sequencer.GetNextMixerInputs(12); |
|||
Assert.AreEqual(1, mixerInputs.Count, "Second Read"); |
|||
Assert.AreEqual(4, mixerInputs[0].DelayBy, "Inputs[0].DelayBy"); |
|||
|
|||
// read 12 more - (start from pos 8, ends at pos 4)
|
|||
mixerInputs = sequencer.GetNextMixerInputs(12); |
|||
Assert.AreEqual(2, mixerInputs[0].DelayBy, "3rd Read Inputs[0].DelayBy"); |
|||
Assert.AreEqual(8, mixerInputs[1].DelayBy, "3rd Read Inputs[1].DelayBy"); |
|||
} |
|||
|
|||
[Test] |
|||
public void Tempo_Can_Be_Changed() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
for (int n = 0; n < pattern.Steps; n++) |
|||
pattern[0, n] = 127; |
|||
|
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
sequencer.Tempo = 60; // half tempo
|
|||
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(16); |
|||
// tempo is half, so only half the beats should get read
|
|||
Assert.AreEqual(8, mixerInputs.Count, "First read"); |
|||
} |
|||
|
|||
[Test] |
|||
public void When_Tempo_Is_Halved_DelayBy_Is_Doubled() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum" }, 16); |
|||
for (int n = 0; n < pattern.Steps; n++) |
|||
pattern[0, n] = 127; |
|||
|
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
sequencer.Tempo = 60; // half tempo
|
|||
|
|||
var mixerInputs = sequencer.GetNextMixerInputs(16); |
|||
// tempo is half, so only half the beats should get read
|
|||
Assert.AreEqual(2, mixerInputs[1].DelayBy, "First beat DelayBy"); |
|||
} |
|||
|
|||
[Test] |
|||
public void Pattern_Sequencer_Should_Return_Mixer_Inputs_for_Beats_On_Any_Note() |
|||
{ |
|||
var pattern = new DrumPattern(new string[] { "Bass Drum", "Snare Drum" }, 16); |
|||
pattern[1, 5] = 127; |
|||
var sequencer = new PatternSequencer(pattern, new TestKit(CalculateSampleRateForTempo(120))); |
|||
var mixerInputs = sequencer.GetNextMixerInputs(16); |
|||
// tempo is half, so only half the beats should get read
|
|||
Assert.AreEqual(1, mixerInputs.Count); |
|||
|
|||
} |
|||
} |
|||
} |
@ -0,0 +1,4 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<packages> |
|||
<package id="NUnit" version="2.5.10.11092" /> |
|||
</packages> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue