ardftsrc 0.0.3

High-quality audio sample-rate conversion using the ARDFTSRC algorithm.
Documentation

ARDFTSRC

Crates.io Docs.rs

A rust implementation of the Arbitrary Rate Discrete Fourier Transform Sample Rate Converter (ARDFTSRC) algorithm.

ardftsrc is a streaming audio sample-rate converter for interleaved audio streams, and is appropriate for both realtime and offline resampling.

Generally ardftsrc is preferred over other resamplers when quality is paramount. Although it is generic over both f32 and f64, it is highly recommended to use it with f64, even when processing an f32 audio stream.

It is more compute intensive than other resamplers, so consider sinc rubato if you want more efficiency. See PERFORMANCE.md for a detailed speed and quality comparison vs rubato.

Quick Start

Use InterleavedResampler::process_all to resample a complete interleaved audio stream for a single track.

use ardftsrc::{InterleavedResampler, PRESET_HIGH};

fn resample_all(input: &[f32], in_rate: usize, out_rate: usize, channels: usize) -> Vec<f32> {
    // When using a preset other than "FAST", f64 processing is preferred.
    let input_f64: Vec<f64> = input.iter().map(|v| *v as f64).collect();

    let config = PRESET_HIGH
        .with_input_rate(in_rate)
        .with_output_rate(out_rate)
        .with_channels(channels);

    let mut resampler = InterleavedResampler::<f64>::new(config).unwrap();

    let output = resampler.process_all(&input_f64).unwrap();

    // Convert back to the original interleaved f32
    output.interleave().into_iter().map(|v| v as f32).collect()
}

Chunk Resampling

Use chunk resampling when you can control both read and write buffer sizes. Query input_chunk_size() and output_chunk_size() and size your input and output slices to the sizes required. The chunk API is more efficient than the streaming API and is preferred when you are not doing live resampling.

There are two chunked resamplers depending on the shape of your audio:

  1. InterleavedResampler - for interleaved audio
  2. PlanarResampler - for planar audio.

Internally ardftsrc uses planar representation, so PlanarResampler is more efficient, but if you're already working with interleaved audio, prefer InterleavedResampler since it has an optimized de-interleave / re-interleave path. Working with all chunked resamplers is the same:

  1. Create the resampler with let resampler = Resampler::new(config)
  2. Query the required input buffer size and output buffer size with resampler.input_buffer_size() and resampler.output_buffer_size()
  3. Call process_chunk(...) for each chunk, using the appropriate buffer sizes.
  4. Call process_chunk_final(...) for the final chunk, it can be undersized.
  5. Finally, call finalize(...) once per stream to emit delayed tail samples and reset stream state.

To end the stream early, you can always just stop and call reset() on the stream.

use ardftsrc::{InterleavedResampler, PRESET_GOOD};

fn resample_chunked(input: Vec<f32>, in_rate: usize, out_rate: usize, channels: usize) -> Vec<f32> {
    // When using a preset other than "FAST", f64 processing is preferred.
    let input_f64: Vec<f64> = input.into_iter().map(|v| v as f64).collect();

    let config = PRESET_GOOD
        .with_input_rate(in_rate)
        .with_output_rate(out_rate)
        .with_channels(channels);

    let mut resampler = InterleavedResampler::<f64>::new(config).unwrap();

    // Get the input and output chunk sizes
    // You must read and write in these buffer sizes
    let input_chunk_size = resampler.input_chunk_size();
    let output_chunk_size = resampler.output_chunk_size();
    let mut out_buf = vec![0.0_f64; output_chunk_size];
    let mut out_f64 = Vec::<f64>::new();
    let mut offset = 0;

    // Process whole chunks in the size of input_chunk_size
    while offset + input_chunk_size <= input_f64.len() {
        let chunk = &input_f64[offset..offset + input_chunk_size];

        // Process the chunk
        let written = resampler.process_chunk(chunk, &mut out_buf).unwrap();

        // Process output
        out_f64.extend_from_slice(&out_buf[..written]);
        offset += input_chunk_size;
    }

    // The final chunk can be undersized (or even zero sized)
    let final_chunk = &input_f64[offset..];

    // Process Output
    let written = resampler.process_chunk_final(final_chunk, &mut out_buf).unwrap();
    out_f64.extend_from_slice(&out_buf[..written]);

    // After processing the final chunk, you must call "finalize()" to get tail content.
    // finalize() also resets the resampler instance so it can be used again.
    let written = resampler.finalize(&mut out_buf).unwrap();
    out_f64.extend_from_slice(&out_buf[..written]);

    // Convert back into f32
    out_f64.into_iter().map(|v| v as f32).collect()
}

Gapless Context

For adjacent tracks, you can set edge context before processing:

  • pre(Vec<T>): tail frames from the previous track
  • post(Vec<T>): head frames from the next track

post(...) may be called any time while the current stream is still active, but it must be set before process_chunk_final(...).

This enables live gapless handoff: while track A is streaming, once track B is known you can call post(...) on A with B's head samples so A's stop-edge uses real next-track context.

Streaming Resampler

Use the streaming resampler for live resampling. It accepts interleaved sample slices of any size and buffers internally until enough input is available for the underlying chunk resampler.

  1. Call write_samples(...) with any incoming input size and call read_samples(...) to drain available output.
  2. For multichannel streams, samples must be written interleaved.
  3. Call new_span(input_sample_rate, channels) when the input sample rate or channel count changes.
  4. Before calling new_span(...) or finalize_samples(...), the current span must be frame aligned.
  5. Call finalize_samples(...) once at end-of-stream, then keep calling read_samples(...) until it returns 0.

