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
markheath 14 years ago
parent
commit
852bb0b5bf
  1. 40
      NAudioWpfDemo/DrumMachineDemo/DrumKit.cs
  2. 4
      NAudioWpfDemo/DrumMachineDemo/DrumMachineDemoViewModel.cs
  3. 61
      NAudioWpfDemo/DrumMachineDemo/DrumPatternSampleProvider.cs
  4. 107
      NAudioWpfDemo/DrumMachineDemo/PatternSequencer.cs
  5. 242
      NAudioWpfDemo/DrumMachineDemo/PatternSequencerTests.cs
  6. 14
      NAudioWpfDemo/NAudioWpfDemo.csproj
  7. 4
      NAudioWpfDemo/packages.config

40
NAudioWpfDemo/DrumMachineDemo/DrumKit.cs

@ -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]);
}
}
}

4
NAudioWpfDemo/DrumMachineDemo/DrumMachineDemoViewModel.cs

@ -13,7 +13,7 @@ namespace NAudioWpfDemo.DrumMachineDemo
{
private IWavePlayer waveOut;
private DrumPattern pattern;
private PatternSequencer patternSequencer;
private DrumPatternSampleProvider patternSequencer;
private int tempo;
public ICommand PlayCommand { get; private set; }
public ICommand StopCommand { get; private set; }
@ -37,7 +37,7 @@ namespace NAudioWpfDemo.DrumMachineDemo
Stop();
}
waveOut = new WaveOut();
this.patternSequencer = new PatternSequencer(pattern);
this.patternSequencer = new DrumPatternSampleProvider(pattern);
this.patternSequencer.Tempo = tempo;
IWaveProvider wp = new SampleToWaveProvider(patternSequencer);
waveOut.Init(wp);

61
NAudioWpfDemo/DrumMachineDemo/DrumPatternSampleProvider.cs

@ -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;
}
}
}

107
NAudioWpfDemo/DrumMachineDemo/PatternSequencer.cs

@ -2,44 +2,23 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NAudio.Wave;
using NAudio.Midi;
using System.Diagnostics;
using NAudio.Wave.SampleProviders;
namespace NAudioWpfDemo.DrumMachineDemo
{
class PatternSequencer : ISampleProvider
class PatternSequencer
{
private long position;
private long patternLength;
private MixingSampleProvider mixer;
private WaveFormat waveFormat;
private List<SampleSource> sampleSources;
private readonly DrumPattern drumPattern;
private readonly DrumKit drumKit;
private int tempo;
private int samplesPerStep;
private DrumPattern pattern;
private int tempo;
public PatternSequencer(DrumPattern pattern)
public PatternSequencer(DrumPattern drumPattern, DrumKit kit)
{
this.pattern = pattern;
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);
int sampleRate = openHatsSample.SampleWaveFormat.SampleRate;
int channels = 2; // always stereo for now
this.waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels);
this.Tempo = 100;
mixer = new MixingSampleProvider(waveFormat);
this.drumKit = kit;
this.drumPattern = drumPattern;
this.Tempo = 120;
}
public int Tempo
@ -51,65 +30,43 @@ namespace NAudioWpfDemo.DrumMachineDemo
set
{
this.tempo = value;
int samplesPerBeat = this.WaveFormat.Channels * (this.WaveFormat.SampleRate * 60) / tempo;
int samplesPerBeat = (this.drumKit.WaveFormat.Channels * this.drumKit.WaveFormat.SampleRate * 60) / tempo;
this.samplesPerStep = samplesPerBeat / 4;
this.patternLength = samplesPerStep * pattern.Steps;
position = position % patternLength;
}
}
public WaveFormat WaveFormat
{
get { return waveFormat; }
}
private int currentStep = 0;
private double patternPosition = 0;
private int GetPositionForStep(int step)
public IList<MusicSampleProvider> GetNextMixerInputs(int sampleCount)
{
return step * samplesPerStep;
}
private int GetStepFromPosition(long position)
{
return (int)(position / samplesPerStep) % pattern.Steps;
}
public int Read(float[] buffer, int offset, int count)
{
// find which steps start in this buffer
int startStep = GetStepFromPosition(position);
int endStep = GetStepFromPosition(position+count-1);
for (int step = startStep; step <= endStep; step++)
List<MusicSampleProvider> mixerInputs = new List<MusicSampleProvider>();
int samplePos = 0;
while (samplePos < sampleCount)
{
for (int note = 0; note < pattern.Notes; note++)
for (int note = 0; note < drumPattern.Notes; note++)
{
byte velocity = pattern[note, step];
if (velocity > 0)
if (drumPattern[note, currentStep] != 0)
{
MusicSampleProvider sp = new MusicSampleProvider(sampleSources[note]);
int delayBy = (int)(GetPositionForStep(step) - position);
if (delayBy < 0) delayBy += (int)patternLength;
sp.DelayBy = delayBy;
ISampleProvider mixerInput = sp;
if (mixerInput.WaveFormat.Channels == 1)
{
mixerInput = new MonoToStereoSampleProvider(mixerInput);
}
mixer.AddMixerInput(mixerInput);
var sampleProvider = drumKit.GetSampleProvider(note);
Debug.WriteLine("beat at step {0}, patternPostion={1}", currentStep, patternPosition);
double offsetFromCurrent = (currentStep - patternPosition);
if (offsetFromCurrent < 0) offsetFromCurrent += drumPattern.Steps;
sampleProvider.DelayBy = (int)(this.samplesPerStep * offsetFromCurrent);
mixerInputs.Add(sampleProvider);
}
}
}
// now we just need to read from the mixer
var samplesRead = mixer.Read(buffer, offset, count);
if (samplesRead < count)
samplePos += samplesPerStep;
currentStep++;
currentStep = currentStep % drumPattern.Steps;
}
this.patternPosition += ((double)sampleCount / samplesPerStep);
if (this.patternPosition > drumPattern.Steps)
{
Array.Clear(buffer, offset + samplesRead, count - samplesRead);
samplesRead = count;
this.patternPosition -= drumPattern.Steps;
}
position += samplesRead;
position = position % patternLength; // loop indefinitely
return samplesRead;
return mixerInputs;
}
}
}

