Recursive AI

Resample audio in C#

Resample audio in C# with pure .NET code on any OS: 44.1 kHz to 48 kHz or 16 kHz, files or float buffers, streaming, with no Media Foundation or native code.

To resample audio in C#, add the RecursiveAI.OneAudio package and call Audio.Transcode("in.wav", "out.wav", new TranscodeOptions { SampleRate = 48000 }) for a file, or Audio.Resample(clip, 48000) for samples in memory. The default converter is a 144-tap windowed-sinc polyphase filter with at least 100 dB of alias rejection, its delay is compensated so the output lines up with the input, and it is written in C#, so it runs the same on Windows, Linux and macOS without Media Foundation. This page covers files, float buffers, streaming, choosing a quality, and how this compares with NAudio’s resamplers. Every sample was compiled and run against OneAudio 1.2.1 on .NET 10.

Before you start

TerminalNuGet
dotnet new console -o Resample
cd Resample
dotnet add package RecursiveAI.OneAudio --version 1.2.1

OneAudio targets .NET 10 and is licensed MIT OR Apache-2.0. The samples use music.wav, a 30-second stereo 44.1 kHz 16-bit file, and voice16k.wav, 30 seconds of 16 kHz mono. Use your own files under those names.

Resample a file or a buffer

Program.csOneAudio 1.2.1 · .NET 10
using OneAudio;
 
// 1. A file: 44.1 kHz in, 48 kHz out, converted while it streams.
Audio.Transcode("music.wav", "music-48k.wav", new TranscodeOptions { SampleRate = 48000 });
 
// 2. In memory: a decoded clip to 48 kHz and back to 44.1 kHz.
AudioClip clip = Audio.Load("music.wav");
AudioClip at48 = Audio.Resample(clip, 48000);
AudioClip back = Audio.Resample(at48, 44100);
Console.WriteLine($"{clip.Frames} frames @ {clip.Format.SampleRate} Hz -> " +
                  $"{at48.Frames} @ {at48.Format.SampleRate} Hz -> {back.Frames} @ {back.Format.SampleRate} Hz");
 
// 3. Raw interleaved floats, e.g. 16 kHz mono speech up to 48 kHz.
float[] speech = Audio.Load("voice16k.wav").Samples.ToArray();
float[] speech48 = Audio.Resample(speech, channels: 1, fromRate: 16000, toRate: 48000,
                                  ResamplerQuality.Polyphase);
Console.WriteLine($"{speech.Length} samples @ 16 kHz -> {speech48.Length} @ 48 kHz");
OutputConsole
1323000 frames @ 44100 Hz -> 1440000 @ 48000 Hz -> 1323000 @ 44100 Hz
480000 samples @ 16 kHz -> 1440000 @ 48 kHz
  • Audio.Transcode decodes, resamples and writes block by block, so a long file doesn’t have to fit in memory. The input can be any format OneAudio reads (WAV, MP3, Ogg Vorbis, Opus, FLAC). TranscodeOptions.Channels changes the channel count in the same pass.
  • Audio.Resample returns exactly round(frames × ratio) frames: 1,323,000 frames at 44.1 kHz become 1,440,000 at 48 kHz, and the round trip returns 1,323,000. The filter’s delay is removed, so output frame m is the input at time m / ratio, and nothing shifts in time.
  • Ratios from 1/64 to 64 are accepted, so 8 kHz telephone audio to 96 kHz works in one call.
  • To decode and resample in one step, pass new LoadOptions { SampleRate = 48000 } to Audio.Load.

Stream a long file through the resampler

When audio arrives in blocks (a live input, a network stream, a recording too long to load), create an IResampler and feed it. Here a file goes from the decoder through the resampler into a WAV encoder, a block at a time:

Program.csOneAudio 1.2.1 · .NET 10
using OneAudio;
 
// Stream a file through the resampler into a WAV encoder, block by block.
// Memory stays flat, so this works the same for a 3-minute song or a 3-hour recording.
using IAudioDecoder decoder = Audio.Open("music.wav");
int channels = decoder.Format.Channels;
const int OutRate = 48000;
 
IResampler resampler = AudioResampler.Create(decoder.Format.SampleRate, OutRate, channels,
                                             ResamplerQuality.Polyphase);
 
using FileStream file = File.Create("music-48k-streamed.wav");
using var encoder = new WavEncoder(file, decoder.Format with { SampleRate = OutRate });
 
