#[cfg(test)]
mod tests;
use crate::error::{WhisperError, WhisperResult};
use std::f64::consts::PI;
const DEFAULT_KERNEL_HALF_LEN: usize = 16;
const DEFAULT_KAISER_BETA: f64 = 6.0;
#[derive(Debug, Clone)]
pub struct SincResampler {
source_rate: u32,
target_rate: u32,
ratio: f64,
kernel_half_len: usize,
kaiser_beta: f64,
#[allow(dead_code)]
kernel: Option<Vec<f32>>,
}
impl SincResampler {
pub fn new(source_rate: u32, target_rate: u32) -> WhisperResult<Self> {
Self::with_params(
source_rate,
target_rate,
DEFAULT_KERNEL_HALF_LEN,
DEFAULT_KAISER_BETA,
)
}
pub fn with_params(
source_rate: u32,
target_rate: u32,
kernel_half_len: usize,
kaiser_beta: f64,
) -> WhisperResult<Self> {
if source_rate == 0 || target_rate == 0 {
return Err(WhisperError::Audio("sample rate must be non-zero".into()));
}
if kernel_half_len == 0 {
return Err(WhisperError::Audio(
"kernel half-length must be non-zero".into(),
));
}
let ratio = f64::from(target_rate) / f64::from(source_rate);
Ok(Self {
source_rate,
target_rate,
ratio,
kernel_half_len,
kaiser_beta,
kernel: None,
})
}
pub fn resample(&self, audio: &[f32]) -> WhisperResult<Vec<f32>> {
if audio.is_empty() {
return Err(WhisperError::Audio("cannot resample empty audio".into()));
}
if self.source_rate == self.target_rate {
return Ok(audio.to_vec());
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let output_len = ((audio.len() as f64) * self.ratio).ceil() as usize;
if output_len == 0 {
return Err(WhisperError::Audio("output length would be zero".into()));
}
let mut output = vec![0.0_f32; output_len];
let cutoff = if self.ratio < 1.0 { self.ratio } else { 1.0 };
for (out_idx, out_sample) in output.iter_mut().enumerate() {
let in_pos = out_idx as f64 / self.ratio;
let mut sum = 0.0_f64;
let mut weight_sum = 0.0_f64;
#[allow(clippy::cast_possible_truncation)]
let center = in_pos.floor() as i64;
let frac = in_pos - in_pos.floor();
let half_len = self.kernel_half_len as i64;
for k in -half_len..=half_len {
let idx = center + k;
if idx < 0 || idx >= audio.len() as i64 {
continue;
}
let x = k as f64 - frac;
let sinc_val = self.windowed_sinc(x, cutoff);
#[allow(clippy::cast_sign_loss)]
let sample = audio[idx as usize] as f64;
sum += sample * sinc_val;
weight_sum += sinc_val;
}
#[allow(clippy::cast_possible_truncation)]
if weight_sum.abs() > 1e-10 {
*out_sample = (sum / weight_sum) as f32;
}
}
debug_assert_eq!(
output.len(),
output_len,
"resampled output length must match calculated"
);
debug_assert!(
output.iter().all(|x| x.is_finite()),
"all resampled values must be finite"
);
Ok(output)
}
fn windowed_sinc(&self, x: f64, cutoff: f64) -> f64 {
let sinc_arg = cutoff * x;
let sinc_val = if sinc_arg.abs() < 1e-10 {
1.0
} else {
(PI * sinc_arg).sin() / (PI * sinc_arg)
};
let window_arg = x / self.kernel_half_len as f64;
let window_val = if window_arg.abs() > 1.0 {
0.0
} else {
self.kaiser_window(window_arg)
};
sinc_val * window_val
}
fn kaiser_window(&self, x: f64) -> f64 {
let arg = self.kaiser_beta * x.mul_add(-x, 1.0).max(0.0).sqrt();
bessel_i0(arg) / bessel_i0(self.kaiser_beta)
}
#[must_use]
pub const fn source_rate(&self) -> u32 {
self.source_rate
}
#[must_use]
pub const fn target_rate(&self) -> u32 {
self.target_rate
}
#[must_use]
pub fn ratio(&self) -> f64 {
self.ratio
}
#[must_use]
pub const fn kernel_half_len(&self) -> usize {
self.kernel_half_len
}
}
fn bessel_i0(x: f64) -> f64 {
let mut sum = 1.0;
let mut term = 1.0;
let x_sq_over_4 = (x * x) / 4.0;
for k in 1..50 {
term *= x_sq_over_4 / (k * k) as f64;
sum += term;
if term.abs() < 1e-15 * sum.abs() {
break;
}
}
sum
}
pub type Resampler = SincResampler;