Search code examples
c#wavnaudiomixing

How to mix thousands of WAV files into a single file using NAudio's MixingSampleProvider?


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.


Solution

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