I'm looking for a way to mix thousands of WAV files into a single file using NAudio's MixingSampleProvider
. I'm building a drum sampler and I'd like to be able to create a whole song (based on MIDI information) that could then be exported into a single WAV file.
The issue I'm running into is that the MixingSampleProvider
has a limit of 1024 sources and throws and exception saying Too many mixer inputs
if that limit is reached. I'm sure this limit is there for a reason, I'd like to know how to achieve my goal despite it.
I've searched through the NAudio demos and Mark Heath's blog, but I haven't found exactly what I need there.
I was thinking I could split the song into smaller segments (under 1024 sampler inputs) and merge the separate parts afterwards. Is that the way to go, or is there a better one? Thanks for any advice.
Here's a part of my code:
public class DrumSampler
{
private readonly MixingSampleProvider _mixer;
private readonly Dictionary<string, SampleSource> _cachedSamples = new();
public DrumSampler()
{
var waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(44100, 2);
_mixer = new MixingSampleProvider(waveFormat);
LoadSamples();
}
private void LoadSamples()
{
LoadSample("kick", @"C:\Samples\kick.wav");
LoadSample("snare", @"C:\Samples\snare.wav");
LoadSample("crash", @"C:\Samples\crash.wav");
}
private void LoadSample(string key, string filePath)
{
_cachedSamples.Add(key, SampleSource.CreateFromWaveFile(filePath, _mixer.WaveFormat));
}
public void ExportSong()
{
AddDrums();
WaveFileWriter.CreateWaveFile16("song.wav", _mixer);
}
private void AddDrums()
{
//simulate adding drum samples based on MIDI information
for (int i = 0; i < 1000; i++)
{
var sample = _cachedSamples["kick"];
var delayed = new DelayedSampleProvider(sample, TimeSpan.FromSeconds(123));
_mixer.AddMixerInput(delayed);
}
}
}
The SampleSource
implementation is taken from NAudio's DrumMachineDemo.
The DelayedSampleProvider
implementation is inspired by NAudio's OffsetSampleProvider.
I've been able to resolve this issue by splitting the song into smaller chungs (under 1024 mixer inputs), each chunk having its own MixingSampleProvider
and then concatenating all the chungs using the ConcatenatingSampleProvider
.
It works, however, I'd like to know if there's a more elegant solution...
Here's my code:
public class DrumSampler
{
private readonly WaveFormat _waveFormat;
private readonly Dictionary<string, SampleSource> _cachedSamples = new();
public DrumSampler()
{
_waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(44100, 2);
LoadSamples();
}
private void LoadSamples()
{
LoadSample("kick", @"C:\Samples\kick.wav");
LoadSample("snare", @"C:\Samples\snare.wav");
LoadSample("crash", @"C:\Samples\crash.wav");
}
private void LoadSample(string key, string filePath)
{
_cachedSamples.Add(key, SampleSource.CreateFromWaveFile(filePath, _mixer.WaveFormat));
}
public void ExportSong()
{
var result = GenerateDrumAudio();
WaveFileWriter.CreateWaveFile16("song.wav", result);
}
private ISampleProvider GenerateDrumAudio()
{
//simulate reading drum hits from MIDI
var drumHits = Enumerable.Range(0, 10_000).ToList();
//split drum hits into chunks of 1024 (max input limit of the mixing sample provider)
var drumHitChunks = drumHits.Chunk(1024);
//store all chunk mixers in a list
var mixers = new List<MixingSampleProvider>();
foreach (var drumHitChunk in drumHitChunks)
{
//for each chunk: create a mixer, add drum hits to it and add it to the mixers list
var mixer = new MixingSampleProvider(_waveFormat);
foreach (var drumHit in drumHitChunk)
{
var sample = _cachedSamples["kick"];
var delayed = new DelayedSampleProvider(sample, TimeSpan.FromSeconds(123));
mixer.AddMixerInput(delayed);
}
mixers.Add(mixer);
}
//concatenate all chunk mixers into a single ISampleProvider
var mixersConcatenated = new ConcatenatingSampleProvider(mixers);
return mixersConcatenated;
}
}