use crate::error::{IoError, Result};
use scirs2_core::ndarray::{Array2, ArrayD, IxDyn};
#[derive(Debug, Clone)]
pub struct SincConfig {
pub kernel_half_width: usize,
pub window: WindowFunction,
}
impl Default for SincConfig {
fn default() -> Self {
SincConfig {
kernel_half_width: 16,
window: WindowFunction::BlackmanHarris,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowFunction {
Rectangular,
Hann,
Blackman,
BlackmanHarris,
Kaiser(u32), }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResampleQuality {
Low,
Medium,
High,
Ultra,
}
impl ResampleQuality {
pub fn to_sinc_config(self) -> SincConfig {
match self {
ResampleQuality::Low => SincConfig {
kernel_half_width: 4,
window: WindowFunction::Hann,
},
ResampleQuality::Medium => SincConfig {
kernel_half_width: 8,
window: WindowFunction::Blackman,
},
ResampleQuality::High => SincConfig {
kernel_half_width: 16,
window: WindowFunction::BlackmanHarris,
},
ResampleQuality::Ultra => SincConfig {
kernel_half_width: 32,
window: WindowFunction::BlackmanHarris,
},
}
}
}
fn evaluate_window(window: WindowFunction, x: f64, half_width: f64) -> f64 {
if x.abs() > half_width {
return 0.0;
}
let n = (x + half_width) / (2.0 * half_width);
match window {
WindowFunction::Rectangular => 1.0,
WindowFunction::Hann => 0.5 * (1.0 - (2.0 * std::f64::consts::PI * n).cos()),
WindowFunction::Blackman => {
let a0 = 0.42;
let a1 = 0.5;
let a2 = 0.08;
a0 - a1 * (2.0 * std::f64::consts::PI * n).cos()
+ a2 * (4.0 * std::f64::consts::PI * n).cos()
}
WindowFunction::BlackmanHarris => {
let a0 = 0.35875;
let a1 = 0.48829;
let a2 = 0.14128;
let a3 = 0.01168;
a0 - a1 * (2.0 * std::f64::consts::PI * n).cos()
+ a2 * (4.0 * std::f64::consts::PI * n).cos()
- a3 * (6.0 * std::f64::consts::PI * n).cos()
}
WindowFunction::Kaiser(beta_x100) => {
let beta = beta_x100 as f64 / 100.0;
let t = 2.0 * n - 1.0;
let arg = 1.0 - t * t;
if arg < 0.0 {
return 0.0;
}
bessel_i0(beta * arg.sqrt()) / bessel_i0(beta)
}
}
}
fn bessel_i0(x: f64) -> f64 {
let mut sum = 1.0;
let mut term = 1.0;
let half_x = x / 2.0;
for k in 1..50 {
term *= (half_x / k as f64) * (half_x / k as f64);
sum += term;
if term < sum * 1e-16 {
break;
}
}
sum
}
fn windowed_sinc(x: f64, half_width: f64, window: WindowFunction) -> f64 {
if x.abs() < 1e-12 {
return 1.0; }
let sinc_val = (std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x);
let win_val = evaluate_window(window, x, half_width);
sinc_val * win_val
}
pub fn resample_linear(data: &ArrayD<f32>, src_rate: u32, dst_rate: u32) -> Result<ArrayD<f32>> {
if src_rate == 0 || dst_rate == 0 {
return Err(IoError::ConversionError(
"Sample rate must be positive".to_string(),
));
}
if src_rate == dst_rate {
return Ok(data.clone());
}
if data.ndim() < 2 {
return Err(IoError::FormatError(
"Audio data must be 2D [channels, samples]".to_string(),
));
}
let channels = data.shape()[0];
let src_samples = data.shape()[1];
if src_samples == 0 {
return Ok(data.clone());
}
let ratio = dst_rate as f64 / src_rate as f64;
let dst_samples = ((src_samples as f64) * ratio).ceil() as usize;
let mut output = Array2::zeros((channels, dst_samples));
for ch in 0..channels {
for i in 0..dst_samples {
let src_pos = i as f64 / ratio;
let src_idx = src_pos.floor() as usize;
let frac = src_pos - src_idx as f64;
if src_idx + 1 < src_samples {
let s0 = data[[ch, src_idx]];
let s1 = data[[ch, src_idx + 1]];
output[[ch, i]] = s0 + (s1 - s0) * frac as f32;
} else if src_idx < src_samples {
output[[ch, i]] = data[[ch, src_idx]];
}
}
}
Ok(output.into_dyn())
}
pub fn resample_sinc(
data: &ArrayD<f32>,
src_rate: u32,
dst_rate: u32,
config: SincConfig,
) -> Result<ArrayD<f32>> {
if src_rate == 0 || dst_rate == 0 {
return Err(IoError::ConversionError(
"Sample rate must be positive".to_string(),
));
}
if src_rate == dst_rate {
return Ok(data.clone());
}
if data.ndim() < 2 {
return Err(IoError::FormatError(
"Audio data must be 2D [channels, samples]".to_string(),
));
}
let channels = data.shape()[0];
let src_samples = data.shape()[1];
if src_samples == 0 {
return Ok(data.clone());
}
let ratio = dst_rate as f64 / src_rate as f64;
let dst_samples = ((src_samples as f64) * ratio).ceil() as usize;
let cutoff = if ratio < 1.0 { ratio } else { 1.0 };
let half_width = config.kernel_half_width as f64;
let effective_half_width = if ratio < 1.0 {
half_width / cutoff
} else {
half_width
};
let mut output = Array2::zeros((channels, dst_samples));
for ch in 0..channels {
for i in 0..dst_samples {
let src_pos = i as f64 / ratio;
let src_center = src_pos.floor() as i64;
let lo = (src_center - effective_half_width.ceil() as i64).max(0) as usize;
let hi = (src_center + effective_half_width.ceil() as i64 + 1).min(src_samples as i64)
as usize;
let mut sum = 0.0f64;
let mut weight_sum = 0.0f64;
for j in lo..hi {
let delta = (j as f64 - src_pos) * cutoff;
let w = windowed_sinc(delta, half_width, config.window);
sum += data[[ch, j]] as f64 * w;
weight_sum += w;
}
if weight_sum.abs() > 1e-12 {
output[[ch, i]] = (sum / weight_sum) as f32;
}
}
}
Ok(output.into_dyn())
}
pub fn resample(
data: &ArrayD<f32>,
src_rate: u32,
dst_rate: u32,
quality: ResampleQuality,
) -> Result<ArrayD<f32>> {
if quality == ResampleQuality::Low {
resample_linear(data, src_rate, dst_rate)
} else {
resample_sinc(data, src_rate, dst_rate, quality.to_sinc_config())
}
}
pub fn resampled_length(src_samples: usize, src_rate: u32, dst_rate: u32) -> usize {
if src_rate == 0 || dst_rate == 0 {
return 0;
}
let ratio = dst_rate as f64 / src_rate as f64;
((src_samples as f64) * ratio).ceil() as usize
}
pub fn sample_rate_ratio(src_rate: u32, dst_rate: u32) -> (u32, u32) {
let g = gcd(src_rate, dst_rate);
(dst_rate / g, src_rate / g)
}
fn gcd(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn sine_wave(freq: f64, sample_rate: u32, duration_secs: f64) -> ArrayD<f32> {
let n = (sample_rate as f64 * duration_secs) as usize;
let mut data = Array2::zeros((1, n));
for i in 0..n {
let t = i as f64 / sample_rate as f64;
data[[0, i]] = (2.0 * std::f64::consts::PI * freq * t).sin() as f32;
}
data.into_dyn()
}
#[test]
fn test_resample_linear_identity() {
let data = sine_wave(440.0, 44100, 0.1);
let result = resample_linear(&data, 44100, 44100).expect("resample failed");
assert_eq!(result.shape(), data.shape());
let orig = data.as_slice().expect("not contiguous");
let res = result.as_slice().expect("not contiguous");
for (a, b) in orig.iter().zip(res.iter()) {
assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn test_resample_linear_upsample() {
let data = sine_wave(440.0, 22050, 0.05);
let result = resample_linear(&data, 22050, 44100).expect("resample failed");
let expected_len = resampled_length(data.shape()[1], 22050, 44100);
assert_eq!(result.shape()[1], expected_len);
assert_eq!(result.shape()[0], 1);
}
#[test]
fn test_resample_linear_downsample() {
let data = sine_wave(440.0, 44100, 0.05);
let result = resample_linear(&data, 44100, 22050).expect("resample failed");
let expected_len = resampled_length(data.shape()[1], 44100, 22050);
assert_eq!(result.shape()[1], expected_len);
}
#[test]
fn test_resample_sinc_identity() {
let data = sine_wave(440.0, 44100, 0.1);
let result =
resample_sinc(&data, 44100, 44100, SincConfig::default()).expect("resample failed");
assert_eq!(result.shape(), data.shape());
}
#[test]
fn test_resample_sinc_upsample_quality() {
let data = sine_wave(1000.0, 8000, 0.05);
let result =
resample_sinc(&data, 8000, 48000, SincConfig::default()).expect("resample failed");
let dst_samples = result.shape()[1];
let period_samples = 48000.0 / 1000.0;
let mid = dst_samples / 2;
let max_val = result
.as_slice()
.expect("not contiguous")
.iter()
.skip(mid.saturating_sub(48))
.take(96)
.fold(0.0f32, |a, &b| a.max(b.abs()));
assert!(
max_val > 0.8,
"Upsampled signal amplitude too low: {}",
max_val
);
assert!(
max_val < 1.2,
"Upsampled signal amplitude too high: {}",
max_val
);
let _ = period_samples; }
#[test]
fn test_resample_sinc_downsample() {
let data = sine_wave(440.0, 48000, 0.05);
let result =
resample_sinc(&data, 48000, 16000, SincConfig::default()).expect("resample failed");
let expected_len = resampled_length(data.shape()[1], 48000, 16000);
assert_eq!(result.shape()[1], expected_len);
}
#[test]
fn test_resample_stereo() {
let n = 1000;
let mut data = Array2::zeros((2, n));
for i in 0..n {
let t = i as f64 / 44100.0;
data[[0, i]] = (2.0 * std::f64::consts::PI * 440.0 * t).sin() as f32;
data[[1, i]] = (2.0 * std::f64::consts::PI * 880.0 * t).sin() as f32;
}
let dyn_data = data.into_dyn();
let result =
resample_sinc(&dyn_data, 44100, 22050, SincConfig::default()).expect("resample failed");
assert_eq!(result.shape()[0], 2);
let expected_len = resampled_length(n, 44100, 22050);
assert_eq!(result.shape()[1], expected_len);
}
#[test]
fn test_resample_quality_presets() {
let data = sine_wave(440.0, 44100, 0.02);
for quality in [
ResampleQuality::Low,
ResampleQuality::Medium,
ResampleQuality::High,
ResampleQuality::Ultra,
] {
let result = resample(&data, 44100, 22050, quality).expect("resample failed");
let expected_len = resampled_length(data.shape()[1], 44100, 22050);
assert_eq!(result.shape()[1], expected_len);
}
}
#[test]
fn test_resample_zero_rate_error() {
let data = sine_wave(440.0, 44100, 0.01);
assert!(resample_linear(&data, 0, 44100).is_err());
assert!(resample_linear(&data, 44100, 0).is_err());
}
#[test]
fn test_resample_1d_error() {
let data = scirs2_core::ndarray::arr1(&[1.0f32, 2.0, 3.0]).into_dyn();
assert!(resample_linear(&data, 44100, 22050).is_err());
}
#[test]
fn test_sample_rate_ratio() {
let (p, q) = sample_rate_ratio(44100, 48000);
assert_eq!(p, 160);
assert_eq!(q, 147);
let (p, q) = sample_rate_ratio(44100, 22050);
assert_eq!(p, 1);
assert_eq!(q, 2);
let (p, q) = sample_rate_ratio(8000, 48000);
assert_eq!(p, 6);
assert_eq!(q, 1);
}
#[test]
fn test_resampled_length() {
assert_eq!(resampled_length(44100, 44100, 22050), 22050);
assert_eq!(resampled_length(44100, 44100, 88200), 88200);
assert_eq!(resampled_length(0, 44100, 22050), 0);
assert_eq!(resampled_length(100, 0, 22050), 0);
}
#[test]
fn test_window_functions() {
let hw = 16.0;
for window in [
WindowFunction::Rectangular,
WindowFunction::Hann,
WindowFunction::Blackman,
WindowFunction::BlackmanHarris,
WindowFunction::Kaiser(800), ] {
let center = evaluate_window(window, 0.0, hw);
if window != WindowFunction::Rectangular {
assert!(center > 0.3, "Window center too low: {}", center);
}
let outside = evaluate_window(window, hw + 1.0, hw);
assert!(
outside.abs() < 1e-10,
"Window outside not zero: {}",
outside
);
}
}
#[test]
fn test_bessel_i0() {
assert!((bessel_i0(0.0) - 1.0).abs() < 1e-12);
assert!((bessel_i0(1.0) - 1.2660658).abs() < 1e-4);
assert!((bessel_i0(5.0) - 27.2398718).abs() < 1e-3);
}
#[test]
fn test_windowed_sinc_at_zero() {
let val = windowed_sinc(0.0, 16.0, WindowFunction::BlackmanHarris);
assert!((val - 1.0).abs() < 1e-12);
}
#[test]
fn test_windowed_sinc_at_integers() {
for n in 1..10 {
let val = windowed_sinc(n as f64, 16.0, WindowFunction::BlackmanHarris);
assert!(val.abs() < 1e-10, "sinc({}) = {} (expected 0)", n, val);
}
}
#[test]
fn test_empty_data_resample() {
let data = Array2::<f32>::zeros((1, 0)).into_dyn();
let result = resample_linear(&data, 44100, 22050).expect("resample failed");
assert_eq!(result.shape()[1], 0);
}
#[test]
fn test_resample_preserves_dc_offset() {
let n = 1000;
let dc_level = 0.5f32;
let mut data = Array2::zeros((1, n));
for i in 0..n {
data[[0, i]] = dc_level;
}
let dyn_data = data.into_dyn();
let result =
resample_sinc(&dyn_data, 44100, 22050, SincConfig::default()).expect("resample failed");
let out = result.as_slice().expect("not contiguous");
let skip = 20; for &v in out
.iter()
.skip(skip)
.take(out.len().saturating_sub(2 * skip))
{
assert!(
(v - dc_level).abs() < 0.05,
"DC not preserved: {} (expected {})",
v,
dc_level
);
}
}
}