use std::sync::Arc;
use crate::core::resample::{SincInterpTable, StreamingSincResampler, STREAM_SINC_HALF_TAPS};
use crate::engine::control::MIN_TEMPO_RATE;
pub(crate) const FEED_CHUNK_FRAMES: usize = 32;
pub(crate) const MAX_OUT_PER_FEED: usize = (FEED_CHUNK_FRAMES as f64 / MIN_TEMPO_RATE) as usize + 2;
#[derive(Debug)]
pub(crate) struct VarispeedHead {
resamplers: Vec<StreamingSincResampler>,
channels: usize,
ch_in: Vec<f32>,
ch_out: Vec<Vec<f32>>,
}
impl VarispeedHead {
pub(crate) fn new(channels: usize) -> Self {
let table = SincInterpTable::new_stream_default();
Self {
resamplers: (0..channels)
.map(|_| StreamingSincResampler::new(Arc::clone(&table)))
.collect(),
channels,
ch_in: vec![0.0; FEED_CHUNK_FRAMES],
ch_out: (0..channels)
.map(|_| Vec::with_capacity(MAX_OUT_PER_FEED))
.collect(),
}
}
#[cfg(test)]
pub(crate) fn feed(&mut self, interleaved: &[f32], rate: f64) -> usize {
self.feed_capped(interleaved, rate, usize::MAX)
}
pub(crate) fn feed_capped(&mut self, interleaved: &[f32], rate: f64, max_out: usize) -> usize {
let frames = interleaved.len() / self.channels;
debug_assert_eq!(interleaved.len() % self.channels, 0);
debug_assert!(frames <= FEED_CHUNK_FRAMES);
if frames == 0 {
for out in &mut self.ch_out {
out.clear();
}
return 0;
}
for ch in 0..self.channels {
for f in 0..frames {
self.ch_in[f] = interleaved[f * self.channels + ch];
}
let result = self.resamplers[ch].process_into_capped(
&self.ch_in[..frames],
rate,
&mut self.ch_out[ch],
max_out,
);
debug_assert!(result.is_ok(), "varispeed scratch overflow: {result:?}");
}
let produced = self.ch_out[0].len();
debug_assert!(self.ch_out.iter().all(|out| out.len() == produced));
produced
}
#[inline]
pub(crate) fn output(&self, ch: usize) -> &[f32] {
&self.ch_out[ch]
}
pub(crate) fn source_pos(&self) -> f64 {
self.resamplers[0].next_output_source_pos()
}
pub(crate) fn lookahead_frames(&self) -> usize {
STREAM_SINC_HALF_TAPS
}
pub(crate) fn reset(&mut self) {
for resampler in &mut self.resamplers {
resampler.reset();
}
for out in &mut self.ch_out {
out.clear();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unity_rate_is_sample_aligned_passthrough() {
let mut head = VarispeedHead::new(2);
let mut input = Vec::new();
for f in 0..FEED_CHUNK_FRAMES * 4 {
input.push(f as f32);
input.push(-(f as f32));
}
let mut left = Vec::new();
let mut right = Vec::new();
for chunk in input.chunks(FEED_CHUNK_FRAMES * 2) {
let produced = head.feed(chunk, 1.0);
left.extend_from_slice(&head.output(0)[..produced]);
right.extend_from_slice(&head.output(1)[..produced]);
}
assert_eq!(left.len(), FEED_CHUNK_FRAMES * 4 - head.lookahead_frames());
for (i, (&l, &r)) in left.iter().zip(right.iter()).enumerate() {
assert!((l - i as f32).abs() < 1e-3, "left[{i}] = {l}");
assert!((r + i as f32).abs() < 1e-3, "right[{i}] = {r}");
}
}
#[test]
fn channels_emit_identical_counts_across_retargets() {
let mut head = VarispeedHead::new(2);
let chunk = vec![0.25f32; FEED_CHUNK_FRAMES * 2];
for step in [1.0, 1.3, 0.8, 4.0, 0.25, 1.0] {
let produced = head.feed(&chunk, step);
assert_eq!(head.output(0).len(), produced);
assert_eq!(head.output(1).len(), produced);
assert!(produced <= MAX_OUT_PER_FEED);
}
}
#[test]
fn source_pos_tracks_rate_integral() {
let mut head = VarispeedHead::new(1);
let chunk = vec![0.5f32; FEED_CHUNK_FRAMES];
let mut emitted = 0usize;
for _ in 0..64 {
emitted += head.feed(&chunk, 1.25);
}
assert!((head.source_pos() - emitted as f64 * 1.25).abs() < 1e-6);
}
}