use crate::ring::RingProducer;
pub(crate) struct StreamingResampler {
from_hz: u32,
to_hz: u32,
factor: f64,
pos: f64,
tail: Option<f32>,
}
impl StreamingResampler {
pub(crate) fn new(from_hz: u32, to_hz: u32) -> Self {
StreamingResampler { from_hz, to_hz, factor: from_hz as f64 / to_hz as f64, pos: 0.0, tail: None }
}
pub(crate) fn factor(&self) -> f64 {
self.factor
}
pub(crate) fn output_len_for(&self, input_len: usize) -> usize {
if self.from_hz == self.to_hz || input_len == 0 {
return input_len;
}
let base = usize::from(self.tail.is_some());
let limit = (base + input_len) as f64 - 1.0;
let diff = limit - self.pos;
if diff <= 0.0 {
return 0;
}
(diff / self.factor).ceil() as usize
}
pub(crate) fn largest_prefix_within(&self, input_len: usize, room: usize) -> usize {
if self.output_len_for(input_len) <= room {
return input_len;
}
let (mut lo, mut hi) = (0usize, input_len);
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if self.output_len_for(mid) <= room {
lo = mid;
} else {
hi = mid - 1;
}
}
lo
}
pub(crate) fn process(&mut self, input: &[f32]) -> Vec<f32> {
if self.from_hz == self.to_hz {
return input.to_vec();
}
let n = input.len();
if n == 0 {
return Vec::new();
}
let base = usize::from(self.tail.is_some());
let max_local_idx = base + n - 1;
let value_at = |idx: usize| -> f32 {
let idx = idx.min(max_local_idx);
if base == 1 && idx == 0 { self.tail.unwrap_or(0.0) } else { input[idx - base] }
};
let count = self.output_len_for(n);
let mut out = Vec::with_capacity(count);
let mut p = self.pos;
for _ in 0..count {
let j = p.floor() as usize;
let t = (p - j as f64) as f32;
let a = value_at(j);
let b = value_at(j + 1);
out.push(a * (1.0 - t) + b * t);
p += self.factor;
}
self.pos = (p - max_local_idx as f64).max(0.0);
self.tail = Some(input[n - 1]);
out
}
}
pub(crate) struct ResamplingProducer {
producer: RingProducer,
resampler: Option<StreamingResampler>,
input_total_written: usize,
}
impl ResamplingProducer {
pub(crate) fn new(producer: RingProducer, from_hz: u32, to_hz: u32) -> Self {
let resampler = (from_hz != to_hz).then(|| StreamingResampler::new(from_hz, to_hz));
ResamplingProducer { producer, resampler, input_total_written: 0 }
}
pub(crate) fn push(&mut self, samples: &[f32]) -> usize {
let Some(resampler) = self.resampler.as_mut() else {
return self.producer.push(samples);
};
if samples.is_empty() {
return 0;
}
let room = self.producer.capacity().saturating_sub(self.producer.pending());
let prefix = resampler.largest_prefix_within(samples.len(), room);
if prefix == 0 {
return 0;
}
let out = resampler.process(&samples[..prefix]);
let accepted = self.producer.push(&out);
debug_assert_eq!(
accepted,
out.len(),
"room was computed to fit exactly this resampled prefix; a short accept here \
would desynchronise the resampler's state from what physically landed in the ring"
);
self.input_total_written += prefix;
prefix
}
pub(crate) fn pending(&self) -> usize {
match &self.resampler {
None => self.producer.pending(),
Some(r) => ((self.producer.pending() as f64) * r.factor()).ceil() as usize,
}
}
pub(crate) fn total_written(&self) -> usize {
match &self.resampler {
None => self.producer.total_written(),
Some(_) => self.input_total_written,
}
}
pub(crate) fn capacity(&self) -> usize {
match &self.resampler {
None => self.producer.capacity(),
Some(r) => ((self.producer.capacity() as f64) * r.factor()).ceil() as usize,
}
}
pub(crate) fn clear(&mut self) {
self.producer.clear()
}
pub(crate) fn set_paused(&mut self, paused: bool) {
self.producer.set_paused(paused)
}
pub(crate) fn is_paused(&self) -> bool {
self.producer.is_paused()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ring::ring;
#[test]
fn passthrough_at_equal_rates_returns_input_unchanged() {
let mut r = StreamingResampler::new(24_000, 24_000);
let input = vec![0.1, -0.2, 0.3, 0.0, 1.0, -1.0];
assert_eq!(r.process(&input), input, "equal rates must be sample-for-sample identity");
let input2 = vec![9.0, 8.0, 7.0];
assert_eq!(r.process(&input2), input2);
}
#[test]
fn upsampling_roughly_doubles_length() {
let input: Vec<f32> = (0..1000).map(|i| i as f32).collect();
let mut r = StreamingResampler::new(24_000, 48_000);
let out = r.process(&input);
assert!(
out.len() >= 1990 && out.len() <= 2000,
"expected close to 2000 output samples, got {}",
out.len()
);
}
#[test]
fn downsampling_roughly_halves_length() {
let input: Vec<f32> = (0..1000).map(|i| i as f32).collect();
let mut r = StreamingResampler::new(48_000, 24_000);
let out = r.process(&input);
assert!(
out.len() >= 495 && out.len() <= 500,
"expected close to 500 output samples, got {}",
out.len()
);
}
fn sine(n: usize, freq_hz: f32, sample_rate: f32) -> Vec<f32> {
(0..n).map(|i| (2.0 * std::f32::consts::PI * freq_hz * i as f32 / sample_rate).sin()).collect()
}
#[test]
fn no_discontinuity_at_chunk_boundaries() {
let sr_in = 24_000.0f32;
let sr_out = 48_000.0f32;
let freq = 440.0f32;
let total = 24_000usize; let input = sine(total, freq, sr_in);
let mut r = StreamingResampler::new(sr_in as u32, sr_out as u32);
let mut out = Vec::new();
let mut i = 0usize;
let mut chunk_len = 17usize; while i < input.len() {
let end = (i + chunk_len).min(input.len());
out.extend(r.process(&input[i..end]));
i = end;
chunk_len = (chunk_len % 61) + 13; }
let max_slope = 2.0 * std::f32::consts::PI * freq / sr_out;
let bound = max_slope * 3.0;
let mut max_delta = 0.0f32;
for w in out.windows(2) {
max_delta = max_delta.max((w[1] - w[0]).abs());
}
assert!(
max_delta <= bound,
"discontinuity at a chunk boundary: max consecutive-sample delta {max_delta} \
exceeds the signal's own slope-derived bound {bound} (chunks={}, out_len={})",
i / 17 + 1,
out.len()
);
}
#[test]
fn feeding_the_same_signal_in_small_chunks_matches_one_big_call() {
let sr_in = 24_000u32;
let sr_out = 44_100u32; let input = sine(10_000, 440.0, sr_in as f32);
let mut chunked = StreamingResampler::new(sr_in, sr_out);
let mut chunked_out = Vec::new();
for chunk in input.chunks(23) {
chunked_out.extend(chunked.process(chunk));
}
let mut single = StreamingResampler::new(sr_in, sr_out);
let single_out = single.process(&input);
assert!(
(chunked_out.len() as i64 - single_out.len() as i64).abs() <= 2,
"chunked len {} vs single-call len {} differ by more than the expected \
end-of-stream residual",
chunked_out.len(),
single_out.len()
);
let common = chunked_out.len().min(single_out.len());
let mut max_diff = 0.0f32;
for i in 0..common {
max_diff = max_diff.max((chunked_out[i] - single_out[i]).abs());
}
assert!(
max_diff < 1e-4,
"chunked and single-call resampling disagree by up to {max_diff}, expected them \
to agree closely (streaming state should make chunking invisible to the math)"
);
}
#[test]
fn passthrough_producer_has_no_conversion_at_equal_rates() {
let (raw, _cons) = ring(64);
let mut rp = ResamplingProducer::new(raw, 24_000, 24_000);
assert_eq!(rp.push(&[1.0, 2.0, 3.0]), 3);
assert_eq!(rp.pending(), 3, "equal rates: pending is untouched ring pending, no arithmetic");
assert_eq!(rp.total_written(), 3);
assert_eq!(rp.capacity(), 64);
}
#[test]
fn unit_contract_pending_and_total_written_are_input_units_and_converge_to_zero() {
let (raw, mut cons) = ring(4096);
let mut rp = ResamplingProducer::new(raw, 24_000, 48_000);
let input: Vec<f32> = (0..500).map(|i| i as f32 * 0.001).collect();
let n = rp.push(&input);
assert_eq!(n, 500, "small push relative to a large ring should be fully accepted");
assert_eq!(rp.total_written(), 500, "total_written is in input units");
let pending = rp.pending();
assert!((pending as i64 - 500).abs() <= 2, "expected pending() close to 500, got {pending}");
let mut out = [0.0f32; 200];
for _ in 0..20 {
cons.fill(&mut out, 1);
}
assert_eq!(rp.pending(), 0, "pending must converge to zero (in input units) once played");
assert_eq!(rp.total_written(), 500, "total_written never decreases");
}
#[test]
fn capacity_is_input_equivalent_and_matches_a_true_ten_second_buffer() {
let device_rate = 48_000u32;
let input_rate = 24_000u32;
let (raw, _cons) = ring(device_rate as usize * 10);
let rp = ResamplingProducer::new(raw, input_rate, device_rate);
assert_eq!(rp.capacity(), input_rate as usize * 10);
}
#[test]
fn partial_acceptance_returns_short_count_and_resumes_without_loss_or_repeat() {
let device_rate = 48_000u32;
let input_rate = 24_000u32;
let (raw, mut cons) = ring(64); let mut rp = ResamplingProducer::new(raw, input_rate, device_rate);
let input = sine(2000, 300.0, input_rate as f32);
let mut consumed = 0usize;
let mut played = Vec::new();
let mut out_buf = [0.0f32; 16];
let mut guard = 0;
while consumed < input.len() {
let n = rp.push(&input[consumed..]);
if n == 0 {
cons.fill(&mut out_buf, 1);
played.extend_from_slice(&out_buf);
} else {
consumed += n;
}
guard += 1;
assert!(guard < 1_000_000, "made no progress -- partial acceptance stuck");
}
assert_eq!(consumed, input.len(), "every input sample must eventually be consumed");
for _ in 0..device_rate as usize {
cons.fill(&mut out_buf, 1);
played.extend_from_slice(&out_buf);
}
while played.last() == Some(&0.0) {
played.pop();
}
let mut oneshot = StreamingResampler::new(input_rate, device_rate);
let expected = oneshot.process(&input);
assert!(
(played.len() as i64 - expected.len() as i64).abs() <= 2,
"played {} samples vs {} expected from a one-shot resample -- suggests loss or \
duplication across a partial-accept boundary",
played.len(),
expected.len()
);
let common = played.len().min(expected.len());
let mut max_diff = 0.0f32;
for i in 0..common {
max_diff = max_diff.max((played[i] - expected[i]).abs());
}
assert!(
max_diff < 1e-4,
"played audio diverges from the one-shot reference by up to {max_diff} -- a \
partial accept must resume the resampler exactly where it left off"
);
}
#[test]
fn a_following_push_after_a_short_accept_does_not_repeat_the_accepted_prefix() {
let (raw, _cons) = ring(4); let mut rp = ResamplingProducer::new(raw, 24_000, 48_000);
let input: Vec<f32> = (0..50).map(|i| i as f32).collect();
let n1 = rp.push(&input);
assert!(n1 > 0 && n1 < input.len(), "expected a short accept, got {n1} of {}", input.len());
assert_eq!(rp.total_written(), n1);
let remainder = &input[n1..];
assert!(!remainder.is_empty());
}
}