Recursive AI

Resample audio in C#

All oneaudio recipes

Resample audio in C# with oneaudio, from 44.1 kHz to 48 kHz and back or any ratio up to 64, in one call or as a stream, with three quality levels and no allocation while running.

C#
using OneAudio;
 
// A whole clip, 44.1 kHz to 48 kHz with the default Polyphase filter.
AudioClip clip = Audio.Load("music.wav");
AudioClip at48 = Audio.Resample(clip, 48000);
Console.WriteLine($"{clip.Frames} frames at 44.1 kHz -> {at48.Frames} at 48 kHz");
 
// And back. The length is always round(frames x ratio), and the filter
// delay is removed.
AudioClip back = Audio.Resample(at48, 44100);
Console.WriteLine($"and back: {back.Frames} frames");
 
// Interleaved floats you already hold, at a chosen quality.
float[] voice = Audio.Load("voice.wav").Samples.ToArray();     // 16 kHz mono
ResamplerQuality[] qualities =
    [ResamplerQuality.Polyphase, ResamplerQuality.PolyphaseFast, ResamplerQuality.Linear];
foreach (ResamplerQuality q in qualities)
{
    float[] up = Audio.Resample(voice, channels: 1, fromRate: 16000, toRate: 48000, q);
    Console.WriteLine($"{q,-13} {voice.Length} -> {up.Length} samples");
}
 
// Streaming, for live audio: feed blocks of any size. The output doesn't
// depend on how the input was split.
IResampler resampler = AudioResampler.Create(inputRate: 44100, outputRate: 48000,
                                             channels: 2, ResamplerQuality.Polyphase);
float[] input = clip.Samples.ToArray();
float[] output = new float[resampler.MaxOutputFrames(512) * 2];   // fits 512 in
long produced = 0;
for (int pos = 0; pos < clip.Frames;)
{
    int frames = (int)Math.Min(512, clip.Frames - pos);
    resampler.Process(input.AsSpan(pos * 2, frames * 2), output,
                      out int used, out int written);
    pos += used;                    // input that didn't fit is offered again
    produced += written;
}
int tail;
while ((tail = resampler.Flush(output)) > 0)    // what the filter still holds
    produced += tail;
Console.WriteLine($"streamed: {produced} frames, latency {resampler.LatencyFrames:F1} frames");
Text
1411200 frames at 44.1 kHz -> 1536000 at 48 kHz
and back: 1411200 frames
Polyphase     96400 -> 289200 samples
PolyphaseFast 96400 -> 289200 samples
Linear        96400 -> 289200 samples
streamed: 1536001 frames, latency 78.4 frames

Choosing a quality

QualityFilterUse it for
Polyphase (default)144-tap Kaiser-windowed sinc, flat within 0.05 dB to 0.9 of the lower Nyquist frequency, more than 100 dB of alias rejectionFiles and anything you keep
PolyphaseFast44 taps, flat to 0.8 of the lower Nyquist frequency, about 75 dB of alias rejectionReal-time voices in a mixer
LinearTwo tapsPitch effects where speed matters more than quality

When the rate goes down, the filter’s cutoff follows the new Nyquist frequency.

One call or a stream

  • Audio.Resample returns exactly round(frames × ratio) frames, with the filter delay removed so that output frame m is the input at time m / ratio. It accepts ratios from 1/64 to 64.
  • AudioResampler.Create returns an IResampler for audio that arrives in blocks. Everything is allocated when it’s created, and Process allocates nothing. The output doesn’t depend on how the input is split. Input that doesn’t fit in the output buffer is left unused and must be offered again, which is what pos += used does.
  • Flush returns the frames still held in the filter. Call it until it returns 0. The streamed total can be a frame longer than Audio.Resample’s, which trims to round(frames × ratio), as in this run.
  • LatencyFrames is the filter’s look-ahead, in output frames.
  • SetRatio changes the ratio while running, without a glitch. That’s how the mixer bends pitch. Keep each change to a few percent.

The mixer resamples each voice whose source rate differs from its own, so audio for AudioMixer needs no resampling first. Every resampler detail is in the API overview.

A longer guide: Resample audio in C#.