Expect bursty read behavior. read_samples(...) accepts any output buffer size.

Spans

Streaming sources sometimes change format while they are still producing samples. For example, a playlist-like source may play one file at 44.1 kHz stereo and then another at 48 kHz mono. The streaming resampler models those format regions as spans. You can start a new span with new_span(). When a new span starts, writes go to the new span immediately, and reads continue draining the previous span first before switching to the next.

Input spans and output spans are non-synchronous. After calling new_span, you should query samples_left_in_span() to see how many samples are left on the output side before the output will switch to a new span.

To end the stream early, you can always just stop and call reset() on the stream.

use ardftsrc::{PRESET_GOOD, StreamingResampler};

fn resample_streaming(span_1_input: Vec<f32>, span_2_input: Vec<f32>) -> Vec<f32> {
    // Span 1 is 44.1 kHz stereo. Span 2 is 48 kHz mono.
    // Both spans are resampled to the same 48 kHz output rate.
    assert!(span_1_input.len().is_multiple_of(2));

    let config = PRESET_GOOD
        .with_input_rate(44_100)
        .with_output_rate(48_000)
        .with_channels(2);

    let mut resampler = StreamingResampler::<f32>::new(config).unwrap();
    let mut output = Vec::<f32>::new();
    let mut read_sample = [0.0_f32; 1];

    // This intentionally writes one sample at a time. Larger slices are more efficient,
    // but single-sample writes are valid.
    for sample in span_1_input {
        resampler.write_samples(&[sample]).unwrap();

        let written = resampler.read_samples(&mut read_sample);
        output.extend_from_slice(&read_sample[..written]);

        if resampler.samples_left_in_span() == Some(0) {
            // New span detected, maybe switch channel count in output.
        }
    }

    // Starting a new span implicitly finalizes span 1. Span 1 must be frame aligned.
    resampler.new_span(48_000, 1).unwrap();

    for sample in span_2_input {
        resampler.write_samples(&[sample]).unwrap();

        let written = resampler.read_samples(&mut read_sample);
        output.extend_from_slice(&read_sample[..written]);

        if resampler.samples_left_in_span() == Some(0) {
            // New span detected, maybe switch channel count in output.
        }
    }

    // Finalization can produce delayed tail output, so keep reading until the stream is drained.
    resampler.finalize_samples().unwrap();
    loop {
        let written = resampler.read_samples(&mut read_sample);
        if written == 0 {
            break;
        }
        output.extend_from_slice(&read_sample[..written]);
    }

    output
}

Batching

Use batching when you have multiple full tracks to convert with the same configuration.

  • InterleavedResampler::batch(...): processes each interleaved input as an independent stream (no context shared between tracks).
  • InterleavedResampler::batch_gapless(...): preserves adjacent-track context for gapless album-style playback.
  • PlanarResampler exposes the same batch(...) and batch_gapless(...) APIs for already-planar inputs.

Enable the rayon feature to parallelize work across tracks.

use ardftsrc::{InterleavedResampler, PRESET_GOOD, PlanarVecs};

fn resample_tracks(
    inputs: &[&[f64]],
    in_rate: usize,
    out_rate: usize,
    channels: usize,
) -> Vec<PlanarVecs<f64>> {
    let config = PRESET_GOOD
        .with_input_rate(in_rate)
        .with_output_rate(out_rate)
        .with_channels(channels);

    let driver = InterleavedResampler::<f64>::new(config).unwrap();

    // Independent tracks (podcasts, unrelated files, etc.).
    let _independent = driver.batch(inputs).unwrap();

    // Gapless sequence (album tracks played back-to-back).
    let gapless = driver.batch_gapless(inputs).unwrap();

    // Return one of the two results based on your use case.
    gapless
}

Quality Tuning and Presets

ARDFTSRC is built for quality over speed, and despite supporting both f32 and f64 should almost always be run as f64. To resample f32 audio, it is recommended to convert f32 samples to f64, resample them using InterleavedResampler<f64> or PlanarResampler<f64>, then convert back to f32.

If you want better performance than what this project offers, consider using a sinc resampler such as rubato.

Presets are pre-vetted Config for various quality levels.

let config = ardftsrc::PRESET_GOOD
  .with_input_rate(44_100)
  .with_output_rate(48_000)
  .with_channels(2);
Preset Parameters Recommended use Quality Metrics
PRESET_FAST quality=512 bandwidth=0.832 Fast preset for realtime workloads. f32, f64
PRESET_GOOD quality=1878 bandwidth=0.911 Balanced preset for realtime quality. f64
PRESET_HIGH quality=73622 bandwidth=0.987 High quality for offline or quality-focused realtime use. f64
PRESET_EXTREME quality=524514 bandwidth=0.995 Maximum quality, intended for offline use. f64

Feature Flags

Flag Enables Default
audioadapter Experimental audioadapter support No
rayon Parallel processing (channel and track parallelism) No
avx FFT AVX SIMD Yes
sse FFT SSE SIMD Yes
neon FFT NEON SIMD for ARM / Mac Yes
wasm_simd FFT WebAssembly SIMD Yes

Runtime feature detection is in place for all SIMD except webassembly.