use num_traits::Float;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use realfft::FftNum;
use crate::{Config, Error, PlanarResampler, PlanarVecs, config::DerivedConfig, core::ArdftsrcCore};
pub struct InterleavedResampler<T = f64>
where
T: Float + FftNum,
{
config: Config,
derived: DerivedConfig<T>,
pub(crate) cores: Vec<ArdftsrcCore<T>>,
input_staging: Vec<Vec<T>>,
output_staging: Vec<Vec<T>>,
}
impl<T> InterleavedResampler<T>
where
T: Float + FftNum,
{
pub fn new(config: Config) -> Result<Self, Error> {
let derived = config.derive_config::<T>()?;
let cores = (0..config.channels)
.map(|_| ArdftsrcCore::new(derived.clone()))
.collect();
let input_staging = vec![vec![T::zero(); derived.input_chunk_frames]; config.channels];
let output_staging = vec![vec![T::zero(); derived.output_chunk_frames]; config.channels];
Ok(Self {
config,
derived,
cores,
input_staging,
output_staging,
})
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
#[inline]
pub fn input_sample_processed(&self) -> usize {
self.cores.iter().map(ArdftsrcCore::input_sample_processed).sum()
}
#[inline]
pub fn output_sample_processed(&self) -> usize {
self.cores.iter().map(ArdftsrcCore::output_sample_processed).sum()
}
#[must_use]
#[inline]
pub fn input_buffer_size(&self) -> usize {
self.derived.input_chunk_frames * self.config.channels
}
#[must_use]
#[inline]
pub fn output_buffer_size(&self) -> usize {
self.derived.output_chunk_frames * self.config.channels
}
#[must_use]
#[inline]
pub fn output_delay_frames(&self) -> usize {
if self.is_passthrough() {
0
} else {
self.derived.output_offset
}
}
#[must_use]
#[inline]
pub fn expected_output_size(&self, input_size: usize) -> usize {
(input_size * self.config.output_sample_rate).div_ceil(self.config.input_sample_rate)
}
pub fn process_all<'a>(&mut self, input: &[T]) -> Result<PlanarVecs<T>, Error>
where
T: Send + Sync,
{
if self.is_passthrough() {
let input_planar: Vec<Vec<T>> = (0..self.config.channels)
.map(|channel| input[channel..].iter().step_by(self.config.channels).copied().collect())
.collect();
return Ok(PlanarVecs::new(input_planar)?);
}
let input_planar: Vec<Vec<T>> = (0..self.config.channels)
.map(|channel| input[channel..].iter().step_by(self.config.channels).copied().collect())
.collect();
#[cfg(feature = "rayon")]
let output: Vec<Vec<T>> = self
.cores
.par_iter_mut()
.zip(input_planar.par_iter())
.map(|(core, channel)| core.process_all(channel))
.collect::<Result<_, _>>()?;
#[cfg(not(feature = "rayon"))]
let output: Vec<Vec<T>> = {
let mut output = Vec::with_capacity(self.config.channels);
for (core, channel) in self.cores.iter_mut().zip(input_planar.iter()) {
output.push(core.process_all(channel)?);
}
output
};
PlanarVecs::new(output)
}
pub fn reset(&mut self) {
for core in &mut self.cores {
core.reset();
}
}
pub fn process_chunk<'a>(&mut self, input: &[T], output: &mut [T]) -> Result<usize, Error> {
self.ensure_output_buffer_shape(output)?;
self.ensure_input_buffer_shape(input, false)?;
self.input_to_staging(input);
let (cores, input_staging, output_staging) =
(&mut self.cores, &mut self.input_staging, &mut self.output_staging);
let num_input_frames = input.len() / self.config.channels;
let total_written = Self::process_chunk_inner(cores, input_staging, num_input_frames, output_staging, false)?;
self.staging_to_output(output, total_written);
Ok(total_written)
}
pub fn process_chunk_final<'a>(&mut self, input: &[T], output: &mut [T]) -> Result<usize, Error> {
self.ensure_output_buffer_shape(output)?;
self.ensure_input_buffer_shape(input, true)?;
self.input_to_staging(input);
let (cores, input_staging, output_staging) =
(&mut self.cores, &mut self.input_staging, &mut self.output_staging);
let num_input_frames = input.len() / self.config.channels;
let total_written = Self::process_chunk_inner(cores, input_staging, num_input_frames, output_staging, true)?;
self.staging_to_output(output, total_written);
Ok(total_written)
}
#[inline]
fn process_chunk_inner(
cores: &mut [ArdftsrcCore<T>],
input: &[Vec<T>],
num_frames: usize,
output: &mut [Vec<T>],
is_final: bool,
) -> Result<usize, Error> {
let mut total_written = 0;
for (channel_idx, (channel_input, channel_output)) in input.into_iter().zip(output).enumerate() {
let core_output = cores[channel_idx].process_chunk(&channel_input[..num_frames], is_final)?;
channel_output[..core_output.len()].copy_from_slice(&core_output);
total_written += core_output.len();
}
Ok(total_written)
}
pub fn finalize<'a>(&mut self, output: &mut [T]) -> Result<usize, Error> {
self.ensure_output_buffer_shape(output)?;
let mut total_written = 0;
for (channel_idx, core) in self.cores.iter_mut().enumerate() {
let core_output = core.finalize()?;
self.output_staging[channel_idx][..core_output.len()].copy_from_slice(&core_output);
total_written += core_output.len();
}
self.staging_to_output(output, total_written);
Ok(total_written)
}
#[inline]
fn ensure_input_buffer_shape<'a>(&self, input: &[T], is_final: bool) -> Result<(), Error> {
if !input.len().is_multiple_of(self.config.channels) {
return Err(Error::MalformedInputLength {
channels: self.config.channels,
samples: input.len(),
});
}
let frames = input.len() / self.config.channels;
if (!is_final && frames != self.derived.input_chunk_frames)
|| (is_final && frames > self.derived.input_chunk_frames)
{
return Err(Error::WrongFrameCount {
expected: self.derived.input_chunk_frames,
actual: frames,
});
}
Ok(())
}
#[inline]
fn ensure_output_buffer_shape<'a>(&self, output: &[T]) -> Result<(), Error> {
let expected = self.output_buffer_size();
if output.len() < expected {
return Err(Error::InsufficientOutputBuffer {
expected,
actual: output.len(),
});
}
Ok(())
}
#[inline]
fn is_passthrough(&self) -> bool {
self.config.input_sample_rate == self.config.output_sample_rate
}
pub fn pre<'a>(&mut self, pre: Vec<T>) -> Result<(), Error> {
if !pre.len().is_multiple_of(self.config.channels) {
return Err(Error::MalformedInputLength {
channels: self.config.channels,
samples: pre.len(),
});
}
let channels = self.config.channels;
let max_samples = self.input_buffer_size();
let start = pre.len().saturating_sub(max_samples);
let start = start.div_ceil(channels) * channels;
for (channel_idx, core) in self.cores.iter_mut().enumerate() {
let samples = pre[start + channel_idx..].iter().step_by(channels).copied().collect();
core.pre(samples);
}
Ok(())
}
pub fn post<'a>(&mut self, post: Vec<T>) -> Result<(), Error> {
if !post.len().is_multiple_of(self.config.channels) {
return Err(Error::MalformedInputLength {
channels: self.config.channels,
samples: post.len(),
});
}
let channels = self.config.channels;
let end = post.len().min(self.input_buffer_size());
for (channel_idx, core) in self.cores.iter_mut().enumerate() {
let samples = post[channel_idx..end].iter().step_by(channels).copied().collect();
core.post(samples);
}
Ok(())
}
pub fn batch(&self, inputs: &[&[T]]) -> Result<Vec<PlanarVecs<T>>, Error>
where
T: Send + Sync,
{
let prepared_inputs = self.batch_prepare_interleaved_inputs(inputs)?;
let planar_adapter = PlanarResampler::new(self.config.clone())?;
planar_adapter.batch(prepared_inputs)
}
pub fn batch_gapless(&self, inputs: &[&[T]]) -> Result<Vec<PlanarVecs<T>>, Error>
where
T: Send + Sync,
{
let prepared_inputs = self.batch_prepare_interleaved_inputs(inputs)?;
let planar_adapter = PlanarResampler::new(self.config.clone())?;
planar_adapter.batch_gapless(prepared_inputs)
}
fn batch_prepare_interleaved_inputs(&self, inputs: &[&[T]]) -> Result<Vec<PlanarVecs<T>>, Error>
where
T: Send + Sync,
{
#[cfg(feature = "rayon")]
{
inputs
.par_iter()
.map(|input| Self::batch_deinterleave_track(input, self.config.channels))
.collect()
}
#[cfg(not(feature = "rayon"))]
{
inputs
.iter()
.map(|input| Self::batch_deinterleave_track(input, self.config.channels))
.collect()
}
}
fn batch_deinterleave_track(input: &[T], channels: usize) -> Result<PlanarVecs<T>, Error> {
if !input.len().is_multiple_of(channels) {
return Err(Error::MalformedInputLength {
channels,
samples: input.len(),
});
}
let frames = input.len() / channels;
let mut per_channel = vec![Vec::with_capacity(frames); channels];
for frame in input.chunks_exact(channels) {
for (channel_idx, sample) in frame.iter().copied().enumerate() {
per_channel[channel_idx].push(sample);
}
}
PlanarVecs::new(per_channel)
}
fn input_to_staging(&mut self, input: &[T]) {
let channels = self.config.channels;
let frames = input.len() / channels;
debug_assert_eq!(input.len(), frames * channels);
for channel in &mut self.input_staging {
channel.resize(frames, T::zero());
}
for (frame_idx, frame) in input.chunks_exact(channels).enumerate() {
for (channel_idx, sample) in frame.iter().copied().enumerate() {
self.input_staging[channel_idx][frame_idx] = sample;
}
}
}
fn staging_to_output(&self, output: &mut [T], written: usize) {
let channels = self.config.channels;
let frames = written / channels;
debug_assert_eq!(written, frames * channels);
debug_assert!(output.len() >= written);
debug_assert_eq!(self.output_staging.len(), channels);
debug_assert!(self.output_staging.iter().all(|channel| channel.len() >= frames));
for (frame_idx, frame) in output[..written].chunks_exact_mut(channels).enumerate() {
for (channel_idx, sample) in frame.iter_mut().enumerate() {
*sample = self.output_staging[channel_idx][frame_idx];
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
TaperType,
core::ArdftsrcCore,
test_utils::{assert_no_nans, process_all_samples},
};
use dasp_signal::Signal;
fn input_chunk_frames(resampler: &InterleavedResampler<f32>) -> usize {
resampler.input_buffer_size() / resampler.config().channels
}
fn output_chunk_frames(resampler: &InterleavedResampler<f32>) -> usize {
resampler.output_buffer_size() / resampler.config().channels
}
fn process_chunk_samples(
resampler: &mut InterleavedResampler<f32>,
input: &[f32],
output: &mut [f32],
) -> Result<usize, Error> {
let written = resampler.process_chunk(input, output)?;
assert_no_nans(&output[..written], "chunk::process_chunk_samples output");
Ok(written)
}
fn process_chunk_final_samples(
resampler: &mut InterleavedResampler<f32>,
input: &[f32],
output: &mut [f32],
) -> Result<usize, Error> {
let written = resampler.process_chunk_final(input, output)?;
assert_no_nans(&output[..written], "chunk::process_chunk_final_samples output");
Ok(written)
}
fn finalize_samples_chunk(resampler: &mut InterleavedResampler<f32>, output: &mut [f32]) -> Result<usize, Error> {
let written = resampler.finalize(output)?;
assert_no_nans(&output[..written], "chunk::finalize_samples_chunk output");
Ok(written)
}
fn mono_config(input_sample_rate: usize, output_sample_rate: usize) -> Config {
Config {
input_sample_rate,
output_sample_rate,
channels: 1,
quality: 64,
bandwidth: 0.95,
taper_type: TaperType::Cosine(3.45),
..Config::default()
}
}
fn stereo_config(input_sample_rate: usize, output_sample_rate: usize) -> Config {
Config {
channels: 2,
..mono_config(input_sample_rate, output_sample_rate)
}
}
fn resample_stream_with_context(
config: Config,
input: &[f32],
pre: Option<&[f32]>,
post: Option<&[f32]>,
) -> Vec<f32> {
let mut resampler = InterleavedResampler::new(config).unwrap();
if let Some(pre) = pre {
resampler.pre(pre.to_vec()).unwrap();
}
if let Some(post) = post {
resampler.post(post.to_vec()).unwrap();
}
let input_buffer_size = resampler.input_buffer_size();
let full_chunks = input.len() / input_buffer_size;
let has_partial = !input.len().is_multiple_of(input_buffer_size);
let output_blocks = full_chunks + usize::from(has_partial) + 1;
let mut output = vec![0.0; output_blocks * resampler.output_buffer_size()];
let mut written = 0;
let mut offset = 0;
while offset + input_buffer_size <= input.len() {
written += process_chunk_samples(
&mut resampler,
&input[offset..offset + input_buffer_size],
&mut output[written..],
)
.unwrap();
offset += input_buffer_size;
}
written += process_chunk_final_samples(&mut resampler, &input[offset..], &mut output[written..]).unwrap();
written += finalize_samples_chunk(&mut resampler, &mut output[written..]).unwrap();
output.truncate(written);
output
}
fn max_abs_error_with_index(actual: &[f32], expected: &[f32]) -> (f32, usize) {
assert_eq!(actual.len(), expected.len());
let mut max_abs_error = 0.0f32;
let mut max_abs_error_idx = 0;
for (idx, (left, right)) in actual.iter().zip(expected.iter()).enumerate() {
let abs_error = (left - right).abs();
if abs_error > max_abs_error {
max_abs_error = abs_error;
max_abs_error_idx = idx;
}
}
(max_abs_error, max_abs_error_idx)
}
fn run_core_process_all(core: &mut ArdftsrcCore<f32>, input: &[f32]) -> Vec<f32> {
let mut output = Vec::new();
let mut offset = 0;
let input_chunk = core.input_buffer_size();
while offset + input_chunk <= input.len() {
let written = core.process_chunk(&input[offset..offset + input_chunk], false).unwrap();
output.extend_from_slice(written);
offset += input_chunk;
}
let final_input = &input[offset..];
let written = core.process_chunk(final_input, true).unwrap();
output.extend_from_slice(written);
let written = core.finalize().unwrap();
output.extend_from_slice(written);
assert_no_nans(&output, "chunk::run_core_process_all output");
output
}
fn deinterleave_channel(samples: &[f32], channels: usize, channel: usize) -> Vec<f32> {
samples.chunks_exact(channels).map(|frame| frame[channel]).collect()
}
#[test]
fn silence_stays_silent() {
let mut resampler = InterleavedResampler::new(mono_config(48_000, 48_000)).unwrap();
let input = vec![0.0; input_chunk_frames(&resampler)];
let output = process_all_samples(&mut resampler, &input).unwrap();
assert!(output.iter().all(|sample| *sample == 0.0));
}
#[test]
fn same_rate_passthrough_preserves_samples() {
let mut resampler = InterleavedResampler::new(mono_config(48_000, 48_000)).unwrap();
let input: Vec<f32> = (0..input_chunk_frames(&resampler) * 2 + 7)
.map(|frame| (frame as f32 * 0.013).cos())
.collect();
let output = process_all_samples(&mut resampler, &input).unwrap();
assert_eq!(output, input);
assert_eq!(resampler.output_delay_frames(), 0);
}
#[test]
fn impulse_output_is_finite() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let mut input = vec![0.0; input_chunk_frames(&resampler)];
input[0] = 1.0;
let output = process_all_samples(&mut resampler, &input).unwrap();
assert_eq!(output.len(), resampler.expected_output_size(input.len()));
assert!(output.iter().all(|sample| sample.is_finite()));
}
#[test]
fn first_chunk_lpc_start_edge_is_finite() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input: Vec<f32> = (0..input_chunk_frames(&resampler))
.map(|frame| (frame as f32 * 0.01).sin() * 0.25)
.collect();
let mut output = vec![0.0; output_chunk_frames(&resampler)];
let written = process_chunk_samples(&mut resampler, &input, &mut output).unwrap();
assert_eq!(
written,
output_chunk_frames(&resampler) - resampler.output_delay_frames()
);
assert!(output[..written].iter().all(|sample| sample.is_finite()));
assert!(output[..written].iter().any(|sample| sample.abs() > 1e-6));
}
#[test]
fn short_final_chunk_lpc_stop_edge_is_finite_and_bounded() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input_frames = input_chunk_frames(&resampler) / 3;
let input: Vec<f32> = (0..input_frames)
.map(|frame| (frame as f32 * 0.02).sin() * 0.1)
.collect();
let output = process_all_samples(&mut resampler, &input).unwrap();
assert_eq!(output.len(), resampler.expected_output_size(input_frames));
assert!(output.iter().all(|sample| sample.is_finite()));
assert!(output.iter().all(|sample| sample.abs() < 1.0));
}
#[test]
fn expected_output_size_matches_frame_count_conversion() {
let resampler = InterleavedResampler::<f32>::new(stereo_config(44_100, 48_000)).unwrap();
let input_samples = resampler.input_buffer_size() * 2 + 14;
let input_frames = input_samples / resampler.config().channels;
let expected_from_frames = resampler.expected_output_size(input_frames) * resampler.config().channels;
let expected_from_samples = resampler.expected_output_size(input_samples);
assert_eq!(expected_from_samples, expected_from_frames);
}
#[test]
fn expected_output_size_accepts_non_interleaved_length() {
let resampler = InterleavedResampler::<f32>::new(stereo_config(44_100, 48_000)).unwrap();
let expected = (3usize * 48_000).div_ceil(44_100);
assert_eq!(resampler.expected_output_size(3), expected);
}
#[test]
fn stereo_wrapper_matches_channel_core_outputs() {
let config = stereo_config(44_100, 48_000);
let mut wrapper = InterleavedResampler::<f32>::new(config.clone()).unwrap();
let input_frames = input_chunk_frames(&wrapper) * 2 + 37;
let input: Vec<f32> = (0..input_frames)
.flat_map(|frame| {
let t = frame as f32;
[(t * 0.01).sin() * 0.25, (t * 0.017).cos() * 0.2]
})
.collect();
let context_frames = input_chunk_frames(&wrapper) / 2;
let pre: Vec<f32> = (0..context_frames)
.flat_map(|frame| {
let t = frame as f32;
[(t * 0.03).sin() * 0.1, (t * 0.027).cos() * 0.08]
})
.collect();
let post: Vec<f32> = (0..context_frames)
.flat_map(|frame| {
let t = frame as f32;
[(t * 0.021).sin() * 0.09, (t * 0.012).cos() * 0.07]
})
.collect();
wrapper.pre(pre.clone()).unwrap();
wrapper.post(post.clone()).unwrap();
let wrapped_output = process_all_samples(&mut wrapper, &input).unwrap();
let mono_config = Config {
channels: 1,
..config.clone()
};
let derived = mono_config.derive_config::<f32>().unwrap();
let mut left_core = ArdftsrcCore::<f32>::new(derived.clone());
let mut right_core = ArdftsrcCore::<f32>::new(derived);
left_core.pre(deinterleave_channel(&pre, 2, 0));
right_core.pre(deinterleave_channel(&pre, 2, 1));
left_core.post(deinterleave_channel(&post, 2, 0));
right_core.post(deinterleave_channel(&post, 2, 1));
let left_output = run_core_process_all(&mut left_core, &deinterleave_channel(&input, 2, 0));
let right_output = run_core_process_all(&mut right_core, &deinterleave_channel(&input, 2, 1));
assert_eq!(left_output.len(), right_output.len());
assert_eq!(wrapped_output.len(), left_output.len() * 2);
for frame_idx in 0..left_output.len() {
let left = wrapped_output[frame_idx * 2];
let right = wrapped_output[frame_idx * 2 + 1];
assert!((left - left_output[frame_idx]).abs() < 1e-6);
assert!((right - right_output[frame_idx]).abs() < 1e-6);
}
}
#[test]
fn pre_and_post_reject_wrong_channel_count() {
let mut resampler = InterleavedResampler::<f32>::new(stereo_config(44_100, 48_000)).unwrap();
let unaligned_context = [0.0f32; 9];
assert!(matches!(
resampler.pre(unaligned_context.to_vec()),
Err(Error::MalformedInputLength {
channels: 2,
samples: 9
})
));
assert!(matches!(
resampler.post(unaligned_context.to_vec()),
Err(Error::MalformedInputLength {
channels: 2,
samples: 9
})
));
}
#[test]
fn pre_context_changes_first_chunk_output() {
let mut with_zero_pre = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let mut with_one_pre = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input: Vec<f32> = (0..input_chunk_frames(&with_zero_pre))
.map(|frame| (frame as f32 * 0.01).sin() * 0.25)
.collect();
let mut out_zero = vec![0.0; output_chunk_frames(&with_zero_pre)];
let mut out_one = vec![0.0; output_chunk_frames(&with_one_pre)];
let pre_zero = vec![0.0; input_chunk_frames(&with_zero_pre)];
let pre_one = vec![1.0; input_chunk_frames(&with_one_pre)];
with_zero_pre.pre(pre_zero).unwrap();
with_one_pre.pre(pre_one).unwrap();
let written_zero = process_chunk_samples(&mut with_zero_pre, &input, &mut out_zero).unwrap();
let written_one = process_chunk_samples(&mut with_one_pre, &input, &mut out_one).unwrap();
assert_eq!(written_zero, written_one);
assert!(
out_zero[..written_zero]
.iter()
.zip(out_one[..written_one].iter())
.any(|(a, b)| (a - b).abs() > 1e-6)
);
}
#[test]
fn post_context_changes_flush_output() {
let mut with_zero_post = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let mut with_one_post = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input: Vec<f32> = (0..input_chunk_frames(&with_zero_post))
.map(|frame| (frame as f32 * 0.015).cos() * 0.125)
.collect();
let mut chunk_out = vec![0.0; output_chunk_frames(&with_zero_post)];
let mut flush_zero = vec![0.0; output_chunk_frames(&with_zero_post)];
let mut flush_one = vec![0.0; output_chunk_frames(&with_one_post)];
let post_zero = vec![0.0; input_chunk_frames(&with_zero_post)];
let post_one = vec![1.0; input_chunk_frames(&with_one_post)];
with_zero_post.post(post_zero).unwrap();
with_one_post.post(post_one).unwrap();
process_chunk_samples(&mut with_zero_post, &input, &mut chunk_out).unwrap();
process_chunk_samples(&mut with_one_post, &input, &mut chunk_out).unwrap();
let written_zero = finalize_samples_chunk(&mut with_zero_post, &mut flush_zero).unwrap();
let written_one = finalize_samples_chunk(&mut with_one_post, &mut flush_one).unwrap();
assert_eq!(written_zero, written_one);
assert!(
flush_zero[..written_zero]
.iter()
.zip(flush_one[..written_one].iter())
.any(|(a, b)| (a - b).abs() > 1e-6)
);
}
#[test]
fn flush_after_full_chunk_uses_lpc_tail_edge() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input: Vec<f32> = (0..input_chunk_frames(&resampler))
.map(|frame| (frame as f32 * 0.015).cos() * 0.125)
.collect();
let mut output = vec![0.0; output_chunk_frames(&resampler)];
let first_written = process_chunk_samples(&mut resampler, &input, &mut output).unwrap();
let flush_written = finalize_samples_chunk(&mut resampler, &mut output).unwrap();
assert_eq!(first_written + flush_written, output_chunk_frames(&resampler));
assert!(output[..flush_written].iter().all(|sample| sample.is_finite()));
}
#[test]
fn streaming_and_offline_paths_match() {
let config = mono_config(44_100, 48_000);
let mut offline = InterleavedResampler::new(config.clone()).unwrap();
let mut streaming = InterleavedResampler::new(config).unwrap();
let input_frames = input_chunk_frames(&streaming) * 2 + input_chunk_frames(&streaming) / 3;
let input: Vec<f32> = (0..input_frames)
.map(|frame| (frame as f32 * 0.01).sin() * 0.25)
.collect();
let offline_output = process_all_samples(&mut offline, &input).unwrap();
let full_chunks = input.len() / input_chunk_frames(&streaming);
let has_partial = !input.len().is_multiple_of(input_chunk_frames(&streaming));
let output_blocks = full_chunks + usize::from(has_partial) + 1;
let mut streaming_output = vec![0.0; output_blocks * output_chunk_frames(&streaming)];
let mut written = 0;
let chunk_len = input_chunk_frames(&streaming);
let mut offset = 0;
while offset + chunk_len <= input.len() {
written += process_chunk_samples(
&mut streaming,
&input[offset..offset + chunk_len],
&mut streaming_output[written..],
)
.unwrap();
offset += chunk_len;
}
written +=
process_chunk_final_samples(&mut streaming, &input[offset..], &mut streaming_output[written..]).unwrap();
written += finalize_samples_chunk(&mut streaming, &mut streaming_output[written..]).unwrap();
streaming_output.truncate(written);
streaming_output.truncate(offline_output.len());
assert_eq!(offline_output.len(), streaming_output.len());
for (left, right) in offline_output.iter().zip(streaming_output) {
assert!((left - right).abs() < 1e-5);
}
}
#[test]
fn split_resampling_with_pre_post_matches_full_stream() {
let mut config = mono_config(44_100, 48_000);
config.taper_type = TaperType::Cosine(1.55);
let mut full_resampler = InterleavedResampler::new(config.clone()).unwrap();
let context_len = full_resampler.input_buffer_size();
let split_frames = 4 * config.input_sample_rate;
let total_frames = split_frames * 3;
let output_sample_rate = config.output_sample_rate;
let input: Vec<f32> = dasp_signal::rate(config.input_sample_rate as f64)
.const_hz(440.0)
.sine()
.take(total_frames)
.map(|sample| sample as f32 * 0.25)
.collect();
let full_output = process_all_samples(&mut full_resampler, &input).unwrap();
let split0 = &input[..split_frames];
let split1 = &input[split_frames..split_frames * 2];
let split2 = &input[split_frames * 2..];
let split0_post = &split1[..context_len];
let split1_pre = &split0[split0.len() - context_len..];
let split1_post = &split2[..context_len];
let split2_pre = &split1[split1.len() - context_len..];
let out0 = resample_stream_with_context(config.clone(), split0, None, Some(split0_post));
let out1 = resample_stream_with_context(config.clone(), split1, Some(split1_pre), Some(split1_post));
let out2 = resample_stream_with_context(config, split2, Some(split2_pre), None);
let mut stitched = Vec::with_capacity(out0.len() + out1.len() + out2.len());
stitched.extend_from_slice(&out0);
stitched.extend_from_slice(&out1);
stitched.extend_from_slice(&out2);
let tolerance = 5e-7f32;
assert_eq!(stitched.len(), full_output.len());
let (max_abs_error, max_abs_error_idx) = max_abs_error_with_index(&stitched, &full_output);
let output_seam_1 = out0.len();
let output_seam_2 = out0.len() + out1.len();
let nearest_seam_distance = max_abs_error_idx
.abs_diff(output_seam_1)
.min(max_abs_error_idx.abs_diff(output_seam_2));
let max_error_time_seconds = max_abs_error_idx as f64 / output_sample_rate as f64;
eprintln!(
"max_abs_error={max_abs_error} at sample={max_abs_error_idx} (~{max_error_time_seconds:.6}s), seam_1={output_seam_1}, seam_2={output_seam_2}, nearest_seam_distance={nearest_seam_distance}"
);
assert!(
max_abs_error < tolerance,
"max_abs_error={max_abs_error}, tolerance={tolerance}"
);
}
#[test]
fn split_resampling_is_worse_without_pre_post() {
let config = mono_config(44_100, 48_000);
let mut full_resampler = InterleavedResampler::new(config.clone()).unwrap();
let context_len = full_resampler.input_buffer_size();
let split_frames = 4 * config.input_sample_rate;
let total_frames = split_frames * 3;
let output_sample_rate = config.output_sample_rate;
let input: Vec<f32> = dasp_signal::rate(config.input_sample_rate as f64)
.const_hz(440.0)
.sine()
.take(total_frames)
.map(|sample| sample as f32 * 0.25)
.collect();
let full_output = process_all_samples(&mut full_resampler, &input).unwrap();
let split0 = &input[..split_frames];
let split1 = &input[split_frames..split_frames * 2];
let split2 = &input[split_frames * 2..];
let split0_post = &split1[..context_len];
let split1_pre = &split0[split0.len() - context_len..];
let split1_post = &split2[..context_len];
let split2_pre = &split1[split1.len() - context_len..];
let with_out0 = resample_stream_with_context(config.clone(), split0, None, Some(split0_post));
let with_out1 = resample_stream_with_context(config.clone(), split1, Some(split1_pre), Some(split1_post));
let with_out2 = resample_stream_with_context(config.clone(), split2, Some(split2_pre), None);
let without_out0 = resample_stream_with_context(config.clone(), split0, None, None);
let without_out1 = resample_stream_with_context(config.clone(), split1, None, None);
let without_out2 = resample_stream_with_context(config, split2, None, None);
let mut with_context = Vec::with_capacity(with_out0.len() + with_out1.len() + with_out2.len());
with_context.extend_from_slice(&with_out0);
with_context.extend_from_slice(&with_out1);
with_context.extend_from_slice(&with_out2);
let mut without_context = Vec::with_capacity(without_out0.len() + without_out1.len() + without_out2.len());
without_context.extend_from_slice(&without_out0);
without_context.extend_from_slice(&without_out1);
without_context.extend_from_slice(&without_out2);
let (with_max_abs_error, with_max_abs_error_idx) = max_abs_error_with_index(&with_context, &full_output);
let (without_max_abs_error, without_max_abs_error_idx) =
max_abs_error_with_index(&without_context, &full_output);
eprintln!(
"with_context max_abs_error={with_max_abs_error} at sample={with_max_abs_error_idx} (~{:.6}s)",
with_max_abs_error_idx as f64 / output_sample_rate as f64
);
eprintln!(
"without_context max_abs_error={without_max_abs_error} at sample={without_max_abs_error_idx} (~{:.6}s)",
without_max_abs_error_idx as f64 / output_sample_rate as f64
);
let min_ratio = 100.0f32;
let ratio = without_max_abs_error / with_max_abs_error;
assert!(
ratio >= min_ratio,
"expected without_context/with_context ratio >= {min_ratio}, got {ratio} (without={without_max_abs_error}, with={with_max_abs_error})"
);
}
#[test]
fn process_all_resets_state_between_calls() {
let config = mono_config(44_100, 48_000);
let mut reused = InterleavedResampler::new(config.clone()).unwrap();
let mut reference = InterleavedResampler::new(config).unwrap();
let first_input: Vec<f32> = (0..(input_chunk_frames(&reused) * 2 + 11))
.map(|frame| (frame as f32 * 0.009).sin() * 0.2)
.collect();
let second_input: Vec<f32> = (0..(input_chunk_frames(&reused) + 7))
.map(|frame| (frame as f32 * 0.013).cos() * 0.15)
.collect();
let first_reused = process_all_samples(&mut reused, &first_input).unwrap();
let first_reference = process_all_samples(&mut reference, &first_input).unwrap();
assert_eq!(first_reused.len(), first_reference.len());
for (left, right) in first_reused.iter().zip(first_reference.iter()) {
assert!((*left - *right).abs() < 1e-5);
}
let second_reused = process_all_samples(&mut reused, &second_input).unwrap();
let second_reference = process_all_samples(&mut reference, &second_input).unwrap();
assert_eq!(second_reused.len(), second_reference.len());
for (left, right) in second_reused.iter().zip(second_reference.iter()) {
assert!((*left - *right).abs() < 1e-5);
}
}
#[test]
fn batch_gapless_matches_planar_gapless() {
let config = mono_config(44_100, 48_000);
let interleaved = InterleavedResampler::<f32>::new(config.clone()).unwrap();
let planar = PlanarResampler::<f32>::new(config).unwrap();
let context_chunk_size = interleaved.input_buffer_size() / interleaved.config().channels;
let track_frames = context_chunk_size * 2 + 17;
let tracks: Vec<Vec<f32>> = (0..3)
.map(|track_idx| {
(0..track_frames)
.map(|frame| {
let continuous_frame = track_idx * track_frames + frame;
(continuous_frame as f32 * 0.017).sin() * 0.25
})
.collect()
})
.collect();
let planar_tracks = tracks
.iter()
.map(|track| PlanarVecs::new(vec![track.clone()]))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let input_refs = tracks.iter().map(Vec::as_slice).collect::<Vec<_>>();
let interleaved_outputs = interleaved.batch_gapless(&input_refs).unwrap();
let planar_outputs = planar.batch_gapless(planar_tracks).unwrap();
assert_eq!(interleaved_outputs.len(), planar_outputs.len());
for (interleaved_output, planar_output) in interleaved_outputs.iter().zip(planar_outputs.iter()) {
assert_eq!(interleaved_output, planar_output);
}
}
#[test]
fn batch_matches_independent_process_all() {
let config = mono_config(44_100, 48_000);
let batch = InterleavedResampler::<f32>::new(config.clone()).unwrap();
let chunk = batch.input_buffer_size() / batch.config().channels;
let tracks: Vec<Vec<f32>> = vec![
(0..(chunk + 17))
.map(|frame| (frame as f32 * 0.009).sin() * 0.2)
.collect(),
(0..(chunk * 2 + 5))
.map(|frame| (frame as f32 * 0.012).cos() * 0.15)
.collect(),
(0..(chunk / 2 + 11))
.map(|frame| (frame as f32 * 0.021).sin() * 0.25)
.collect(),
];
let input_refs = tracks.iter().map(Vec::as_slice).collect::<Vec<_>>();
let expected: Vec<Vec<f32>> = tracks
.iter()
.map(|track| {
let mut resampler = InterleavedResampler::new(config.clone()).unwrap();
process_all_samples(&mut resampler, track).unwrap()
})
.collect();
let actual = batch.batch(&input_refs).unwrap();
assert_eq!(actual.len(), expected.len());
for (actual_track, expected_track) in actual.iter().zip(expected.iter()) {
let actual_channel = actual_track.get_channel(0).unwrap();
assert_eq!(actual_channel.len(), expected_track.len());
for (left, right) in actual_channel.iter().zip(expected_track.iter()) {
assert!((*left - *right).abs() < 1e-5);
}
}
}
#[test]
fn stereo_channels_are_processed_independently() {
let mut resampler = InterleavedResampler::new(stereo_config(44_100, 48_000)).unwrap();
let mut input = vec![0.0; resampler.input_buffer_size()];
input[0] = 1.0;
let output = process_all_samples(&mut resampler, &input).unwrap();
assert_eq!(
output.len(),
resampler.expected_output_size(input_chunk_frames(&resampler)) * 2
);
assert!(output.iter().all(|sample| sample.is_finite()));
assert!(output.chunks_exact(2).any(|frame| frame[0].abs() > 1e-6));
assert!(output.chunks_exact(2).all(|frame| frame[1].abs() < 1e-6));
}
#[test]
fn stereo_streaming_and_offline_paths_match() {
let config = stereo_config(44_100, 48_000);
let mut offline = InterleavedResampler::new(config.clone()).unwrap();
let mut streaming = InterleavedResampler::new(config).unwrap();
let input_frames = input_chunk_frames(&streaming) * 2 + input_chunk_frames(&streaming) / 3;
let mut input = Vec::with_capacity(input_frames * 2);
for frame in 0..input_frames {
input.push((frame as f32 * 0.01).sin() * 0.25);
input.push((frame as f32 * 0.017).cos() * 0.125);
}
let offline_output = process_all_samples(&mut offline, &input).unwrap();
let input_buffer_size = streaming.input_buffer_size();
let full_chunks = input.len() / input_buffer_size;
let has_partial = !input.len().is_multiple_of(input_buffer_size);
let output_blocks = full_chunks + usize::from(has_partial) + 1;
let mut streaming_output = vec![0.0; output_blocks * streaming.output_buffer_size()];
let mut written = 0;
let mut offset = 0;
while offset + input_buffer_size <= input.len() {
written += process_chunk_samples(
&mut streaming,
&input[offset..offset + input_buffer_size],
&mut streaming_output[written..],
)
.unwrap();
offset += input_buffer_size;
}
written +=
process_chunk_final_samples(&mut streaming, &input[offset..], &mut streaming_output[written..]).unwrap();
written += finalize_samples_chunk(&mut streaming, &mut streaming_output[written..]).unwrap();
streaming_output.truncate(written);
streaming_output.truncate(offline_output.len());
assert_eq!(offline_output.len(), streaming_output.len());
for (left, right) in offline_output.iter().zip(streaming_output) {
assert!((left - right).abs() < 1e-5);
}
}
#[test]
fn rejects_wrong_streaming_chunk_size() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input = vec![0.0; input_chunk_frames(&resampler) - 1];
assert!(process_chunk_samples(&mut resampler, &input, &mut []).is_err());
}
#[test]
fn too_small_output_does_not_advance_stream_state() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input = vec![0.0; input_chunk_frames(&resampler)];
let mut too_small = vec![0.0; output_chunk_frames(&resampler) - 1];
assert!(matches!(
process_chunk_samples(&mut resampler, &input, &mut too_small),
Err(Error::InsufficientOutputBuffer { .. })
));
assert_eq!(resampler.input_sample_processed(), 0);
let mut output = vec![0.0; output_chunk_frames(&resampler)];
let expected = output_chunk_frames(&resampler) - resampler.output_delay_frames();
assert_eq!(
process_chunk_samples(&mut resampler, &input, &mut output).unwrap(),
expected
);
assert_eq!(resampler.input_sample_processed(), input.len());
}
#[test]
fn input_sample_count_tracks_full_and_partial_stream_input() {
let mut resampler = InterleavedResampler::new(stereo_config(44_100, 48_000)).unwrap();
let mut output = vec![0.0; resampler.output_buffer_size()];
let full = vec![0.0; resampler.input_buffer_size()];
let partial = vec![0.0; 10];
assert_eq!(resampler.input_sample_processed(), 0);
let _ = process_chunk_samples(&mut resampler, &full, &mut output).unwrap();
assert_eq!(resampler.input_sample_processed(), full.len());
let _ = process_chunk_final_samples(&mut resampler, &partial, &mut output).unwrap();
assert_eq!(resampler.input_sample_processed(), full.len() + partial.len());
}
#[test]
fn too_small_finish_output_does_not_mark_flushed() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input = vec![0.0; input_chunk_frames(&resampler)];
let mut first_output = vec![0.0; output_chunk_frames(&resampler)];
process_chunk_samples(&mut resampler, &input, &mut first_output).unwrap();
let mut too_small = vec![0.0; resampler.output_delay_frames() - 1];
assert!(matches!(
finalize_samples_chunk(&mut resampler, &mut too_small),
Err(Error::InsufficientOutputBuffer { .. })
));
let mut output = vec![0.0; output_chunk_frames(&resampler)];
assert_eq!(
finalize_samples_chunk(&mut resampler, &mut output).unwrap(),
resampler.output_delay_frames()
);
}
#[test]
fn first_chunk_is_delay_trimmed_and_flushes_tail() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input = vec![0.0; input_chunk_frames(&resampler)];
let mut output = vec![0.0; output_chunk_frames(&resampler)];
let first_written = process_chunk_samples(&mut resampler, &input, &mut output).unwrap();
let flush_written = finalize_samples_chunk(&mut resampler, &mut output).unwrap();
assert_eq!(first_written + flush_written, output_chunk_frames(&resampler));
assert_eq!(
first_written,
output_chunk_frames(&resampler) - resampler.output_delay_frames()
);
assert_eq!(flush_written, resampler.output_delay_frames());
}
#[test]
fn finalize_without_explicit_final_chunk_caps_to_expected_total() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input = vec![0.0; input_chunk_frames(&resampler)];
let input_samples = input.len() * 2;
let expected_total = resampler.expected_output_size(input_samples);
let mut output = vec![0.0; output_chunk_frames(&resampler)];
let first_written = process_chunk_samples(&mut resampler, &input, &mut output).unwrap();
let second_written = process_chunk_samples(&mut resampler, &input, &mut output).unwrap();
let flush_written = finalize_samples_chunk(&mut resampler, &mut output).unwrap();
assert_eq!(first_written + second_written + flush_written, expected_total);
}
#[test]
fn empty_finish_does_not_emit_extra_block() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let input = vec![0.0; input_chunk_frames(&resampler)];
let mut output = vec![0.0; output_chunk_frames(&resampler)];
process_chunk_samples(&mut resampler, &input, &mut output).unwrap();
assert_eq!(
process_chunk_final_samples(&mut resampler, &[], &mut output).unwrap(),
0
);
assert_eq!(
finalize_samples_chunk(&mut resampler, &mut output).unwrap(),
resampler.output_delay_frames()
);
}
#[test]
fn empty_stream_flushes_no_samples() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let mut output = vec![0.0; output_chunk_frames(&resampler)];
assert_eq!(
process_chunk_final_samples(&mut resampler, &[], &mut output).unwrap(),
0
);
assert_eq!(finalize_samples_chunk(&mut resampler, &mut output).unwrap(), 0);
}
#[test]
fn second_finish_after_reset_returns_zero_without_input() {
let mut resampler = InterleavedResampler::new(mono_config(44_100, 48_000)).unwrap();
let mut output = vec![0.0; output_chunk_frames(&resampler)];
assert_eq!(finalize_samples_chunk(&mut resampler, &mut output).unwrap(), 0);
assert!(matches!(
finalize_samples_chunk(&mut resampler, &mut output),
Err(Error::AlreadyFlushed)
));
}
}