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
//! Integration tests — `audio-core-bsd` `AudioFrame` (planar) <-> `RubatoResampler`
//! compatibility.
//!
//! Verifies that the planar layout of [`audio_core_bsd::AudioFrame::samples`]
//! (`[ch0_frames..., ch1_frames...]`) matches the `process_into_buffer` input
//! contract of [`RubatoResampler`] exactly, so an `AudioFrame` can be passed
//! directly with no extra copy or rearrangement.

use audio_core_bsd::AudioFrame;
use audio_resample_bsd::{Resampler, RubatoResampler};

/// Builds a planar `AudioFrame` filled with a per-channel sine at the given
/// frequencies.
fn sine_frame(channels: u16, num_frames: usize, sample_rate: u32, freqs: &[f32]) -> AudioFrame {
    let mut frame = AudioFrame::silence(channels, num_frames, sample_rate);
    let nf = frame.num_frames();
    for ch_idx in 0..channels as usize {
        let freq = freqs.get(ch_idx).copied().unwrap_or(1000.0);
        let slice = frame.channel_slice_mut(ch_idx);
        for (i, s) in slice.iter_mut().enumerate().take(nf) {
            *s = (2.0_f32 * std::f32::consts::PI * freq * i as f32 / sample_rate as f32).sin();
        }
    }
    frame
}

#[test]
fn audio_frame_samples_pass_directly_to_resampler() {
    let in_rate = 44_100_u32;
    let out_rate = 48_000_u32;
    let chunk = 1024_usize;
    let ch = 2_u16;
    let mut rs = RubatoResampler::new(f64::from(in_rate), f64::from(out_rate), ch.into(), chunk)
        .expect("construct");

    // Build a planar sine via AudioFrame — channel 0 = 1 kHz, channel 1 = 2 kHz.
    let frame = sine_frame(ch, chunk, in_rate, &[1000.0, 2000.0]);
    assert_eq!(frame.samples.len(), chunk * ch as usize);

    // ★ Key point: frame.samples (&[f32]) is passed directly — planar layout is
    // compatible, no copy.
    let mut out_samples = vec![0.0_f32; rs.output_frames_max() * ch as usize];

    // Feed several chunks to flush the group delay (reusing the same frame as a
    // smoke signal).
    let mut last_written = 0;
    for _ in 0..16 {
        last_written = rs
            .process_into_buffer(&frame.samples, &mut out_samples)
            .expect("process");
    }
    assert!(last_written > 0, "output frames must be produced");

    // Wrap the output back in an AudioFrame to confirm planar access works.
    let out_len = ch as usize * last_written;
    let out_frame = AudioFrame::from_planar(ch, out_rate, out_samples[..out_len].to_vec());
    assert_eq!(out_frame.channels, ch);
    assert_eq!(out_frame.num_frames(), last_written);
    // Planar disjointness across both channels: slices 0/1 have equal length
    // and are memory-disjoint.
    assert_eq!(out_frame.channel_slice(0).len(), last_written);
    assert_eq!(out_frame.channel_slice(1).len(), last_written);
}

#[test]
fn audio_frame_planar_disjointness_survives_resample() {
    // Planar disjointness invariant: modifying one channel must not affect the
    // other. (Indirectly verifies that the SequentialSlice zero-copy view
    // respects planar boundaries.)
    let in_rate = 48_000_u32;
    let ch = 2_u16;
    let chunk = 512;
    let mut rs =
        RubatoResampler::new(f64::from(in_rate), 96_000.0, ch.into(), chunk).expect("construct");

    let mut frame = sine_frame(ch, chunk, in_rate, &[500.0, 500.0]);
    // Setting channel 0 to DC (0.0) must leave channel 1 as a sine (input stage).
    for s in frame.channel_slice_mut(0) {
        *s = 0.0;
    }
    assert!(frame.channel_slice(1).iter().any(|&s| s.abs() > 1e-6));

    let mut out = vec![0.0_f32; rs.output_frames_max() * ch as usize];
    let mut written = 0;
    for _ in 0..16 {
        written = rs
            .process_into_buffer(&frame.samples, &mut out)
            .expect("process");
    }
    assert!(written > 0);
    let out_frame = AudioFrame::from_planar(ch, 96_000, out[..ch as usize * written].to_vec());
    // Planar structure preserved: consistent channel-slice lengths.
    assert_eq!(
        out_frame.channel_slice(0).len(),
        out_frame.channel_slice(1).len()
    );
}

#[test]
fn round_trip_audio_frame_preserves_structure() {
    // After a 44.1k -> 48k -> 44.1k round trip, the original planar structure
    // (channel disjointness) is preserved.
    //
    // When chaining two fixed-input resamplers, `up`'s output size varies per
    // call, so `up`'s output must be buffered and then fed to `down` in fixed
    // chunk_size slices (honouring the fixed-input contract).
    let rate = 44_100_u32;
    let chunk = 1024;
    let ch = 1_u16;
    let mut up = RubatoResampler::new(f64::from(rate), 48_000.0, ch.into(), chunk).unwrap();
    let down_chunk = 1024;
    let mut down = RubatoResampler::new(48_000.0, f64::from(rate), ch.into(), down_chunk).unwrap();

    let frame = sine_frame(ch, chunk, rate, &[1000.0]);

    // Stage 1: process many chunks through `up` -> accumulate a 48 kHz buffer.
    let mut mid: Vec<f32> = Vec::new();
    let mut mid_tmp = vec![0.0_f32; up.output_frames_max() * ch as usize];
    for _ in 0..32 {
        let w = up
            .process_into_buffer(&frame.samples, &mut mid_tmp)
            .unwrap();
        mid.extend_from_slice(&mid_tmp[..w * ch as usize]);
    }

    // Stage 2: consume the intermediate buffer in `down`'s fixed chunks -> 44.1
    // kHz round-trip output.
    let mut out: Vec<f32> = Vec::new();
    let mut down_tmp = vec![0.0_f32; down.output_frames_max() * ch as usize];
    let stride = down_chunk * ch as usize;
    let mut pos = 0;
    while pos + stride <= mid.len() {
        let w = down
            .process_into_buffer(&mid[pos..pos + stride], &mut down_tmp)
            .unwrap();
        out.extend_from_slice(&down_tmp[..w * ch as usize]);
        pos += stride;
    }
    assert!(!out.is_empty(), "round-trip output must not be empty");

    // The round-trip output can be reconstructed as an AudioFrame — planar
    // structure preserved.
    let roundtrip = AudioFrame::from_planar(ch, rate, out);
    assert_eq!(roundtrip.channels, ch);
    assert!(roundtrip.num_frames() > 0);
}