Posted in:

A few years back I wrote a blog post explaining how you can perform “input driven resampling” with NAudio using the ACM resampler codec installed on your system.

Input driven resampling is when you have blocks of audio you are receiving (say from the microphone) and you want to pass them to the resampler, and ask it to give you however many samples that input turns into.

For playing back audio, generally output driven resampling is what you want, and NAudio has lots of good options available for that. But a common use case for input driven resampling is where you want to save the audio you’re recording from the microphone in a different format (usually a lower sample rate) than the microphone is providing the audio samples in.

Now the input driven ACM resampling option is fine if you’re working on desktop windows which has the ACM resampler, and if your audio is 16 bit. But if you’re dealing with IEEE floating point audio and you’re on Windows Azure or a non-windows platform, you might not have access to built-in codecs.

So in this post, I want to explain how we can use a C# port of the Cockos WDL resampler to perform input driven resampling of our audio.

I made the port so NAudio could provide the WdlResamplingSampleProvider which provides a fully managed output-driven resampler.

But I never made the underlying WdlResampler class public. That’s because I wanted to clean up it’s interface a bit before sharing it. However, it is fully self-contained, so there is no difficulty just copying the code from this class and including it in your own project.

Let’s see how we can use it. I’ll simulate input driven resampling, by reading 1000 samples at a time from an input file, but this audio could just as easily be coming from the soundcard via WasapiCapture. And then I’ll pass each block of input samples into the WdlResampler, and ask to read out whatever’s available. I’ll use an output buffer that’s bigger than I need so I’ll always fully read out resampled audio.

There’s a few points worth noting here.

  • I’m using AudioFileReader so the input audio is floating point samples. The WdlResampler only works with floating point
  • The WdlResampler has a SetFeedMode method that allows us to specify we are in input driven mode
  • In this example I’m downsampling to 16kHz (assuming the input file is >16kHz)
  • The WdlResampler uses counts of sample “frames” rather than number of samples. In a stereo file, a sample frame is two samples – a left and a right sample. I haven’t checked if the resampler supports changing the channel count – I suspect not, but you can easily change channel count as a separate step.
int outRate = 16000;
var inFile = @"E:\Some Input File.mp3";
var outFile = @"E:\Resampled Output File.wav";

using (var reader = new AudioFileReader(inFile))
using (var writer = new WaveFileWriter(outFile, WaveFormat.CreateIeeeFloatWaveFormat(outRate, reader.WaveFormat.Channels)))
{
    var read = 0;
    var buffer = new float[1000];
    var resampler = new WdlResampler();
    resampler.SetMode(true, 2, false);
    resampler.SetFilterParms();
    resampler.SetFeedMode(true); // input driven
    resampler.SetRates(reader.WaveFormat.SampleRate, outRate);
    
    while ((read = reader.Read(buffer, 0, buffer.Length)) > 0)
    {
        int framesAvailable = read / reader.WaveFormat.Channels;
        float[] inBuffer;
        int inBufferOffset;
        int inNeeded = resampler.ResamplePrepare(framesAvailable, writer.WaveFormat.Channels, out inBuffer, out inBufferOffset);
     
        Array.Copy(buffer,0,inBuffer,inBufferOffset,inNeeded * reader.WaveFormat.Channels);
        
        int inAvailable = inNeeded;
        float[] outBuffer = new float[2000]; // plenty big enough
        int framesRequested = outBuffer.Length / writer.WaveFormat.Channels;
        int outAvailable = resampler.ResampleOut(outBuffer, 0, inAvailable, framesRequested, writer.WaveFormat.Channels);

        writer.WriteSamples(outBuffer, 0, outAvailable * writer.WaveFormat.Channels);
    }
}

So that’s how you can perform input driven resampling in fully managed C#. Admittedly it is a little more cumbersome than the equivalent output driven code:

int outRate = 16000;
var inFile = @"E:\some input file.mp3";
var outFile = @"E:\resampled wdl.wav";
using (var reader = new AudioFileReader(inFile))
{
    var resampler = new WdlResamplingSampleProvider(reader, outRate);
    WaveFileWriter.CreateWaveFile16(outFile, resampler);
}

But hopefully this helps people who are stuck with how to perform input driven resampling of IEEE floating point samples on systems without built-in codecs. I’ve provided a gist with the full code here.

And I’ll probably make the WdlResampler class in NAudio public in the future so you won’t need to take a copy.

Note – if you are using WasapiCapture or WasapiLoopbackCapture and you are wondering how to turn the incoming byte array into a float array, then check out my article here for some techniques – the WaveBuffer one is probably the best choice.

Want to get up to speed with the the fundamentals principles of digital audio and how to got about writing audio applications with NAudio? Be sure to check out my Pluralsight courses, Digital Audio Fundamentals, and Audio Programming with NAudio.

Comments

Comment by mickael riviere

Hi Mark
Thank you for this very good sample of resampling.
I'm trying to apply it to my use case, and a little help woudl be great because I am in the specific case thta you mentioned, that is resampling from wasapiloopback stereo to 16khz mono.
how do I change the channels count? before/after/while rate resampling?
here is my code sample where I got an error on the line " Array.Copy(source, 0, inBuffer, inBufferOffset, inNeeded * waveInFormat.Channels);
", saying that the destination array is too small. Actually, it is only 19800, whereas the inNeeded value is 19800 and waveInFormat.Channels is 2, so it tries to push 38400 bytes in a 19800 array...
//resampling from wasapi loopback capture to 16KHz mono
MMDevice device = WasapiLoopbackCapture.GetDefaultLoopbackCaptureDevice();
using (IWaveIn waveIn = new NAudio.Wave.WasapiLoopbackCapture(device))
{
WaveFormat waveInFormat = waveIn.WaveFormat;
WaveFormat waveOutFormat = new WaveFormat(16000, 1);
var resampler = new NAudio.Dsp.WdlResampler();
resampler.SetMode(true, 2, false);
resampler.SetFilterParms();
resampler.SetFeedMode(true); // input driven
resampler.SetRates(waveInFormat.SampleRate, waveOutFormat.SampleRate);
var temp = Path.ChangeExtension(Path.GetTempFileName(), "wav");
using (WaveFileWriter writer = new WaveFileWriter(temp, waveOutFormat))
{
waveIn.DataAvailable +=
(object sender, NAudio.Wave.WaveInEventArgs e) =>
{
byte[] source = e.Buffer;
int framesAvailable = source.Length / waveInFormat.Channels;
float[] inBuffer;
int inBufferOffset;
int inNeeded = resampler.ResamplePrepare(framesAvailable, waveOutFormat.Channels, out inBuffer, out inBufferOffset);
Array.Copy(source, 0, inBuffer, inBufferOffset, inNeeded * waveInFormat.Channels);
int inAvailable = inNeeded;
float[] outBuffer = new float[2000]; // plenty big enough
int framesRequested = outBuffer.Length / waveOutFormat.Channels;
int outAvailable = resampler.ResampleOut(outBuffer, 0, inAvailable, framesRequested, waveOutFormat.Channels);
byte[] resampled = new byte[outBuffer.Length];
Buffer.BlockCopy(outBuffer, 0, resampled, 0, outBuffer.Length);
writer.Write(resampled, 0, resampled.Length);
};
waveIn.StartRecording();
Thread.Sleep(10000);
waveIn.StopRecording();
writer.Close();
}
}
thanks for any help

mickael riviere