audio-resample-bsd 0.2.1

RT-safe audio resampling crate (rubato-based) — the only processing interface callable directly from the real-time audio thread
Documentation
//! Property tests — verify the mathematical invariants of resampling over
//! random inputs.
//!
//! These cover a wider input space than unit tests. Since SNR quality is
//! measured quantitatively in `tests/resample_snr.rs`, the focus here is on
//! **structural invariants** (finite output, bounded delay, stability after
//! `set_rate`) rather than accuracy.
//!
//! Note: rubato `Fft` rejects some (rate, chunk) combinations (notably a small
//! chunk relative to a large rate ratio) at construction time. This is not a
//! bug but an FFT-block constraint, so these tests **assume construction
//! succeeds** (via `prop_assume!`/early return) and verify the processing-path
//! invariants.

use std::f64::consts::TAU;

use audio_resample_bsd::{Resampler, RubatoResampler};
use proptest::prelude::*;

/// Fast property-test config — limits the case count to keep the suite quick
/// (target < 1s per case). A runtime value (`const` is impossible because
/// `ProptestConfig` has a `Drop` field).
fn fast_config() -> ProptestConfig {
    ProptestConfig {
        cases: 32,
        timeout: 10_000,
        ..ProptestConfig::default()
    }
}

/// Generates a `freq` Hz sine of `n` samples at `rate`.
fn sine(freq: f64, rate: f64, n: usize) -> Vec<f32> {
    let omega = TAU * freq / rate;
    (0..n).map(|i| (omega * i as f64).sin() as f32).collect()
}

proptest! {
    #![proptest_config(fast_config())]

    /// Processing a sine in a valid configuration completes without panicking
    /// and produces all-finite output. Configurations that rubato rejects are
    /// skipped via early return (FFT-block constraint, not a bug).
    #[test]
    fn valid_config_processes_sine_to_finite_output(
        rate_in in 8000u32..=192_000,
        rate_out in 8000u32..=192_000,
        channels in 1u16..=4u16,
        chunk in 256usize..=4096,
        freq in 50.0f64..4000.0,
    ) {
        prop_assume!(freq < (rate_in as f64) / 2.0 && freq < (rate_out as f64) / 2.0);
        let rs = RubatoResampler::new(
            f64::from(rate_in),
            f64::from(rate_out),
            channels.into(),
            chunk,
        );
        // Construction rejection (FFT constraint) is skipped — not a processing
        // invariant.
        let mut rs = match rs {
            Ok(r) => r,
            Err(_) => return Ok(()),
        };
        let input = sine(freq, f64::from(rate_in), chunk * channels as usize);
        let mut output = vec![0.0_f32; rs.output_frames_max() * channels as usize];
        let written = rs.process_into_buffer(&input, &mut output)?;
        // All written output must be finite (no NaN/inf) — a prerequisite for
        // RT stability.
        for &s in &output[..written * channels as usize] {
            prop_assert!(s.is_finite(), "non-finite output sample {s}");
        }
    }

    /// The delay (output_delay) is always finite and within a range close to
    /// output_frames_max.
    #[test]
    fn delay_is_finite_and_bounded(
        rate_in in 8000u32..=192_000u32,
        rate_out in 8000u32..=192_000u32,
        chunk in 256usize..=8192,
    ) {
        let rs = match RubatoResampler::new(f64::from(rate_in), f64::from(rate_out), 1, chunk) {
            Ok(r) => r,
            Err(_) => return Ok(()),
        };
        let delay = rs.output_delay();
        let out_max = rs.output_frames_max();
        prop_assert!(delay < out_max + chunk, "delay {delay} unexpectedly large vs out_max {out_max}");
    }

    /// Processing remains valid after `set_rate` — a ratio change does not cause
    /// a crash or non-finite output.
    #[test]
    fn set_rate_then_process_is_stable(
        rate_a in 22050u32..=48_000u32,
        rate_b in 48_000u32..=96_000u32,
    ) {
        let mut rs = match RubatoResampler::new(f64::from(rate_a), 48_000.0, 1, 1024) {
            Ok(r) => r,
            Err(_) => return Ok(()),
        };
        rs.set_rate(f64::from(rate_b), 48_000.0);
        let input = sine(1000.0, f64::from(rate_b), 1024);
        let mut output = vec![0.0_f32; rs.output_frames_max()];
        let written = rs.process_into_buffer(&input, &mut output)?;
        for &s in &output[..written] {
            prop_assert!(s.is_finite(), "non-finite after set_rate: {s}");
        }
    }
}