float[] input = new float[4096 * channels];
float[] output = new float[resampler.MaxOutputFrames(4096) * channels];
long written = 0;
int frames;
while ((frames = decoder.Read(input)) > 0)
{
    int offset = 0;
    while (offset < frames)   // the resampler may take less input than offered
    {
        resampler.Process(input.AsSpan(offset * channels, (frames - offset) * channels), output,
                          out int used, out int produced);
        encoder.Write(output.AsSpan(0, produced * channels));
        offset += used;
        written += produced;
    }
}
int tail;
while ((tail = resampler.Flush(output)) > 0)   // the samples still inside the filter
{
    encoder.Write(output.AsSpan(0, tail * channels));
    written += tail;
}
encoder.Finish();
Console.WriteLine($"{decoder.LengthFrames} frames @ {decoder.Format.SampleRate} Hz -> {written} frames @ {OutRate} Hz");
OutputConsole
1323000 frames @ 44100 Hz -> 1440001 frames @ 48000 Hz
  • Everything is allocated in AudioResampler.Create. Process and Flush allocate nothing, so the same loop can run on an audio thread.
  • The output doesn’t depend on how the input is split into blocks, so a live stream and a file give the same samples.
  • Process reports how much input it used and how much output it produced. Input that didn’t fit is offered again, which is what the inner loop does.
  • Flush returns what is still inside the filter at the end. Call it until it returns 0. The streamed total can be one frame longer than Audio.Resample’s, which trims to round(frames × ratio), as here.
  • decoder.Format with { SampleRate = OutRate } keeps the source’s 16-bit sample type, so the encoder writes a 16-bit WAV at 48 kHz.

Choosing a resampler quality

QualityFilterUse it for
Polyphase (default)144-tap Kaiser-windowed sinc, flat within 0.05 dB to 0.9 of the lower Nyquist frequency, 100 dB or more of alias rejection, THD+N below −100 dBFiles, mastering, anything you keep
PolyphaseFast44-tap Kaiser-windowed sinc, flat to 0.8 of the lower Nyquist frequency, about 75 dB of alias rejectionMany real-time voices, game audio
LinearTwo-tap linear interpolationPitch effects where speed matters more than fidelity

The figures are from the API overview. For 44.1 kHz to 48 kHz, 0.9 of the lower Nyquist frequency is 19.8 kHz, so the default stays flat across the audible band. When you resample down, for example 48 kHz to 16 kHz for speech recognition, the filter’s cutoff follows the new Nyquist frequency. That is the anti-aliasing low-pass filter a down-conversion needs, built in, so you don’t add one yourself.

Resampling audio with NAudio

NAudio’s resampling guide lists four options: MediaFoundationResampler, which uses the Windows Media Foundation resampler; MacAudioConverter in the prerelease NAudio.MacOS package, for macOS; WdlResamplingSampleProvider, a managed port of the Cockos WDL resampler that works everywhere; and the legacy ACM WaveFormatConversionStream on Windows. The cross-platform one, from the guide’s sample with a WAV input (on Linux and macOS, AudioFileReader reads WAV and AIFF but not MP3), runs on Linux with NAudio 3.3.0:

Program.csNAudio 3.3.0 · Linux
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
 
int outRate = 48000;
var inFile = "music.wav";
var outFile = "music-48k-wdl.wav";
using (var reader = new AudioFileReader(inFile))
{
    var resampler = new WdlResamplingSampleProvider(reader, outRate);
    WaveFileWriter.CreateWaveFile16(outFile, resampler);
}
 
using var check = new WaveFileReader(outFile);
Console.WriteLine($"{check.WaveFormat}, {check.SampleCount} frames");
OutputConsole
16 bit PCM: 48000Hz 2 channels, 1440000 frames

Check which NAudio version a quality figure refers to. The guide on NAudio’s main branch describes a 64-tap sinc mode as the default and calls linear interpolation with two IIR low-pass filters “the pre-3.4 algorithm”. NAudio 3.4.0 is a preview on NuGet (3.4.0-preview.1), and in 3.3.0, the current release, WdlResamplingSampleProvider has no quality argument. In the guide’s measurements, that linear algorithm takes an 18 kHz tone at 44.1 to 48 kHz down by 20.9 dB, against 0.00 dB for the sinc mode.

NAudio is a good fit when your audio already runs through its ISampleProvider pipeline and its players. If you only need to convert rates in files or buffers, OneAudio does it in one call on every OS, with the same filter everywhere. For moving existing NAudio code, see migrating from NAudio.

Common conversions

  • 44.1 kHz to 48 kHz for video, games and most audio devices: new TranscodeOptions { SampleRate = 48000 }.
  • 48 kHz to 16 kHz mono for speech-to-text: new TranscodeOptions { SampleRate = 16000, Channels = 1 }. The channel change uses the mixer’s channel-mapping rules.
  • Opus works at 48 kHz only. Audio.Transcode("in.wav", "out.opus") resamples to 48 kHz for you and records the original rate in the file’s header.
  • Playback speed or pitch: in the mixer, a voice’s Pitch runs it through a resampler whose ratio can change while it plays.
  • A device at another rate: when you play through the RecursiveAI.OneAudio.Devices package and the output device runs at, say, 44.1 kHz while your mixer runs at 48 kHz, it converts with the same polyphase resampler (AllowRateConversion in DeviceStreamOptions, on by default). See play audio in C#.

Where next