242
NAudioWpfDemo/DrumMachineDemo/PatternSequencerTests.cs

@ -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);
}
}
}

14
NAudioWpfDemo/NAudioWpfDemo.csproj

@ -54,8 +54,18 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Reference Include="nunit.framework">
<HintPath>..\packages\NUnit.2.5.10.11092\lib\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="nunit.mocks">
<HintPath>..\packages\NUnit.2.5.10.11092\lib\nunit.mocks.dll</HintPath>
</Reference>
<Reference Include="pnunit.framework">
<HintPath>..\packages\NUnit.2.5.10.11092\lib\pnunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Core">
@ -145,6 +155,7 @@
<Compile Include="AudioPlaybackDemo\IVisualizationPlugin.cs" />
<Compile Include="AudioPlaybackDemo\PolygonWaveFormVisualization.cs" />
<Compile Include="AudioPlaybackDemo\SpectrumAnalyzerVisualization.cs" />
<Compile Include="DrumMachineDemo\DrumKit.cs" />
<Compile Include="DrumMachineDemo\DrumMachineDemoPlugin.cs" />
<Compile Include="DrumMachineDemo\DrumMachineDemoView.xaml.cs">
<DependentUpon>DrumMachineDemoView.xaml</DependentUpon>
@ -155,7 +166,9 @@
<DependentUpon>DrumPatternEditor.xaml</DependentUpon>
</Compile>
<Compile Include="DrumMachineDemo\MusicSampleProvider.cs" />
<Compile Include="DrumMachineDemo\DrumPatternSampleProvider.cs" />
<Compile Include="DrumMachineDemo\PatternSequencer.cs" />
<Compile Include="DrumMachineDemo\PatternSequencerTests.cs" />
<Compile Include="DrumMachineDemo\SampleSource.cs" />
<Compile Include="IModule.cs" />
<Compile Include="MainWindow.xaml.cs">
@ -201,6 +214,7 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="app.config" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>

4
NAudioWpfDemo/packages.config

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NUnit" version="2.5.10.11092" />
</packages>
Loading…
Cancel
Save