use serde::{Deserialize, Serialize};
use crate::error::{Result, ShravanError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ResampleQuality {
Draft,
Good,
Best,
}
impl ResampleQuality {
#[must_use]
#[inline]
fn kernel_half_width(self) -> usize {
match self {
Self::Draft => 2,
Self::Good => 8,
Self::Best => 32,
}
}
}
#[must_use = "resampled audio data is returned and should not be discarded"]
pub fn resample(
samples: &[f32],
channels: u16,
source_rate: u32,
target_rate: u32,
quality: ResampleQuality,
) -> Result<Vec<f32>> {
if source_rate == 0 {
return Err(ShravanError::InvalidSampleRate(0));
}
if target_rate == 0 {
return Err(ShravanError::InvalidSampleRate(0));
}
if channels == 0 {
return Err(ShravanError::InvalidChannels(0));
}
if target_rate == source_rate {
return Ok(samples.to_vec());
}
let ch = channels as usize;
let frames = samples.len() / ch;
if frames == 0 {
return Ok(Vec::new());
}
if ch > 1 {
let channel_bufs: Vec<Vec<f32>> = (0..ch)
.map(|c| {
let mut buf = Vec::with_capacity(frames);
for f in 0..frames {
buf.push(samples[f * ch + c]);
}
buf
})
.collect();
let mut out_channels: Vec<Vec<f32>> = Vec::with_capacity(ch);
for buf in &channel_bufs {
out_channels.push(resample_mono(buf, source_rate, target_rate, quality)?);
}
let new_frames = out_channels[0].len();
let mut out = Vec::with_capacity(new_frames * ch);
for f in 0..new_frames {
for c_buf in &out_channels {
out.push(if f < c_buf.len() { c_buf[f] } else { 0.0 });
}
}
return Ok(out);
}
resample_mono(samples, source_rate, target_rate, quality)
}
#[inline]
fn resample_mono(
samples: &[f32],
source_rate: u32,
target_rate: u32,
quality: ResampleQuality,
) -> Result<Vec<f32>> {
let frames = samples.len();
if frames == 0 {
return Ok(Vec::new());
}
let ratio = target_rate as f64 / source_rate as f64;
let new_frames = (frames as f64 * ratio).ceil() as usize;
let half_width = quality.kernel_half_width();
let (filter_scale, kernel_scale) = if ratio < 1.0 {
(ratio, ratio)
} else {
(1.0, 1.0)
};
let mut out = vec![0.0f32; new_frames];
#[cfg(feature = "simd")]
let max_kernel_len = ((half_width as f64 / filter_scale).ceil() as usize) * 2 + 1;
#[cfg(feature = "simd")]
let mut kernel_samples = Vec::with_capacity(max_kernel_len);
#[cfg(feature = "simd")]
let mut kernel_weights = Vec::with_capacity(max_kernel_len);
#[allow(clippy::needless_range_loop)]
for frame in 0..new_frames {
let src_pos = frame as f64 / ratio;
let src_center = src_pos.floor() as i64;
let frac = src_pos - src_center as f64;
let scaled_half = (half_width as f64 / filter_scale).ceil() as i64;
#[cfg(feature = "simd")]
{
kernel_samples.clear();
kernel_weights.clear();
for i in -scaled_half..=scaled_half {
let src_idx = src_center + i;
if src_idx < 0 || src_idx >= frames as i64 {
continue;
}
let x = (i as f64 - frac) * kernel_scale;
let w = windowed_sinc(x, scaled_half as f64) as f32;
kernel_samples.push(samples[src_idx as usize]);
kernel_weights.push(w);
}
let (sum, weight_sum) = crate::simd::weighted_sum(&kernel_samples, &kernel_weights);
if weight_sum.abs() > 1e-7 {
out[frame] = sum / weight_sum;
}
}
#[cfg(not(feature = "simd"))]
{
let mut sum = 0.0f64;
let mut weight_sum = 0.0f64;
for i in -scaled_half..=scaled_half {
let src_idx = src_center + i;
if src_idx < 0 || src_idx >= frames as i64 {
continue;
}
let x = (i as f64 - frac) * kernel_scale;
let w = windowed_sinc(x, scaled_half as f64);
let sample = samples[src_idx as usize] as f64;
sum += sample * w;
weight_sum += w;
}
if weight_sum.abs() > 1e-10 {
out[frame] = (sum / weight_sum) as f32;
}
}
}
Ok(out)
}
fn windowed_sinc(x: f64, half_width: f64) -> f64 {
if x.abs() < 1e-10 {
return 1.0;
}
let sinc = libm::sin(core::f64::consts::PI * x) / (core::f64::consts::PI * x);
if x.abs() > half_width {
return 0.0;
}
let t = (x / half_width + 1.0) * 0.5; let tau = core::f64::consts::TAU;
let window = 0.35875 - 0.48829 * libm::cos(tau * t) + 0.14128 * libm::cos(2.0 * tau * t)
- 0.01168 * libm::cos(3.0 * tau * t);
sinc * window
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn same_rate_identity() {
let samples: Vec<f32> = vec![0.5, -0.5, 0.3, -0.3];
let out = resample(&samples, 1, 44100, 44100, ResampleQuality::Good).unwrap();
assert_eq!(out.len(), samples.len());
assert_eq!(out, samples);
}
#[test]
fn zero_rate_rejected() {
let samples = vec![0.0f32; 100];
assert!(resample(&samples, 1, 44100, 0, ResampleQuality::Draft).is_err());
}
#[test]
fn zero_channels_rejected() {
let samples = vec![0.0f32; 100];
assert!(resample(&samples, 0, 44100, 48000, ResampleQuality::Draft).is_err());
}
#[test]
fn empty_input() {
let out = resample(&[], 2, 44100, 48000, ResampleQuality::Good).unwrap();
assert!(out.is_empty());
}
#[test]
fn upsample_increases_length() {
let samples = vec![0.0f32; 1000];
let out = resample(&samples, 1, 44100, 96000, ResampleQuality::Draft).unwrap();
assert!(out.len() > 1000);
}
#[test]
fn downsample_decreases_length() {
let samples = vec![0.0f32; 1000];
let out = resample(&samples, 1, 96000, 44100, ResampleQuality::Draft).unwrap();
assert!(out.len() < 1000);
}
#[test]
fn roundtrip_preserves_signal() {
let sr = 44100u32;
let frames = 4096;
let samples: Vec<f32> = (0..frames)
.map(|i| libm::sinf(2.0 * core::f32::consts::PI * 440.0 * i as f32 / sr as f32))
.collect();
let rms_orig = rms(&samples);
let up = resample(&samples, 1, 44100, 48000, ResampleQuality::Good).unwrap();
let back = resample(&up, 1, 48000, 44100, ResampleQuality::Good).unwrap();
let rms_back = rms(&back);
assert!(
(rms_back - rms_orig).abs() < rms_orig * 0.1,
"Round-trip RMS: {rms_back} vs original: {rms_orig}"
);
}
#[test]
fn quality_levels_all_work() {
let samples: Vec<f32> = (0..1024)
.map(|i| libm::sinf(2.0 * core::f32::consts::PI * 440.0 * i as f32 / 44100.0))
.collect();
for quality in [
ResampleQuality::Draft,
ResampleQuality::Good,
ResampleQuality::Best,
] {
let out = resample(&samples, 1, 44100, 48000, quality).unwrap();
assert!(!out.is_empty());
assert!(out.iter().all(|s| s.is_finite()));
}
}
#[test]
fn stereo_resample_roundtrip() {
let sr = 44100u32;
let frames = 2048;
let mut samples = Vec::with_capacity(frames * 2);
for i in 0..frames {
let t = i as f32 / sr as f32;
samples.push(libm::sinf(2.0 * core::f32::consts::PI * 440.0 * t));
samples.push(libm::sinf(2.0 * core::f32::consts::PI * 880.0 * t));
}
let up = resample(&samples, 2, 44100, 48000, ResampleQuality::Good).unwrap();
assert_eq!(up.len() % 2, 0); let back = resample(&up, 2, 48000, 44100, ResampleQuality::Good).unwrap();
let rms_orig = rms(&samples);
let rms_back = rms(&back);
assert!(
(rms_back - rms_orig).abs() < rms_orig * 0.15,
"Stereo roundtrip RMS: {rms_back} vs original: {rms_orig}"
);
}
#[test]
fn multichannel_4ch() {
let frames = 1024;
let ch = 4;
let samples: Vec<f32> = (0..frames * ch)
.map(|i| libm::sinf(i as f32 * 0.1))
.collect();
let out = resample(&samples, ch as u16, 44100, 48000, ResampleQuality::Draft).unwrap();
assert_eq!(out.len() % ch, 0);
assert!(out.iter().all(|s| s.is_finite()));
}
fn rms(samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let sum: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
libm::sqrt(sum / samples.len() as f64) as f32
}
}