const REJECTION_DB: f64 = 60.0;
const KAISER_BETA: f64 = 0.1102 * (REJECTION_DB - 8.7);
const MAX_FILTER_TAPS: usize = 10_000_001;
pub(crate) fn resample(input: &[f64], from_rate: u32, to_rate: u32) -> Option<Vec<f64>> {
if from_rate == 0 || to_rate == 0 {
return None;
}
if from_rate == to_rate {
return Some(input.to_vec());
}
if input.is_empty() {
return Some(Vec::new());
}
let divisor = gcd(from_rate, to_rate);
let up = usize::try_from(to_rate / divisor).ok()?;
let down = usize::try_from(from_rate / divisor).ok()?;
let mut filter = octave_window(up, down)?;
let half_len = (filter.len() - 1) / 2;
for coefficient in &mut filter {
*coefficient *= up as f64;
}
let scaled_len = input.len().checked_mul(up)?;
let output_len = scaled_len / down + usize::from(scaled_len % down != 0);
let mut output = Vec::new();
output.try_reserve_exact(output_len).ok()?;
let pre_pad = down.checked_sub(half_len % down)?;
let pre_remove = half_len.checked_add(pre_pad)?.checked_div(down)?;
let last_filter_index = pre_pad.checked_add(filter.len() - 1)?;
for output_index in 0..output_len {
let raw_index = pre_remove.checked_add(output_index)?;
let time = raw_index.checked_mul(down)?;
if time < pre_pad {
output.push(0.0);
continue;
}
let first_input = if time > last_filter_index {
ceil_div(time - last_filter_index, up)?
} else {
0
};
let last_input = ((time - pre_pad) / up).min(input.len() - 1);
let mut value = 0.0;
if first_input <= last_input {
for input_index in first_input..=last_input {
let filter_index = time - input_index * up - pre_pad;
value += input[input_index] * filter[filter_index];
}
}
output.push(value);
}
Some(output)
}
fn octave_window(up: usize, down: usize) -> Option<Vec<f64>> {
let max_rate = up.max(down) as f64;
let stopband_cutoff = 1.0 / (2.0 * max_rate);
let roll_off_width = stopband_cutoff / 10.0;
let half_len_f64 = ((REJECTION_DB - 8.0) / (28.714 * roll_off_width)).ceil();
if !half_len_f64.is_finite() || half_len_f64 < 1.0 || half_len_f64 > usize::MAX as f64 {
return None;
}
let half_len = half_len_f64 as usize;
let filter_len = half_len.checked_mul(2)?.checked_add(1)?;
if filter_len > MAX_FILTER_TAPS {
return None;
}
let mut window = Vec::new();
window.try_reserve_exact(filter_len).ok()?;
let beta_denominator = modified_bessel_i0(KAISER_BETA);
let ideal_scale = 2.0 * up as f64 * stopband_cutoff;
for index in 0..filter_len {
let t = index as f64 - half_len as f64;
let sinc_argument = 2.0 * stopband_cutoff * t;
let sinc = if sinc_argument == 0.0 {
1.0
} else {
let angle = std::f64::consts::PI * sinc_argument;
angle.sin() / angle
};
let relative_position = t / half_len as f64;
let kaiser_argument = KAISER_BETA * (1.0 - relative_position.powi(2)).max(0.0).sqrt();
let kaiser = modified_bessel_i0(kaiser_argument) / beta_denominator;
window.push(kaiser * ideal_scale * sinc);
}
let sum = window.iter().sum::<f64>();
if !sum.is_finite() || sum == 0.0 {
return None;
}
for coefficient in &mut window {
*coefficient /= sum;
}
Some(window)
}
fn modified_bessel_i0(value: f64) -> f64 {
let squared_quarter = value * value * 0.25;
let mut sum = 1.0;
let mut term = 1.0;
for order in 1..=64 {
let order = order as f64;
term *= squared_quarter / (order * order);
let next = sum + term;
if next == sum {
break;
}
sum = next;
}
sum
}
fn ceil_div(numerator: usize, denominator: usize) -> Option<usize> {
let quotient = numerator.checked_div(denominator)?;
quotient.checked_add(usize::from(numerator % denominator != 0))
}
fn gcd(mut left: u32, mut right: u32) -> u32 {
while right != 0 {
let remainder = left % right;
left = right;
right = remainder;
}
left
}
#[cfg(test)]
mod tests {
use super::resample;
const FIXTURE: [f64; 16] = [
0.25, -0.5, 0.75, -1.0, 0.5, 0.125, -0.25, 0.0, 0.375, -0.625, 0.875, -0.125, 0.25, -0.75,
0.5, 0.1,
];
fn assert_close(actual: &[f64], expected: &[f64]) {
assert_eq!(actual.len(), expected.len());
for (index, (&actual, &expected)) in actual.iter().zip(expected).enumerate() {
let error = (actual - expected).abs();
assert!(
error <= 1.0e-15,
"sample {index}: expected {expected:.17e}, got {actual:.17e}, error {error:.3e}"
);
}
}
#[test]
fn equal_rates_clone_exactly() {
let input = [0.0, -0.0, f64::INFINITY, f64::NEG_INFINITY, f64::NAN];
let output = resample(&input, 10_000, 10_000).expect("valid rates");
assert_eq!(output.len(), input.len());
for (&actual, &expected) in output.iter().zip(&input) {
assert_eq!(actual.to_bits(), expected.to_bits());
}
}
#[test]
fn zero_rate_is_rejected() {
assert_eq!(resample(&FIXTURE, 0, 10_000), None);
assert_eq!(resample(&FIXTURE, 16_000, 0), None);
assert_eq!(resample(&FIXTURE, 0, 0), None);
}
#[test]
fn empty_input_stays_empty() {
assert_eq!(resample(&[], 16_000, 10_000), Some(Vec::new()));
}
#[test]
fn output_length_is_ceil_of_rate_ratio() {
for input_len in 1..=33 {
let input = vec![0.0; input_len];
for &(from_rate, to_rate) in &[(16_000, 10_000), (48_000, 10_000), (10_000, 44_100)] {
let expected = (input_len * to_rate as usize).div_ceil(from_rate as usize);
assert_eq!(
resample(&input, from_rate, to_rate)
.expect("valid conversion")
.len(),
expected
);
}
}
}
#[test]
fn matches_pystoi_0_4_1_from_16_khz() {
let expected = [
0.04027566031699189,
-0.02047008354880606,
-0.14447095456229669,
0.13196267009841994,
-0.06199107929389283,
0.01957429945466374,
0.11501789453329479,
0.30188943296067816,
-0.39557252770425644,
0.3120579915145172,
];
assert_close(
&resample(&FIXTURE, 16_000, 10_000).expect("valid conversion"),
&expected,
);
}
#[test]
fn matches_pystoi_0_4_1_from_48_khz() {
let expected = [
-0.005164767708441263,
-0.01382896956013285,
0.07671091822375789,
0.025623339367896514,
];
assert_close(
&resample(&FIXTURE, 48_000, 10_000).expect("valid conversion"),
&expected,
);
}
}