1use std::borrow::Cow;
2
3#[derive(Debug, Clone)]
28pub struct AudioFrame<'a> {
29 samples: Cow<'a, [f32]>,
30 sample_rate: u32,
31}
32
33impl<'a> AudioFrame<'a> {
34 pub fn new(samples: impl IntoSamples<'a>, sample_rate: u32) -> Self {
38 Self {
39 samples: samples.into_samples(),
40 sample_rate,
41 }
42 }
43
44 pub fn samples(&self) -> &[f32] {
46 &self.samples
47 }
48
49 pub fn sample_rate(&self) -> u32 {
51 self.sample_rate
52 }
53
54 pub fn len(&self) -> usize {
56 self.samples.len()
57 }
58
59 pub fn is_empty(&self) -> bool {
61 self.samples.is_empty()
62 }
63
64 pub fn duration_secs(&self) -> f64 {
66 self.samples.len() as f64 / self.sample_rate as f64
67 }
68
69 pub fn into_owned(self) -> AudioFrame<'static> {
71 AudioFrame {
72 samples: Cow::Owned(self.samples.into_owned()),
73 sample_rate: self.sample_rate,
74 }
75 }
76}
77
78impl AudioFrame<'static> {
79 pub fn from_vec(samples: Vec<f32>, sample_rate: u32) -> Self {
95 Self {
96 samples: Cow::Owned(samples),
97 sample_rate,
98 }
99 }
100}
101
102#[cfg(feature = "resample")]
103impl AudioFrame<'_> {
104 pub fn resample(&self, target_rate: u32) -> Result<AudioFrame<'static>, crate::CoreError> {
127 use rubato::audioadapter_buffers::direct::InterleavedSlice;
128 use rubato::Resampler;
129
130 if self.sample_rate == target_rate {
131 return Ok(self.clone().into_owned());
132 }
133
134 if self.is_empty() {
135 return Ok(AudioFrame::from_vec(Vec::new(), target_rate));
136 }
137
138 let nbr_input_frames = self.samples.len();
139 let chunk_size = nbr_input_frames.min(1024);
142 let mut resampler = build_sinc_resampler(self.sample_rate, target_rate, chunk_size)?;
143
144 let out_len = resampler.process_all_needed_output_len(nbr_input_frames);
148 let mut outdata = vec![0.0f32; out_len];
149
150 let input_adapter = InterleavedSlice::new(self.samples.as_ref(), 1, nbr_input_frames)
151 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
152 let mut output_adapter = InterleavedSlice::new_mut(&mut outdata, 1, out_len)
153 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
154
155 let (_in_consumed, out_produced) = resampler
156 .process_all_into_buffer(&input_adapter, &mut output_adapter, nbr_input_frames, None)
157 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
158
159 outdata.truncate(out_produced);
160 Ok(AudioFrame::from_vec(outdata, target_rate))
161 }
162}
163
164#[cfg(feature = "resample")]
168fn build_sinc_resampler(
169 source_rate: u32,
170 target_rate: u32,
171 chunk_size: usize,
172) -> Result<rubato::Async<f32>, crate::CoreError> {
173 use rubato::{
174 Async, FixedAsync, SincInterpolationParameters, SincInterpolationType, WindowFunction,
175 };
176
177 if source_rate == 0 || target_rate == 0 {
178 return Err(crate::CoreError::Audio(
179 "sample rate must be non-zero".into(),
180 ));
181 }
182 if chunk_size == 0 {
183 return Err(crate::CoreError::Audio(
184 "chunk_size must be non-zero".into(),
185 ));
186 }
187
188 let params = SincInterpolationParameters {
189 sinc_len: 256,
190 f_cutoff: 0.95,
191 interpolation: SincInterpolationType::Cubic,
192 oversampling_factor: 128,
193 window: WindowFunction::BlackmanHarris2,
194 };
195 let ratio = target_rate as f64 / source_rate as f64;
196 Async::<f32>::new_sinc(ratio, 1.0, ¶ms, chunk_size, 1, FixedAsync::Input)
197 .map_err(|e| crate::CoreError::Audio(e.to_string()))
198}
199
200#[cfg(feature = "resample")]
238pub struct StreamingResampler {
239 inner: Option<rubato::Async<f32>>,
241 source_rate: u32,
242 target_rate: u32,
243 chunk_size: usize,
244 input_buf: Vec<f32>,
247 output_buf: Vec<f32>,
250}
251
252#[cfg(feature = "resample")]
253impl StreamingResampler {
254 pub fn new(
264 source_rate: u32,
265 target_rate: u32,
266 chunk_size: usize,
267 ) -> Result<Self, crate::CoreError> {
268 if source_rate == target_rate {
269 if source_rate == 0 {
272 return Err(crate::CoreError::Audio(
273 "sample rate must be non-zero".into(),
274 ));
275 }
276 return Ok(Self {
277 inner: None,
278 source_rate,
279 target_rate,
280 chunk_size,
281 input_buf: Vec::new(),
282 output_buf: Vec::new(),
283 });
284 }
285
286 let inner = build_sinc_resampler(source_rate, target_rate, chunk_size)?;
287 let out_max = {
288 use rubato::Resampler;
289 inner.output_frames_max()
290 };
291 Ok(Self {
292 inner: Some(inner),
293 source_rate,
294 target_rate,
295 chunk_size,
296 input_buf: Vec::with_capacity(chunk_size),
297 output_buf: vec![0.0; out_max],
298 })
299 }
300
301 pub fn source_rate(&self) -> u32 {
303 self.source_rate
304 }
305
306 pub fn target_rate(&self) -> u32 {
308 self.target_rate
309 }
310
311 pub fn chunk_size(&self) -> usize {
313 self.chunk_size
314 }
315
316 pub fn process(&mut self, input: &[f32], out: &mut Vec<f32>) -> Result<(), crate::CoreError> {
325 let Some(inner) = self.inner.as_mut() else {
326 out.extend_from_slice(input);
327 return Ok(());
328 };
329 use rubato::audioadapter_buffers::direct::InterleavedSlice;
330 use rubato::Resampler;
331
332 let mut remaining = input;
333 while !remaining.is_empty() {
334 let need = self.chunk_size - self.input_buf.len();
335 let take = need.min(remaining.len());
336 self.input_buf.extend_from_slice(&remaining[..take]);
337 remaining = &remaining[take..];
338
339 if self.input_buf.len() < self.chunk_size {
340 break;
341 }
342
343 let in_adapter = InterleavedSlice::new(&self.input_buf[..], 1, self.chunk_size)
344 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
345 let out_buf_len = self.output_buf.len();
346 let mut out_adapter =
347 InterleavedSlice::new_mut(&mut self.output_buf[..], 1, out_buf_len)
348 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
349 let (_in_used, out_produced) = inner
350 .process_into_buffer(&in_adapter, &mut out_adapter, None)
351 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
352 out.extend_from_slice(&self.output_buf[..out_produced]);
353 self.input_buf.clear();
354 }
355 Ok(())
356 }
357}
358
359#[cfg(feature = "wav")]
360impl AudioFrame<'_> {
361 pub fn write_wav(&self, path: impl AsRef<std::path::Path>) -> Result<(), crate::CoreError> {
374 let spec = hound::WavSpec {
375 channels: 1,
376 sample_rate: self.sample_rate,
377 bits_per_sample: 32,
378 sample_format: hound::SampleFormat::Float,
379 };
380 let mut writer = hound::WavWriter::create(path, spec)?;
381 for &sample in self.samples() {
382 writer.write_sample(sample)?;
383 }
384 writer.finalize()?;
385 Ok(())
386 }
387}
388
389#[cfg(feature = "wav")]
390impl AudioFrame<'static> {
391 pub fn from_wav(path: impl AsRef<std::path::Path>) -> Result<Self, crate::CoreError> {
406 let bytes = std::fs::read(path)?;
407 if let Some((samples, sample_rate)) = g711_wav_samples(&bytes)? {
411 return Ok(AudioFrame::from_vec(samples, sample_rate));
412 }
413 let mut reader = hound::WavReader::new(std::io::Cursor::new(bytes))?;
414 let spec = reader.spec();
415 let sample_rate = spec.sample_rate;
416 let samples: Vec<f32> = match spec.sample_format {
417 hound::SampleFormat::Float => reader.samples::<f32>().collect::<Result<_, _>>()?,
418 hound::SampleFormat::Int => reader
419 .samples::<i16>()
420 .map(|s| s.map(|v| v as f32 / 32768.0))
421 .collect::<Result<_, _>>()?,
422 };
423 Ok(AudioFrame::from_vec(samples, sample_rate))
424 }
425}
426
427#[cfg(feature = "wav")]
429const WAVE_FORMAT_ALAW: u16 = 0x0006;
430#[cfg(feature = "wav")]
431const WAVE_FORMAT_MULAW: u16 = 0x0007;
432
433#[cfg(feature = "wav")]
437fn riff_chunk<'a>(bytes: &'a [u8], id: &[u8; 4]) -> Option<&'a [u8]> {
438 if bytes.len() < 12 || &bytes[..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
439 return None;
440 }
441 let mut pos = 12;
442 while pos + 8 <= bytes.len() {
443 let size = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().ok()?) as usize;
444 let start = pos + 8;
445 if &bytes[pos..pos + 4] == id {
446 return Some(&bytes[start..start.checked_add(size)?.min(bytes.len())]);
447 }
448 pos = start.checked_add(size)?.checked_add(size & 1)?;
450 }
451 None
452}
453
454#[cfg(feature = "wav")]
460fn g711_wav_samples(bytes: &[u8]) -> Result<Option<(Vec<f32>, u32)>, crate::CoreError> {
461 let Some(fmt) = riff_chunk(bytes, b"fmt ") else {
462 return Ok(None);
463 };
464 if fmt.len() < 16 {
465 return Ok(None);
466 }
467 let decode: fn(u8) -> i16 = match u16::from_le_bytes([fmt[0], fmt[1]]) {
468 WAVE_FORMAT_ALAW => crate::codec::g711::alaw_to_linear,
469 WAVE_FORMAT_MULAW => crate::codec::g711::ulaw_to_linear,
470 _ => return Ok(None),
471 };
472 let sample_rate = u32::from_le_bytes([fmt[4], fmt[5], fmt[6], fmt[7]]);
473 let data = riff_chunk(bytes, b"data")
474 .ok_or_else(|| crate::CoreError::Audio("G.711 WAV has no data chunk".to_string()))?;
475 let samples = data.iter().map(|&b| decode(b) as f32 / 32768.0).collect();
476 Ok(Some((samples, sample_rate)))
477}
478
479pub trait IntoSamples<'a> {
483 fn into_samples(self) -> Cow<'a, [f32]>;
485}
486
487impl<'a> IntoSamples<'a> for &'a [f32] {
488 #[inline]
489 fn into_samples(self) -> Cow<'a, [f32]> {
490 Cow::Borrowed(self)
491 }
492}
493
494impl<'a> IntoSamples<'a> for &'a Vec<f32> {
495 #[inline]
496 fn into_samples(self) -> Cow<'a, [f32]> {
497 Cow::Borrowed(self.as_slice())
498 }
499}
500
501impl<'a, const N: usize> IntoSamples<'a> for &'a [f32; N] {
502 #[inline]
503 fn into_samples(self) -> Cow<'a, [f32]> {
504 Cow::Borrowed(self.as_slice())
505 }
506}
507
508impl<'a> IntoSamples<'a> for &'a [i16] {
509 #[inline]
510 fn into_samples(self) -> Cow<'a, [f32]> {
511 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
512 }
513}
514
515impl<'a> IntoSamples<'a> for &'a Vec<i16> {
516 #[inline]
517 fn into_samples(self) -> Cow<'a, [f32]> {
518 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
519 }
520}
521
522impl<'a, const N: usize> IntoSamples<'a> for &'a [i16; N] {
523 #[inline]
524 fn into_samples(self) -> Cow<'a, [f32]> {
525 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn f32_is_zero_copy() {
535 let samples = vec![0.1f32, -0.2, 0.3];
536 let frame = AudioFrame::new(samples.as_slice(), 16000);
537 assert!(matches!(frame.samples, Cow::Borrowed(_)));
539 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
540 }
541
542 #[test]
543 fn i16_normalizes_to_f32() {
544 let samples: Vec<i16> = vec![0, 16384, -16384, i16::MAX, i16::MIN];
545 let frame = AudioFrame::new(samples.as_slice(), 16000);
546 assert!(matches!(frame.samples, Cow::Owned(_)));
547
548 let s = frame.samples();
549 assert!((s[0] - 0.0).abs() < f32::EPSILON);
550 assert!((s[1] - 0.5).abs() < 0.001);
551 assert!((s[2] - -0.5).abs() < 0.001);
552 assert!((s[3] - (i16::MAX as f32 / 32768.0)).abs() < f32::EPSILON);
553 assert!((s[4] - -1.0).abs() < f32::EPSILON);
554 }
555
556 #[test]
557 fn metadata() {
558 let samples = vec![0.0f32; 160];
559 let frame = AudioFrame::new(samples.as_slice(), 16000);
560 assert_eq!(frame.sample_rate(), 16000);
561 assert_eq!(frame.len(), 160);
562 assert!(!frame.is_empty());
563 assert!((frame.duration_secs() - 0.01).abs() < 1e-9);
564 }
565
566 #[test]
567 fn empty_frame() {
568 let samples: &[f32] = &[];
569 let frame = AudioFrame::new(samples, 16000);
570 assert!(frame.is_empty());
571 assert_eq!(frame.len(), 0);
572 }
573
574 #[test]
575 fn into_owned() {
576 let samples = vec![0.5f32, -0.5];
577 let frame = AudioFrame::new(samples.as_slice(), 16000);
578 let owned: AudioFrame<'static> = frame.into_owned();
579 assert_eq!(owned.samples(), &[0.5, -0.5]);
580 assert_eq!(owned.sample_rate(), 16000);
581 }
582
583 #[cfg(feature = "wav")]
584 #[test]
585 fn wav_read_i16() {
586 let path = std::env::temp_dir().join("wavekat_test_i16.wav");
588 let spec = hound::WavSpec {
589 channels: 1,
590 sample_rate: 16000,
591 bits_per_sample: 16,
592 sample_format: hound::SampleFormat::Int,
593 };
594 let i16_samples: &[i16] = &[0, i16::MAX, i16::MIN, 16384];
595 let mut writer = hound::WavWriter::create(&path, spec).unwrap();
596 for &s in i16_samples {
597 writer.write_sample(s).unwrap();
598 }
599 writer.finalize().unwrap();
600
601 let frame = AudioFrame::from_wav(&path).unwrap();
602 assert_eq!(frame.sample_rate(), 16000);
603 assert_eq!(frame.len(), 4);
604 let s = frame.samples();
605 assert!((s[0] - 0.0).abs() < 1e-6);
606 assert!((s[1] - (i16::MAX as f32 / 32768.0)).abs() < 1e-6);
607 assert!((s[2] - -1.0).abs() < 1e-6);
608 assert!((s[3] - 0.5).abs() < 1e-4);
609 }
610
611 #[cfg(feature = "wav")]
612 #[test]
613 fn wav_round_trip() {
614 let original = AudioFrame::from_vec(vec![0.5f32, -0.5, 0.0, 1.0], 16000);
615 let path = std::env::temp_dir().join("wavekat_test.wav");
616 original.write_wav(&path).unwrap();
617 let loaded = AudioFrame::from_wav(&path).unwrap();
618 assert_eq!(loaded.sample_rate(), 16000);
619 for (a, b) in original.samples().iter().zip(loaded.samples()) {
620 assert!((a - b).abs() < 1e-6, "sample mismatch: {a} vs {b}");
621 }
622 }
623
624 #[cfg(feature = "wav")]
628 fn write_g711_wav(path: &std::path::Path, format_tag: u16, sample_rate: u32, data: &[u8]) {
629 let mut bytes = Vec::new();
630 bytes.extend_from_slice(b"RIFF");
631 bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(b"WAVE");
633 bytes.extend_from_slice(b"fmt ");
634 bytes.extend_from_slice(&18u32.to_le_bytes());
635 bytes.extend_from_slice(&format_tag.to_le_bytes());
636 bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(&sample_rate.to_le_bytes());
638 bytes.extend_from_slice(&sample_rate.to_le_bytes()); bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(&8u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(b"fact");
643 bytes.extend_from_slice(&4u32.to_le_bytes());
644 bytes.extend_from_slice(&(data.len() as u32).to_le_bytes());
645 bytes.extend_from_slice(b"data");
646 bytes.extend_from_slice(&(data.len() as u32).to_le_bytes());
647 bytes.extend_from_slice(data);
648 if data.len() % 2 == 1 {
649 bytes.push(0); }
651 let riff_size = (bytes.len() - 8) as u32;
652 bytes[4..8].copy_from_slice(&riff_size.to_le_bytes());
653 std::fs::write(path, bytes).unwrap();
654 }
655
656 #[cfg(feature = "wav")]
657 #[test]
658 fn wav_read_g711_ulaw() {
659 use crate::codec::g711;
660 let pcm: &[i16] = &[0, 1000, -1000, i16::MAX, i16::MIN + 1];
661 let encoded: Vec<u8> = pcm.iter().map(|&s| g711::linear_to_ulaw(s)).collect();
662 let path = std::env::temp_dir().join("wavekat_test_ulaw.wav");
663 write_g711_wav(&path, 0x0007, 8000, &encoded);
664
665 let frame = AudioFrame::from_wav(&path).unwrap();
666 assert_eq!(frame.sample_rate(), 8000);
667 assert_eq!(frame.len(), pcm.len());
668 for (got, &byte) in frame.samples().iter().zip(&encoded) {
669 let want = g711::ulaw_to_linear(byte) as f32 / 32768.0;
670 assert!(
671 (got - want).abs() < 1e-6,
672 "sample mismatch: {got} vs {want}"
673 );
674 }
675 }
676
677 #[cfg(feature = "wav")]
678 #[test]
679 fn wav_read_g711_alaw() {
680 use crate::codec::g711;
681 let pcm: &[i16] = &[0, 1000, -1000, i16::MAX, i16::MIN + 1];
682 let encoded: Vec<u8> = pcm.iter().map(|&s| g711::linear_to_alaw(s)).collect();
683 let path = std::env::temp_dir().join("wavekat_test_alaw.wav");
684 write_g711_wav(&path, 0x0006, 8000, &encoded);
685
686 let frame = AudioFrame::from_wav(&path).unwrap();
687 assert_eq!(frame.sample_rate(), 8000);
688 assert_eq!(frame.len(), pcm.len());
689 for (got, &byte) in frame.samples().iter().zip(&encoded) {
690 let want = g711::alaw_to_linear(byte) as f32 / 32768.0;
691 assert!(
692 (got - want).abs() < 1e-6,
693 "sample mismatch: {got} vs {want}"
694 );
695 }
696 }
697
698 #[cfg(feature = "wav")]
699 #[test]
700 fn wav_read_g711_missing_data_chunk_errors() {
701 let mut bytes = Vec::new();
703 bytes.extend_from_slice(b"RIFF");
704 bytes.extend_from_slice(&30u32.to_le_bytes());
705 bytes.extend_from_slice(b"WAVE");
706 bytes.extend_from_slice(b"fmt ");
707 bytes.extend_from_slice(&18u32.to_le_bytes());
708 bytes.extend_from_slice(&0x0007u16.to_le_bytes());
709 bytes.extend_from_slice(&1u16.to_le_bytes());
710 bytes.extend_from_slice(&8000u32.to_le_bytes());
711 bytes.extend_from_slice(&8000u32.to_le_bytes());
712 bytes.extend_from_slice(&1u16.to_le_bytes());
713 bytes.extend_from_slice(&8u16.to_le_bytes());
714 bytes.extend_from_slice(&0u16.to_le_bytes());
715 let path = std::env::temp_dir().join("wavekat_test_ulaw_nodata.wav");
716 std::fs::write(&path, bytes).unwrap();
717
718 let err = AudioFrame::from_wav(&path).unwrap_err();
719 assert!(
720 err.to_string().contains("data chunk"),
721 "unexpected error: {err}"
722 );
723 }
724
725 #[cfg(feature = "wav")]
726 #[test]
727 fn wav_read_truncated_fmt_chunk_falls_through_to_hound() {
728 let mut bytes = Vec::new();
731 bytes.extend_from_slice(b"RIFF");
732 bytes.extend_from_slice(&20u32.to_le_bytes());
733 bytes.extend_from_slice(b"WAVE");
734 bytes.extend_from_slice(b"fmt ");
735 bytes.extend_from_slice(&8u32.to_le_bytes());
736 bytes.extend_from_slice(&0x0007u16.to_le_bytes());
737 bytes.extend_from_slice(&1u16.to_le_bytes());
738 bytes.extend_from_slice(&8000u32.to_le_bytes());
739 let path = std::env::temp_dir().join("wavekat_test_short_fmt.wav");
740 std::fs::write(&path, bytes).unwrap();
741
742 assert!(AudioFrame::from_wav(&path).is_err());
743 }
744
745 #[cfg(feature = "wav")]
746 #[test]
747 fn wav_read_non_riff_bytes_still_error() {
748 let path = std::env::temp_dir().join("wavekat_test_not_a_wav.wav");
751 std::fs::write(&path, b"these bytes are not a RIFF WAV").unwrap();
752 assert!(AudioFrame::from_wav(&path).is_err());
753 }
754
755 #[test]
756 fn from_vec_is_zero_copy() {
757 let samples = vec![0.5f32, -0.5];
758 let ptr = samples.as_ptr();
759 let frame = AudioFrame::from_vec(samples, 24000);
760 assert_eq!(frame.samples().as_ptr(), ptr);
761 assert_eq!(frame.sample_rate(), 24000);
762 }
763
764 #[test]
765 fn into_samples_vec_f32() {
766 let samples = vec![0.1f32, -0.2, 0.3];
767 let frame = AudioFrame::new(&samples, 16000);
768 assert!(matches!(frame.samples, Cow::Borrowed(_)));
769 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
770 }
771
772 #[test]
773 fn into_samples_array_f32() {
774 let samples = [0.1f32, -0.2, 0.3];
775 let frame = AudioFrame::new(&samples, 16000);
776 assert!(matches!(frame.samples, Cow::Borrowed(_)));
777 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
778 }
779
780 #[test]
781 fn into_samples_vec_i16() {
782 let samples: Vec<i16> = vec![0, 16384, i16::MIN];
783 let frame = AudioFrame::new(&samples, 16000);
784 assert!(matches!(frame.samples, Cow::Owned(_)));
785 let s = frame.samples();
786 assert!((s[0] - 0.0).abs() < f32::EPSILON);
787 assert!((s[1] - 0.5).abs() < 0.001);
788 assert!((s[2] - -1.0).abs() < f32::EPSILON);
789 }
790
791 #[test]
792 fn into_samples_array_i16() {
793 let samples: [i16; 3] = [0, 16384, i16::MIN];
794 let frame = AudioFrame::new(&samples, 16000);
795 assert!(matches!(frame.samples, Cow::Owned(_)));
796 let s = frame.samples();
797 assert!((s[0] - 0.0).abs() < f32::EPSILON);
798 assert!((s[1] - 0.5).abs() < 0.001);
799 assert!((s[2] - -1.0).abs() < f32::EPSILON);
800 }
801
802 #[cfg(feature = "resample")]
803 #[test]
804 fn resample_noop_same_rate() {
805 let samples = vec![0.1f32, -0.2, 0.3, 0.4, 0.5];
806 let frame = AudioFrame::from_vec(samples.clone(), 16000);
807 let resampled = frame.resample(16000).unwrap();
808 assert_eq!(resampled.sample_rate(), 16000);
809 assert_eq!(resampled.samples(), &samples[..]);
810 }
811
812 #[cfg(feature = "resample")]
813 #[test]
814 fn resample_empty_frame() {
815 let frame = AudioFrame::from_vec(Vec::new(), 44100);
816 let resampled = frame.resample(16000).unwrap();
817 assert_eq!(resampled.sample_rate(), 16000);
818 assert!(resampled.is_empty());
819 }
820
821 #[cfg(feature = "resample")]
822 #[test]
823 fn resample_downsample() {
824 let frame = AudioFrame::from_vec(vec![0.0f32; 48000], 48000);
826 let resampled = frame.resample(16000).unwrap();
827 assert_eq!(resampled.sample_rate(), 16000);
828 let expected = 16000;
830 let tolerance = 50;
831 assert!(
832 (resampled.len() as i64 - expected as i64).unsigned_abs() < tolerance,
833 "expected ~{expected} samples, got {}",
834 resampled.len()
835 );
836 }
837
838 #[cfg(feature = "resample")]
839 #[test]
840 fn resample_upsample() {
841 let frame = AudioFrame::from_vec(vec![0.0f32; 16000], 16000);
843 let resampled = frame.resample(24000).unwrap();
844 assert_eq!(resampled.sample_rate(), 24000);
845 let expected = 24000;
846 let tolerance = 50;
847 assert!(
848 (resampled.len() as i64 - expected as i64).unsigned_abs() < tolerance,
849 "expected ~{expected} samples, got {}",
850 resampled.len()
851 );
852 }
853
854 #[cfg(feature = "resample")]
855 #[test]
856 fn resample_short_input_upsample_large_ratio() {
857 let frame = AudioFrame::from_vec(vec![0.0f32; 160], 8000);
861 let resampled = frame.resample(44_100).unwrap();
862 assert_eq!(resampled.sample_rate(), 44_100);
863 let expected = (160.0 * 44_100.0 / 8_000.0) as i64; let actual = resampled.len() as i64;
865 assert!(
866 (actual - expected).unsigned_abs() < 50,
867 "expected ~{expected} samples, got {actual}"
868 );
869 }
870
871 #[cfg(feature = "resample")]
872 #[test]
873 fn resample_short_input_upsample_small_ratio() {
874 let frame = AudioFrame::from_vec(vec![0.0f32; 160], 8000);
877 let resampled = frame.resample(16_000).unwrap();
878 assert_eq!(resampled.sample_rate(), 16_000);
879 let expected: i64 = 320;
880 let actual = resampled.len() as i64;
881 assert!(
882 (actual - expected).unsigned_abs() < 50,
883 "expected ~{expected} samples, got {actual}"
884 );
885 }
886
887 #[cfg(feature = "resample")]
888 #[test]
889 fn resample_single_g711_frame_to_48k() {
890 let frame = AudioFrame::from_vec(vec![0.0f32; 160], 8000);
892 let resampled = frame.resample(48_000).unwrap();
893 assert_eq!(resampled.sample_rate(), 48_000);
894 let expected: i64 = 960;
895 let actual = resampled.len() as i64;
896 assert!(
897 (actual - expected).unsigned_abs() < 50,
898 "expected ~{expected} samples, got {actual}"
899 );
900 }
901
902 #[cfg(feature = "resample")]
903 #[test]
904 fn resample_preserves_sine_frequency() {
905 let sr_in: u32 = 44100;
909 let sr_out: u32 = 16000;
910 let duration_secs = 1.0;
911 let freq = 440.0;
912 let n = (sr_in as f64 * duration_secs) as usize;
913 let samples: Vec<f32> = (0..n)
914 .map(|i| (2.0 * std::f64::consts::PI * freq * i as f64 / sr_in as f64).sin() as f32)
915 .collect();
916
917 let frame = AudioFrame::from_vec(samples, sr_in);
918 let resampled = frame.resample(sr_out).unwrap();
919
920 let s = resampled.samples();
922 let crossings: usize = s
923 .windows(2)
924 .filter(|w| w[0].signum() != w[1].signum())
925 .count();
926 let measured_freq = crossings as f64 / (2.0 * duration_secs);
928 assert!(
929 (measured_freq - freq).abs() < 5.0,
930 "expected ~{freq} Hz, measured {measured_freq} Hz"
931 );
932 }
933
934 #[cfg(feature = "resample")]
935 #[test]
936 fn streaming_resampler_same_rate_is_passthrough() {
937 use crate::StreamingResampler;
942 let mut r = StreamingResampler::new(16000, 16000, 160).unwrap();
943 let input = vec![0.1, -0.2, 0.3, -0.4];
944 let mut out = Vec::new();
945 r.process(&input, &mut out).unwrap();
946 assert_eq!(out, input);
947 }
948
949 #[cfg(feature = "resample")]
950 #[test]
951 fn streaming_resampler_accessors_report_construction_args() {
952 use crate::StreamingResampler;
953 let r = StreamingResampler::new(8000, 44100, 160).unwrap();
954 assert_eq!(r.source_rate(), 8000);
955 assert_eq!(r.target_rate(), 44100);
956 assert_eq!(r.chunk_size(), 160);
957 }
958
959 #[cfg(feature = "resample")]
960 #[test]
961 fn streaming_resampler_short_input_chunked_calls() {
962 use crate::StreamingResampler;
968 let mut r = StreamingResampler::new(8000, 44100, 160).unwrap();
969 let mut out = Vec::new();
970 for _ in 0..10 {
971 let input = vec![0.0f32; 160];
972 r.process(&input, &mut out).unwrap();
973 }
974 let expected = (10 * 160 * 44100 / 8000) as i64;
977 let actual = out.len() as i64;
978 assert!(
979 (actual - expected).unsigned_abs() < 2000,
980 "expected ~{expected} samples, got {actual}"
981 );
982 }
983
984 #[cfg(feature = "resample")]
985 #[test]
986 fn streaming_resampler_buffers_across_partial_calls() {
987 use crate::StreamingResampler;
991 let input: Vec<f32> = (0..320).map(|i| (i as f32) * 0.01).collect();
992
993 let mut split_out = Vec::new();
994 let mut r1 = StreamingResampler::new(8000, 16000, 160).unwrap();
995 r1.process(&input[..50], &mut split_out).unwrap();
996 assert!(split_out.is_empty(), "no output before a full chunk");
998 r1.process(&input[50..], &mut split_out).unwrap();
999
1000 let mut whole_out = Vec::new();
1001 let mut r2 = StreamingResampler::new(8000, 16000, 160).unwrap();
1002 r2.process(&input, &mut whole_out).unwrap();
1003
1004 assert_eq!(
1005 split_out.len(),
1006 whole_out.len(),
1007 "split call must produce same number of samples as one-shot"
1008 );
1009 for (i, (a, b)) in split_out.iter().zip(whole_out.iter()).enumerate() {
1012 assert!(
1013 (a - b).abs() < 1e-6,
1014 "split vs whole differ at {i}: {a} vs {b}"
1015 );
1016 }
1017 }
1018
1019 #[cfg(feature = "resample")]
1020 #[test]
1021 fn streaming_resampler_avoids_per_frame_edge_artifacts() {
1022 use crate::StreamingResampler;
1038 let sr_in: u32 = 8000;
1039 let sr_out: u32 = 44100;
1040 let chunks = 30;
1041 let chunk_size = 160;
1042
1043 let freq = 600.0_f32;
1046 let signal: Vec<f32> = (0..chunks * chunk_size)
1047 .map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / sr_in as f32).sin())
1048 .collect();
1049
1050 let mut streaming = StreamingResampler::new(sr_in, sr_out, chunk_size).unwrap();
1052 let mut streaming_out: Vec<f32> = Vec::new();
1053 for c in 0..chunks {
1054 streaming
1055 .process(
1056 &signal[c * chunk_size..(c + 1) * chunk_size],
1057 &mut streaming_out,
1058 )
1059 .unwrap();
1060 }
1061
1062 let mut stateless_out: Vec<f32> = Vec::new();
1064 for c in 0..chunks {
1065 let chunk =
1066 AudioFrame::from_vec(signal[c * chunk_size..(c + 1) * chunk_size].to_vec(), sr_in);
1067 let resampled = chunk.resample(sr_out).unwrap();
1068 stateless_out.extend_from_slice(resampled.samples());
1069 }
1070
1071 let skip = 1500;
1074 let tail = 500;
1075
1076 let expected_max_delta = 2.0 * std::f32::consts::PI * freq / sr_out as f32;
1081 let spike_threshold = expected_max_delta * 4.0;
1082
1083 let count_spikes = |samples: &[f32], skip: usize, tail: usize| -> usize {
1084 samples[skip..samples.len() - tail]
1085 .windows(2)
1086 .filter(|w| (w[1] - w[0]).abs() > spike_threshold)
1087 .count()
1088 };
1089
1090 let streaming_spikes = count_spikes(&streaming_out, skip, tail);
1091 let stateless_spikes = count_spikes(&stateless_out, skip, tail);
1092
1093 assert!(
1096 streaming_spikes < 10,
1097 "streaming output should be smooth, found {streaming_spikes} sample-delta spikes (threshold {spike_threshold})"
1098 );
1099 assert!(
1103 stateless_spikes > streaming_spikes * 5,
1104 "stateless per-chunk should have far more spikes than streaming; got stateless={stateless_spikes}, streaming={streaming_spikes}"
1105 );
1106 }
1107
1108 #[cfg(feature = "resample")]
1109 #[test]
1110 fn streaming_resampler_rejects_zero_rate() {
1111 use crate::StreamingResampler;
1112 assert!(StreamingResampler::new(0, 16000, 160).is_err());
1113 assert!(StreamingResampler::new(16000, 0, 160).is_err());
1114 assert!(StreamingResampler::new(0, 0, 160).is_err());
1115 }
1116
1117 #[cfg(feature = "resample")]
1118 #[test]
1119 fn streaming_resampler_rejects_zero_chunk_size() {
1120 use crate::StreamingResampler;
1121 assert!(StreamingResampler::new(8000, 16000, 0).is_err());
1122 }
1123}