simple_src 0.4.0

A simple sample rate conversion lib for audio.
Documentation

simple_src

A simple sample rate conversion lib for audio.

Usage

Usually use sinc Converter, it is flexible and high-quality. The linear Converter is not recommended unless performance is really important and quality is not cared.

Sinc converters have FIR latency. For a complete buffer, call Manager::convert, which pads zeros and drops the leading delay. For streaming, skip manager.latency() samples at the start and call Convert::flush after the last input until it returns 0. Built-in sinc/linear converters stop once the delay line is empty (they may still need more than one call if the flush buffer is short). The trait default of Convert::flush only fills the provided buffer and does not stop on an empty delay.

Float ratios such as 48000.0 / 44100.0 may be reduced to a rational when a continued-fraction fit has numerator and denominator ≤ 16384 and relative error ≤ 1e-12 (so 0.7 becomes 7/10, while π stays float phase). Prefer with_sample_rate / fast_with_sample_rate for exact rate pairs.

Multi-channel audio is N independent mono converters. Keep planar buffers (one slice per channel) and use a converter per channel, or process_planar to keep consume/produce counts aligned. Pass the same converter count, buffer lengths, and process history on every channel; mismatches return Error instead of panicking.

sinc

Generic interpolation uses a half Kaiser-sinc table and quantify. Fast polyphase interpolation precomputes one tap set per rational phase; it requires a rational ratio whose reduced numerator is ≤ 1024, does not take quantify, and returns Error::FastUnavailable otherwise.

Typical 44100/48000 conversion:

use simple_src::{sinc, Quality};

let samples = vec![1.0, 2.0, 3.0, 4.0];
let manager = sinc::Manager::fast_with_sample_rate_quality(
    44100,
    48000,
    Quality::Bit16Fast,
    20000,
).unwrap();
for s in manager.convert(&samples) {
    println!("{s}");
}

Generic path with a quality preset (quantify is used):

use simple_src::{sinc, Quality};

let samples = vec![1.0, 2.0, 3.0, 4.0];
let manager = sinc::Manager::with_quality(2.0, Quality::Bit8Fast, 0.1).unwrap();
for s in manager.convert(&samples) {
    println!("{s}");
}

Or use builder:

use simple_src::{sinc, Convert};

let samples = vec![1.0, 2.0, 3.0, 4.0];
let manager = sinc::Manager::builder()
    .ratio(2.0)
    .attenuation(48.0)
    .quantify(8)
    .pass_width(0.9)
    .build()
    .unwrap();
let mut converter = manager.converter();
for s in converter.process(samples.into_iter()) {
    println!("{s}");
}

For multi-channel example see two_channels.rs.

linear

use simple_src::{linear, Convert};

let samples = vec![1.0, 2.0, 3.0, 4.0];
let manager = linear::Manager::new(2.0).unwrap();
let mut converter = manager.converter();
for s in converter.process(samples.into_iter()) {
    println!("{s}");
}

Sinc parameters

Recommended initialization parameters for sinc converter, also available as Quality presets:

attenuation quantify Quality
8bit fast 48 8 Bit8Fast
8bit medium 60 16 Bit8Medium
8bit better 72 32 Bit8Better
16bit lower 84 64 Bit16Lower
16bit fast 96 128 Bit16Fast
16bit medium 108 256 Bit16Medium
16bit better 120 512 Bit16Better
24bit lower 132 1024 Bit24Lower
24bit fast 144 2048 Bit24Fast
24bit medium 156 4096 Bit24Medium
24bit better 168 8192 Bit24Better

The relationship between attenuation and quantify is about Q = 2 ^ (A / 12 - 1), A = 12 + 12 * log2(Q).

Due to the amount of calculation and the size of LUT, A = 144 or 156 for 24bit audio is usually fine, and for 16bit, A = 120 is enough.

Quality::attenuation applies to Generic and Fast; Quality::quantify is Generic-only.

Filter design notes

  • Order: From attenuation and transition width, length uses A + 6 dB and is rounded up to an even order (capped at 2048). Explicit order / with_raw paths skip that margin.
  • Cutoff: Ideal lowpass cutoff is at min(1, ratio) * (1 - trans_width), so the transition sits entirely below the applicable Nyquist (output Nyquist when downsampling, input Nyquist when upsampling). The −6 dB point is therefore near the pass edge from pass_freq / pass_width; frequencies between that edge and Nyquist are already in the transition or stop band. Stop-band attenuation A applies beyond the stop edge (about halfway from the pass edge to Nyquist), not inside the transition.
  • Coefficients: Fast phases and the Generic half-table are scaled for unity DC gain.

CLI

cargo run -p simple-src-cli -- input.wav -r 48000 -o output.wav

The CLI uses Fast polyphase interpolation by default. Pass --generic (and --quantify if needed) for half-table interpolation.

Plots

Use plots.py to show the results of conversion. It needs numpy, scipy and matplotlib.

Here is an example showing the results of a downsampling 96kHz:

$ cargo test -p simple_src -r --test testwav -- --ignored --exact --show-output generate
$ cargo test -p simple_src -r --test sinc -- --ignored --exact --show-output ta120_2_96k_down
$ python
>>> import plots
>>> import os
>>> os.chdir('output')
>>> plots.spectrum('beep_96k_44k_s_a120_2.wav')
>>> plots.spectrogram('sweep_96k_44k_s_a120_2.wav')
>>> plots.impulse('impulse_96k_44k_s_a120_2.wav')
>>> plots.impulse('impulse_96k_44k_s_a120_2.wav', True)

See code in tests for more details.

References

  1. Smith, J.O. Digital Audio Resampling Home Page https://ccrma.stanford.edu/~jos/resample/.
  2. Alan V. Oppenheim, Ronald W. Schafer. Discrete-Time Signal Processing, Third Edition.