1use crate::error::{IoError, Result};
26use scirs2_core::ndarray::{Array2, ArrayD, IxDyn};
27
28#[derive(Debug, Clone)]
34pub struct SincConfig {
35 pub kernel_half_width: usize,
39 pub window: WindowFunction,
42}
43
44impl Default for SincConfig {
45 fn default() -> Self {
46 SincConfig {
47 kernel_half_width: 16,
48 window: WindowFunction::BlackmanHarris,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum WindowFunction {
56 Rectangular,
58 Hann,
60 Blackman,
62 BlackmanHarris,
64 Kaiser(u32), }
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum ResampleQuality {
71 Low,
73 Medium,
75 High,
77 Ultra,
79}
80
81impl ResampleQuality {
82 pub fn to_sinc_config(self) -> SincConfig {
84 match self {
85 ResampleQuality::Low => SincConfig {
86 kernel_half_width: 4,
87 window: WindowFunction::Hann,
88 },
89 ResampleQuality::Medium => SincConfig {
90 kernel_half_width: 8,
91 window: WindowFunction::Blackman,
92 },
93 ResampleQuality::High => SincConfig {
94 kernel_half_width: 16,
95 window: WindowFunction::BlackmanHarris,
96 },
97 ResampleQuality::Ultra => SincConfig {
98 kernel_half_width: 32,
99 window: WindowFunction::BlackmanHarris,
100 },
101 }
102 }
103}
104
105fn evaluate_window(window: WindowFunction, x: f64, half_width: f64) -> f64 {
110 if x.abs() > half_width {
111 return 0.0;
112 }
113
114 let n = (x + half_width) / (2.0 * half_width); match window {
117 WindowFunction::Rectangular => 1.0,
118 WindowFunction::Hann => 0.5 * (1.0 - (2.0 * std::f64::consts::PI * n).cos()),
119 WindowFunction::Blackman => {
120 let a0 = 0.42;
121 let a1 = 0.5;
122 let a2 = 0.08;
123 a0 - a1 * (2.0 * std::f64::consts::PI * n).cos()
124 + a2 * (4.0 * std::f64::consts::PI * n).cos()
125 }
126 WindowFunction::BlackmanHarris => {
127 let a0 = 0.35875;
128 let a1 = 0.48829;
129 let a2 = 0.14128;
130 let a3 = 0.01168;
131 a0 - a1 * (2.0 * std::f64::consts::PI * n).cos()
132 + a2 * (4.0 * std::f64::consts::PI * n).cos()
133 - a3 * (6.0 * std::f64::consts::PI * n).cos()
134 }
135 WindowFunction::Kaiser(beta_x100) => {
136 let beta = beta_x100 as f64 / 100.0;
137 let t = 2.0 * n - 1.0;
139 let arg = 1.0 - t * t;
140 if arg < 0.0 {
141 return 0.0;
142 }
143 bessel_i0(beta * arg.sqrt()) / bessel_i0(beta)
144 }
145 }
146}
147
148fn bessel_i0(x: f64) -> f64 {
151 let mut sum = 1.0;
152 let mut term = 1.0;
153 let half_x = x / 2.0;
154
155 for k in 1..50 {
156 term *= (half_x / k as f64) * (half_x / k as f64);
157 sum += term;
158 if term < sum * 1e-16 {
159 break;
160 }
161 }
162 sum
163}
164
165fn windowed_sinc(x: f64, half_width: f64, window: WindowFunction) -> f64 {
171 if x.abs() < 1e-12 {
172 return 1.0; }
174 let sinc_val = (std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x);
175 let win_val = evaluate_window(window, x, half_width);
176 sinc_val * win_val
177}
178
179pub fn resample_linear(data: &ArrayD<f32>, src_rate: u32, dst_rate: u32) -> Result<ArrayD<f32>> {
198 if src_rate == 0 || dst_rate == 0 {
199 return Err(IoError::ConversionError(
200 "Sample rate must be positive".to_string(),
201 ));
202 }
203 if src_rate == dst_rate {
204 return Ok(data.clone());
205 }
206 if data.ndim() < 2 {
207 return Err(IoError::FormatError(
208 "Audio data must be 2D [channels, samples]".to_string(),
209 ));
210 }
211
212 let channels = data.shape()[0];
213 let src_samples = data.shape()[1];
214
215 if src_samples == 0 {
216 return Ok(data.clone());
217 }
218
219 let ratio = dst_rate as f64 / src_rate as f64;
220 let dst_samples = ((src_samples as f64) * ratio).ceil() as usize;
221
222 let mut output = Array2::zeros((channels, dst_samples));
223
224 for ch in 0..channels {
225 for i in 0..dst_samples {
226 let src_pos = i as f64 / ratio;
227 let src_idx = src_pos.floor() as usize;
228 let frac = src_pos - src_idx as f64;
229
230 if src_idx + 1 < src_samples {
231 let s0 = data[[ch, src_idx]];
232 let s1 = data[[ch, src_idx + 1]];
233 output[[ch, i]] = s0 + (s1 - s0) * frac as f32;
234 } else if src_idx < src_samples {
235 output[[ch, i]] = data[[ch, src_idx]];
236 }
237 }
239 }
240
241 Ok(output.into_dyn())
242}
243
244pub fn resample_sinc(
264 data: &ArrayD<f32>,
265 src_rate: u32,
266 dst_rate: u32,
267 config: SincConfig,
268) -> Result<ArrayD<f32>> {
269 if src_rate == 0 || dst_rate == 0 {
270 return Err(IoError::ConversionError(
271 "Sample rate must be positive".to_string(),
272 ));
273 }
274 if src_rate == dst_rate {
275 return Ok(data.clone());
276 }
277 if data.ndim() < 2 {
278 return Err(IoError::FormatError(
279 "Audio data must be 2D [channels, samples]".to_string(),
280 ));
281 }
282
283 let channels = data.shape()[0];
284 let src_samples = data.shape()[1];
285
286 if src_samples == 0 {
287 return Ok(data.clone());
288 }
289
290 let ratio = dst_rate as f64 / src_rate as f64;
291 let dst_samples = ((src_samples as f64) * ratio).ceil() as usize;
292
293 let cutoff = if ratio < 1.0 { ratio } else { 1.0 };
295 let half_width = config.kernel_half_width as f64;
296
297 let effective_half_width = if ratio < 1.0 {
299 half_width / cutoff
300 } else {
301 half_width
302 };
303
304 let mut output = Array2::zeros((channels, dst_samples));
305
306 for ch in 0..channels {
307 for i in 0..dst_samples {
308 let src_pos = i as f64 / ratio;
309 let src_center = src_pos.floor() as i64;
310
311 let lo = (src_center - effective_half_width.ceil() as i64).max(0) as usize;
312 let hi = (src_center + effective_half_width.ceil() as i64 + 1).min(src_samples as i64)
313 as usize;
314
315 let mut sum = 0.0f64;
316 let mut weight_sum = 0.0f64;
317
318 for j in lo..hi {
319 let delta = (j as f64 - src_pos) * cutoff;
320 let w = windowed_sinc(delta, half_width, config.window);
321 sum += data[[ch, j]] as f64 * w;
322 weight_sum += w;
323 }
324
325 if weight_sum.abs() > 1e-12 {
326 output[[ch, i]] = (sum / weight_sum) as f32;
327 }
328 }
329 }
330
331 Ok(output.into_dyn())
332}
333
334pub fn resample(
351 data: &ArrayD<f32>,
352 src_rate: u32,
353 dst_rate: u32,
354 quality: ResampleQuality,
355) -> Result<ArrayD<f32>> {
356 if quality == ResampleQuality::Low {
357 resample_linear(data, src_rate, dst_rate)
358 } else {
359 resample_sinc(data, src_rate, dst_rate, quality.to_sinc_config())
360 }
361}
362
363pub fn resampled_length(src_samples: usize, src_rate: u32, dst_rate: u32) -> usize {
369 if src_rate == 0 || dst_rate == 0 {
370 return 0;
371 }
372 let ratio = dst_rate as f64 / src_rate as f64;
373 ((src_samples as f64) * ratio).ceil() as usize
374}
375
376pub fn sample_rate_ratio(src_rate: u32, dst_rate: u32) -> (u32, u32) {
381 let g = gcd(src_rate, dst_rate);
382 (dst_rate / g, src_rate / g)
383}
384
385fn gcd(mut a: u32, mut b: u32) -> u32 {
386 while b != 0 {
387 let t = b;
388 b = a % b;
389 a = t;
390 }
391 a
392}
393
394#[cfg(test)]
399mod tests {
400 use super::*;
401 use scirs2_core::ndarray::Array2;
402
403 fn sine_wave(freq: f64, sample_rate: u32, duration_secs: f64) -> ArrayD<f32> {
405 let n = (sample_rate as f64 * duration_secs) as usize;
406 let mut data = Array2::zeros((1, n));
407 for i in 0..n {
408 let t = i as f64 / sample_rate as f64;
409 data[[0, i]] = (2.0 * std::f64::consts::PI * freq * t).sin() as f32;
410 }
411 data.into_dyn()
412 }
413
414 #[test]
415 fn test_resample_linear_identity() {
416 let data = sine_wave(440.0, 44100, 0.1);
417 let result = resample_linear(&data, 44100, 44100).expect("resample failed");
418 assert_eq!(result.shape(), data.shape());
419 let orig = data.as_slice().expect("not contiguous");
420 let res = result.as_slice().expect("not contiguous");
421 for (a, b) in orig.iter().zip(res.iter()) {
422 assert!((a - b).abs() < 1e-10);
423 }
424 }
425
426 #[test]
427 fn test_resample_linear_upsample() {
428 let data = sine_wave(440.0, 22050, 0.05);
429 let result = resample_linear(&data, 22050, 44100).expect("resample failed");
430
431 let expected_len = resampled_length(data.shape()[1], 22050, 44100);
433 assert_eq!(result.shape()[1], expected_len);
434 assert_eq!(result.shape()[0], 1);
435 }
436
437 #[test]
438 fn test_resample_linear_downsample() {
439 let data = sine_wave(440.0, 44100, 0.05);
440 let result = resample_linear(&data, 44100, 22050).expect("resample failed");
441
442 let expected_len = resampled_length(data.shape()[1], 44100, 22050);
444 assert_eq!(result.shape()[1], expected_len);
445 }
446
447 #[test]
448 fn test_resample_sinc_identity() {
449 let data = sine_wave(440.0, 44100, 0.1);
450 let result =
451 resample_sinc(&data, 44100, 44100, SincConfig::default()).expect("resample failed");
452 assert_eq!(result.shape(), data.shape());
453 }
454
455 #[test]
456 fn test_resample_sinc_upsample_quality() {
457 let data = sine_wave(1000.0, 8000, 0.05);
459 let result =
460 resample_sinc(&data, 8000, 48000, SincConfig::default()).expect("resample failed");
461
462 let dst_samples = result.shape()[1];
463 let period_samples = 48000.0 / 1000.0; let mid = dst_samples / 2;
469 let max_val = result
470 .as_slice()
471 .expect("not contiguous")
472 .iter()
473 .skip(mid.saturating_sub(48))
474 .take(96)
475 .fold(0.0f32, |a, &b| a.max(b.abs()));
476 assert!(
477 max_val > 0.8,
478 "Upsampled signal amplitude too low: {}",
479 max_val
480 );
481 assert!(
482 max_val < 1.2,
483 "Upsampled signal amplitude too high: {}",
484 max_val
485 );
486
487 let _ = period_samples; }
490
491 #[test]
492 fn test_resample_sinc_downsample() {
493 let data = sine_wave(440.0, 48000, 0.05);
494 let result =
495 resample_sinc(&data, 48000, 16000, SincConfig::default()).expect("resample failed");
496 let expected_len = resampled_length(data.shape()[1], 48000, 16000);
497 assert_eq!(result.shape()[1], expected_len);
498 }
499
500 #[test]
501 fn test_resample_stereo() {
502 let n = 1000;
504 let mut data = Array2::zeros((2, n));
505 for i in 0..n {
506 let t = i as f64 / 44100.0;
507 data[[0, i]] = (2.0 * std::f64::consts::PI * 440.0 * t).sin() as f32;
508 data[[1, i]] = (2.0 * std::f64::consts::PI * 880.0 * t).sin() as f32;
509 }
510 let dyn_data = data.into_dyn();
511
512 let result =
513 resample_sinc(&dyn_data, 44100, 22050, SincConfig::default()).expect("resample failed");
514 assert_eq!(result.shape()[0], 2);
515 let expected_len = resampled_length(n, 44100, 22050);
516 assert_eq!(result.shape()[1], expected_len);
517 }
518
519 #[test]
520 fn test_resample_quality_presets() {
521 let data = sine_wave(440.0, 44100, 0.02);
522
523 for quality in [
524 ResampleQuality::Low,
525 ResampleQuality::Medium,
526 ResampleQuality::High,
527 ResampleQuality::Ultra,
528 ] {
529 let result = resample(&data, 44100, 22050, quality).expect("resample failed");
530 let expected_len = resampled_length(data.shape()[1], 44100, 22050);
531 assert_eq!(result.shape()[1], expected_len);
532 }
533 }
534
535 #[test]
536 fn test_resample_zero_rate_error() {
537 let data = sine_wave(440.0, 44100, 0.01);
538 assert!(resample_linear(&data, 0, 44100).is_err());
539 assert!(resample_linear(&data, 44100, 0).is_err());
540 }
541
542 #[test]
543 fn test_resample_1d_error() {
544 let data = scirs2_core::ndarray::arr1(&[1.0f32, 2.0, 3.0]).into_dyn();
545 assert!(resample_linear(&data, 44100, 22050).is_err());
546 }
547
548 #[test]
549 fn test_sample_rate_ratio() {
550 let (p, q) = sample_rate_ratio(44100, 48000);
551 assert_eq!(p, 160);
553 assert_eq!(q, 147);
554
555 let (p, q) = sample_rate_ratio(44100, 22050);
556 assert_eq!(p, 1);
558 assert_eq!(q, 2);
559
560 let (p, q) = sample_rate_ratio(8000, 48000);
561 assert_eq!(p, 6);
563 assert_eq!(q, 1);
564 }
565
566 #[test]
567 fn test_resampled_length() {
568 assert_eq!(resampled_length(44100, 44100, 22050), 22050);
569 assert_eq!(resampled_length(44100, 44100, 88200), 88200);
570 assert_eq!(resampled_length(0, 44100, 22050), 0);
571 assert_eq!(resampled_length(100, 0, 22050), 0);
572 }
573
574 #[test]
575 fn test_window_functions() {
576 let hw = 16.0;
578 for window in [
579 WindowFunction::Rectangular,
580 WindowFunction::Hann,
581 WindowFunction::Blackman,
582 WindowFunction::BlackmanHarris,
583 WindowFunction::Kaiser(800), ] {
585 let center = evaluate_window(window, 0.0, hw);
586 if window != WindowFunction::Rectangular {
587 assert!(center > 0.3, "Window center too low: {}", center);
589 }
590 let outside = evaluate_window(window, hw + 1.0, hw);
592 assert!(
593 outside.abs() < 1e-10,
594 "Window outside not zero: {}",
595 outside
596 );
597 }
598 }
599
600 #[test]
601 fn test_bessel_i0() {
602 assert!((bessel_i0(0.0) - 1.0).abs() < 1e-12);
604 assert!((bessel_i0(1.0) - 1.2660658).abs() < 1e-4);
606 assert!((bessel_i0(5.0) - 27.2398718).abs() < 1e-3);
608 }
609
610 #[test]
611 fn test_windowed_sinc_at_zero() {
612 let val = windowed_sinc(0.0, 16.0, WindowFunction::BlackmanHarris);
614 assert!((val - 1.0).abs() < 1e-12);
615 }
616
617 #[test]
618 fn test_windowed_sinc_at_integers() {
619 for n in 1..10 {
621 let val = windowed_sinc(n as f64, 16.0, WindowFunction::BlackmanHarris);
622 assert!(val.abs() < 1e-10, "sinc({}) = {} (expected 0)", n, val);
623 }
624 }
625
626 #[test]
627 fn test_empty_data_resample() {
628 let data = Array2::<f32>::zeros((1, 0)).into_dyn();
629 let result = resample_linear(&data, 44100, 22050).expect("resample failed");
630 assert_eq!(result.shape()[1], 0);
631 }
632
633 #[test]
634 fn test_resample_preserves_dc_offset() {
635 let n = 1000;
637 let dc_level = 0.5f32;
638 let mut data = Array2::zeros((1, n));
639 for i in 0..n {
640 data[[0, i]] = dc_level;
641 }
642 let dyn_data = data.into_dyn();
643
644 let result =
645 resample_sinc(&dyn_data, 44100, 22050, SincConfig::default()).expect("resample failed");
646
647 let out = result.as_slice().expect("not contiguous");
649 let skip = 20; for &v in out
651 .iter()
652 .skip(skip)
653 .take(out.len().saturating_sub(2 * skip))
654 {
655 assert!(
656 (v - dc_level).abs() < 0.05,
657 "DC not preserved: {} (expected {})",
658 v,
659 dc_level
660 );
661 }
662 }
663}