use rubato::{FftFixedIn, Resampler};
use crate::audio::sanitize_sample;
const CHUNK_FRAMES: usize = 1024;
const SUB_CHUNKS: usize = 2;
pub fn resample(input: &[f64], from_rate: u32, to_rate: u32) -> Result<Vec<f64>, String> {
let channels = resample_channels(&[input.to_vec()], from_rate, to_rate)?;
Ok(channels.into_iter().next().unwrap_or_default())
}
pub fn resample_channels(
input: &[Vec<f64>],
from_rate: u32,
to_rate: u32,
) -> Result<Vec<Vec<f64>>, String> {
if input.is_empty() {
return Ok(Vec::new());
}
if from_rate == 0 || to_rate == 0 {
return Err("sample rates must be greater than zero".into());
}
let frames = input[0].len();
if input.iter().any(|channel| channel.len() != frames) {
return Err("all channels must contain the same number of frames".into());
}
if frames == 0 {
return Ok(input.to_vec());
}
let needs_sanitization = input
.iter()
.flatten()
.any(|sample| !sample.is_finite() || *sample < -1.0 || *sample > 1.0);
let sanitized;
let input = if needs_sanitization {
sanitized = input
.iter()
.map(|channel| channel.iter().copied().map(sanitize_sample).collect())
.collect::<Vec<Vec<f64>>>();
&sanitized
} else {
input
};
if from_rate == to_rate {
return Ok(input.to_vec());
}
let expected =
((frames as u128 * to_rate as u128 + from_rate as u128 / 2) / from_rate as u128) as usize;
let mut converter = FftFixedIn::<f64>::new(
from_rate as usize,
to_rate as usize,
CHUNK_FRAMES,
SUB_CHUNKS,
input.len(),
)
.map_err(|error| format!("failed to create sample-rate converter: {error}"))?;
let delay = converter.output_delay();
let mut output = vec![Vec::with_capacity(expected + delay + CHUNK_FRAMES); input.len()];
let mut position = 0;
while frames - position >= converter.input_frames_next() {
let count = converter.input_frames_next();
let chunk: Vec<&[f64]> = input
.iter()
.map(|channel| &channel[position..position + count])
.collect();
let converted = converter
.process(&chunk, None)
.map_err(|error| format!("sample-rate conversion failed: {error}"))?;
append(&mut output, &converted);
position += count;
}
if position < frames {
let tail: Vec<&[f64]> = input.iter().map(|channel| &channel[position..]).collect();
let converted = converter
.process_partial(Some(&tail), None)
.map_err(|error| format!("sample-rate conversion failed: {error}"))?;
append(&mut output, &converted);
}
while output.first().map_or(0, Vec::len) < delay + expected {
let converted = converter
.process_partial::<&[f64]>(None, None)
.map_err(|error| format!("sample-rate conversion flush failed: {error}"))?;
append(&mut output, &converted);
}
for channel in &mut output {
channel.drain(..delay.min(channel.len()));
channel.truncate(expected);
channel.resize(expected, 0.0);
for sample in channel {
*sample = sanitize_sample(*sample);
}
}
Ok(output)
}
fn append(output: &mut [Vec<f64>], chunk: &[Vec<f64>]) {
for (output, chunk) in output.iter_mut().zip(chunk) {
output.extend_from_slice(chunk);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::TAU;
#[test]
fn same_rate_is_an_exact_identity() {
let input = vec![0.25, -0.5, 1.0];
assert_eq!(resample(&input, 48_000, 48_000).unwrap(), input);
}
#[test]
fn preserves_requested_duration() {
let input = vec![0.0; 44_100];
assert_eq!(resample(&input, 44_100, 16_000).unwrap().len(), 16_000);
assert_eq!(resample(&input, 44_100, 48_000).unwrap().len(), 48_000);
}
#[test]
fn downsampling_rejects_content_above_nyquist() {
let tone = |frequency: f64| {
(0..48_000)
.map(|i| (TAU * frequency * i as f64 / 48_000.0).sin())
.collect::<Vec<_>>()
};
let passband = resample(&tone(1_000.0), 48_000, 16_000).unwrap();
let stopband = resample(&tone(12_000.0), 48_000, 16_000).unwrap();
let rms = |samples: &[f64]| {
(samples.iter().map(|x| x * x).sum::<f64>() / samples.len() as f64).sqrt()
};
assert!(rms(&stopband) < rms(&passband) * 0.01);
}
#[test]
fn linked_channels_remain_sample_identical() {
let channel: Vec<f64> = (0..4_410)
.map(|i| (TAU * 997.0 * i as f64 / 44_100.0).sin())
.collect();
let output = resample_channels(&[channel.clone(), channel], 44_100, 48_000).unwrap();
assert_eq!(output[0], output[1]);
}
#[test]
fn supports_arbitrary_sample_rate_pairs() {
let rates = [8_000, 12_345, 22_050, 32_000, 44_100, 48_000, 96_000];
let frames = 1_001usize;
let input: Vec<f64> = (0..frames)
.map(|frame| (TAU * 997.0 * frame as f64 / 44_100.0).sin() * 0.5)
.collect();
for &from_rate in &rates {
for &to_rate in &rates {
if from_rate == to_rate {
continue;
}
let output = resample(&input, from_rate, to_rate).unwrap_or_else(|error| {
panic!("{from_rate} Hz -> {to_rate} Hz conversion failed: {error}")
});
let expected = ((frames as u128 * to_rate as u128 + from_rate as u128 / 2)
/ from_rate as u128) as usize;
assert_eq!(
output.len(),
expected,
"{from_rate} Hz -> {to_rate} Hz output length"
);
assert!(
output.iter().all(|sample| sample.is_finite()),
"{from_rate} Hz -> {to_rate} Hz produced a non-finite sample"
);
}
}
}
#[test]
fn sanitizes_nonfinite_and_extreme_samples_before_conversion() {
let input = vec![vec![
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
2.0,
-2.0,
0.25,
]];
let output = resample_channels(&input, 48_000, 48_000).unwrap();
assert_eq!(output[0], vec![0.0, 0.0, 0.0, 1.0, -1.0, 0.25]);
}
}