use std::collections::VecDeque;
#[cfg(test)]
pub(crate) fn resampled_len(input_len: usize, from_rate: u32, to_rate: u32) -> usize {
if input_len == 0 {
return 0;
}
(input_len as u64 * to_rate as u64).div_ceil(from_rate as u64) as usize
}
pub(crate) fn catmull_rom(s0: f32, s1: f32, s2: f32, s3: f32, t: f32) -> f32 {
let c1 = 0.5 * (s2 - s0);
let c2 = s0 - 2.5 * s1 + 2.0 * s2 - 0.5 * s3;
let c3 = 0.5 * (s3 - s0) + 1.5 * (s1 - s2);
((c3 * t + c2) * t + c1) * t + s1
}
pub(crate) trait CatmullInterp: Copy {
fn catmull(s0: Self, s1: Self, s2: Self, s3: Self, t: f32) -> Self;
}
impl CatmullInterp for f32 {
fn catmull(s0: Self, s1: Self, s2: Self, s3: Self, t: f32) -> Self {
catmull_rom(s0, s1, s2, s3, t)
}
}
impl CatmullInterp for (f32, f32) {
fn catmull(s0: Self, s1: Self, s2: Self, s3: Self, t: f32) -> Self {
(
catmull_rom(s0.0, s1.0, s2.0, s3.0, t),
catmull_rom(s0.1, s1.1, s2.1, s3.1, t),
)
}
}
pub(crate) struct StreamingResampler<T> {
from_rate: u32,
to_rate: u32,
buf: VecDeque<T>,
buf_start: u64,
n_in: u64,
n_out: u64,
}
impl<T: CatmullInterp> StreamingResampler<T> {
pub(crate) fn new(from_rate: u32, to_rate: u32) -> Self {
assert!(
from_rate > 0 && to_rate > 0,
"resampler rates must be nonzero"
);
Self {
from_rate,
to_rate,
buf: VecDeque::new(),
buf_start: 0,
n_in: 0,
n_out: 0,
}
}
pub(crate) fn reset(&mut self) {
self.buf.clear();
self.buf_start = 0;
self.n_in = 0;
self.n_out = 0;
}
pub(crate) fn set_rates(&mut self, from_rate: u32, to_rate: u32) {
assert!(
from_rate > 0 && to_rate > 0,
"resampler rates must be nonzero"
);
if from_rate != self.from_rate || to_rate != self.to_rate {
self.from_rate = from_rate;
self.to_rate = to_rate;
self.reset();
}
}
fn total_emittable(&self, n_in: u64) -> u64 {
if n_in == 0 {
0
} else {
(n_in - 1) * self.to_rate as u64 / self.from_rate as u64 + 1
}
}
pub(crate) fn pending_output_count(&self, input_len: usize) -> usize {
(self.total_emittable(self.n_in + input_len as u64) - self.n_out) as usize
}
fn at(&self, abs: i64, last: u64) -> T {
let clamped = abs.clamp(0, last as i64) as u64;
self.buf[(clamped - self.buf_start) as usize]
}
fn prune(&mut self) {
let next_idx = self.n_out * self.from_rate as u64 / self.to_rate as u64;
let target_start = next_idx.saturating_sub(1);
while self.buf_start < target_start && self.buf.len() > 1 {
self.buf.pop_front();
self.buf_start += 1;
}
}
pub(crate) fn process(&mut self, input: &[T], mut emit: impl FnMut(T)) {
self.buf.extend(input.iter().copied());
self.n_in += input.len() as u64;
if self.n_in == 0 {
return;
}
let from = self.from_rate as u64;
let to = self.to_rate as u64;
let last = self.n_in - 1;
let target = self.total_emittable(self.n_in);
while self.n_out < target {
let p = self.n_out * from;
let idx = (p / to) as i64;
let t = (p % to) as f32 / to as f32;
let s0 = self.at(idx - 1, last);
let s1 = self.at(idx, last);
let s2 = self.at(idx + 1, last);
let s3 = self.at(idx + 2, last);
emit(T::catmull(s0, s1, s2, s3, t));
self.n_out += 1;
}
self.prune();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resampled_len_upsample() {
assert_eq!(resampled_len(3, 30_000, 48_000), 5);
}
#[test]
fn resampled_len_downsample() {
assert_eq!(resampled_len(5, 60_000, 48_000), 4);
}
#[test]
fn resampled_len_passthrough() {
assert_eq!(resampled_len(10, 48_000, 48_000), 10);
assert_eq!(resampled_len(1, 48_000, 48_000), 1);
assert_eq!(resampled_len(0, 48_000, 48_000), 0);
}
#[test]
fn catmull_rom_passes_through_knots() {
let s0 = 0.0;
let s1 = 1.0;
let s2 = 2.0;
let s3 = 3.0;
assert!((catmull_rom(s0, s1, s2, s3, 0.0) - s1).abs() < 1e-6);
assert!((catmull_rom(s0, s1, s2, s3, 1.0) - s2).abs() < 1e-6);
}
#[test]
fn catmull_rom_midpoint_on_linear_data() {
assert!((catmull_rom(0.0, 1.0, 2.0, 3.0, 0.5) - 1.5).abs() < 1e-6);
}
#[test]
fn catmull_rom_with_clamped_boundaries() {
let s1 = -1.0;
let s2 = 1.0;
let result = catmull_rom(s1, s1, s2, s2, 0.5);
assert!(result > s1 && result < s2);
}
fn resample_all(from: u32, to: u32, input: &[f32]) -> Vec<f32> {
let mut r = StreamingResampler::new(from, to);
let mut out = Vec::new();
r.process(input, |s| out.push(s));
out
}
fn resample_chunked(from: u32, to: u32, input: &[f32], chunk: usize) -> Vec<f32> {
let mut r = StreamingResampler::new(from, to);
let mut out = Vec::new();
for c in input.chunks(chunk) {
let expected = r.pending_output_count(c.len());
let before = out.len();
r.process(c, |s| out.push(s));
assert_eq!(
out.len() - before,
expected,
"pending_output_count matches process"
);
}
out
}
#[test]
fn streaming_passthrough_is_exact() {
let input: Vec<f32> = (0..64).map(|i| i as f32).collect();
let out = resample_all(48_000, 48_000, &input);
assert_eq!(out.len(), input.len());
for (a, b) in out.iter().zip(input.iter()) {
assert!((a - b).abs() < 1e-6);
}
}
#[test]
fn chunked_matches_single_call_on_smooth_signal() {
let input: Vec<f32> = (0..500).map(|i| (i as f32 * 0.05).sin() * 0.9).collect();
let one = resample_all(30_000, 96_000, &input);
for chunk in [1usize, 3, 7, 32, 128] {
let many = resample_chunked(30_000, 96_000, &input, chunk);
assert_eq!(one.len(), many.len(), "chunk={chunk} output length");
let max_diff = one
.iter()
.zip(many.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff < 0.01,
"chunk={chunk} max_diff={max_diff} exceeds tolerance"
);
}
}
#[test]
fn total_output_count_matches_rate_ratio() {
let len = 100_000usize;
let input: Vec<f32> = (0..len).map(|i| (i as f32 * 0.001).sin()).collect();
for (from, to) in [(30_000u32, 96_000u32), (96_000, 48_000), (30_000, 30_000)] {
let many = resample_chunked(from, to, &input, 137);
let ideal = (len as u64 - 1) * to as u64 / from as u64 + 1;
let diff = (many.len() as i64 - ideal as i64).abs();
assert!(
diff <= 1,
"from={from} to={to}: got {} expected ~{ideal} (diff {diff})",
many.len()
);
}
}
#[test]
fn reset_rephases_stream() {
let input: Vec<f32> = (0..32).map(|i| i as f32).collect();
let mut r = StreamingResampler::new(30_000, 96_000);
let mut first = Vec::new();
r.process(&input, |s| first.push(s));
r.reset();
let mut second = Vec::new();
r.process(&input, |s| second.push(s));
assert_eq!(first, second);
}
#[test]
fn set_rates_resets_only_on_change() {
let mut r = StreamingResampler::new(30_000, 96_000);
r.process(&[0.0, 1.0, 2.0, 3.0], |_| {});
let n_in_before = r.n_in;
r.set_rates(30_000, 96_000); assert_eq!(r.n_in, n_in_before);
r.set_rates(48_000, 96_000); assert_eq!(r.n_in, 0);
}
#[test]
fn stereo_pair_resamples_both_channels() {
let input: Vec<(f32, f32)> = (0..16).map(|i| (i as f32, -(i as f32))).collect();
let mut r = StreamingResampler::new(48_000, 96_000);
let mut out = Vec::new();
r.process(&input, |s| out.push(s));
assert!(out.len() > input.len());
assert!((out[0].0 - 0.0).abs() < 1e-6);
assert!((out[0].1 - 0.0).abs() < 1e-6);
for (l, rr) in &out {
assert!((l + rr).abs() < 1e-4);
}
}
}