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::{
129 Async, FixedAsync, Resampler, SincInterpolationParameters, SincInterpolationType,
130 WindowFunction,
131 };
132
133 if self.sample_rate == target_rate {
134 return Ok(self.clone().into_owned());
135 }
136
137 if self.is_empty() {
138 return Ok(AudioFrame::from_vec(Vec::new(), target_rate));
139 }
140
141 let ratio = target_rate as f64 / self.sample_rate as f64;
142 let nbr_input_frames = self.samples.len();
143
144 let params = SincInterpolationParameters {
145 sinc_len: 256,
146 f_cutoff: 0.95,
147 interpolation: SincInterpolationType::Cubic,
148 oversampling_factor: 128,
149 window: WindowFunction::BlackmanHarris2,
150 };
151
152 let mut resampler = Async::<f32>::new_sinc(ratio, 1.0, ¶ms, 1024, 1, FixedAsync::Input)
153 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
154
155 let out_len = (nbr_input_frames as f64 * ratio) as usize + 1024;
157 let mut outdata = vec![0.0f32; out_len];
158
159 let input_adapter = InterleavedSlice::new(self.samples.as_ref(), 1, nbr_input_frames)
160 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
161 let mut output_adapter = InterleavedSlice::new_mut(&mut outdata, 1, out_len)
162 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
163
164 let (_in_consumed, out_produced) = resampler
165 .process_all_into_buffer(&input_adapter, &mut output_adapter, nbr_input_frames, None)
166 .map_err(|e| crate::CoreError::Audio(e.to_string()))?;
167
168 outdata.truncate(out_produced);
169 Ok(AudioFrame::from_vec(outdata, target_rate))
170 }
171}
172
173#[cfg(feature = "wav")]
174impl AudioFrame<'_> {
175 pub fn write_wav(&self, path: impl AsRef<std::path::Path>) -> Result<(), crate::CoreError> {
188 let spec = hound::WavSpec {
189 channels: 1,
190 sample_rate: self.sample_rate,
191 bits_per_sample: 32,
192 sample_format: hound::SampleFormat::Float,
193 };
194 let mut writer = hound::WavWriter::create(path, spec)?;
195 for &sample in self.samples() {
196 writer.write_sample(sample)?;
197 }
198 writer.finalize()?;
199 Ok(())
200 }
201}
202
203#[cfg(feature = "wav")]
204impl AudioFrame<'static> {
205 pub fn from_wav(path: impl AsRef<std::path::Path>) -> Result<Self, crate::CoreError> {
219 let mut reader = hound::WavReader::open(path)?;
220 let spec = reader.spec();
221 let sample_rate = spec.sample_rate;
222 let samples: Vec<f32> = match spec.sample_format {
223 hound::SampleFormat::Float => reader.samples::<f32>().collect::<Result<_, _>>()?,
224 hound::SampleFormat::Int => reader
225 .samples::<i16>()
226 .map(|s| s.map(|v| v as f32 / 32768.0))
227 .collect::<Result<_, _>>()?,
228 };
229 Ok(AudioFrame::from_vec(samples, sample_rate))
230 }
231}
232
233pub trait IntoSamples<'a> {
237 fn into_samples(self) -> Cow<'a, [f32]>;
239}
240
241impl<'a> IntoSamples<'a> for &'a [f32] {
242 #[inline]
243 fn into_samples(self) -> Cow<'a, [f32]> {
244 Cow::Borrowed(self)
245 }
246}
247
248impl<'a> IntoSamples<'a> for &'a Vec<f32> {
249 #[inline]
250 fn into_samples(self) -> Cow<'a, [f32]> {
251 Cow::Borrowed(self.as_slice())
252 }
253}
254
255impl<'a, const N: usize> IntoSamples<'a> for &'a [f32; N] {
256 #[inline]
257 fn into_samples(self) -> Cow<'a, [f32]> {
258 Cow::Borrowed(self.as_slice())
259 }
260}
261
262impl<'a> IntoSamples<'a> for &'a [i16] {
263 #[inline]
264 fn into_samples(self) -> Cow<'a, [f32]> {
265 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
266 }
267}
268
269impl<'a> IntoSamples<'a> for &'a Vec<i16> {
270 #[inline]
271 fn into_samples(self) -> Cow<'a, [f32]> {
272 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
273 }
274}
275
276impl<'a, const N: usize> IntoSamples<'a> for &'a [i16; N] {
277 #[inline]
278 fn into_samples(self) -> Cow<'a, [f32]> {
279 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn f32_is_zero_copy() {
289 let samples = vec![0.1f32, -0.2, 0.3];
290 let frame = AudioFrame::new(samples.as_slice(), 16000);
291 assert!(matches!(frame.samples, Cow::Borrowed(_)));
293 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
294 }
295
296 #[test]
297 fn i16_normalizes_to_f32() {
298 let samples: Vec<i16> = vec![0, 16384, -16384, i16::MAX, i16::MIN];
299 let frame = AudioFrame::new(samples.as_slice(), 16000);
300 assert!(matches!(frame.samples, Cow::Owned(_)));
301
302 let s = frame.samples();
303 assert!((s[0] - 0.0).abs() < f32::EPSILON);
304 assert!((s[1] - 0.5).abs() < 0.001);
305 assert!((s[2] - -0.5).abs() < 0.001);
306 assert!((s[3] - (i16::MAX as f32 / 32768.0)).abs() < f32::EPSILON);
307 assert!((s[4] - -1.0).abs() < f32::EPSILON);
308 }
309
310 #[test]
311 fn metadata() {
312 let samples = vec![0.0f32; 160];
313 let frame = AudioFrame::new(samples.as_slice(), 16000);
314 assert_eq!(frame.sample_rate(), 16000);
315 assert_eq!(frame.len(), 160);
316 assert!(!frame.is_empty());
317 assert!((frame.duration_secs() - 0.01).abs() < 1e-9);
318 }
319
320 #[test]
321 fn empty_frame() {
322 let samples: &[f32] = &[];
323 let frame = AudioFrame::new(samples, 16000);
324 assert!(frame.is_empty());
325 assert_eq!(frame.len(), 0);
326 }
327
328 #[test]
329 fn into_owned() {
330 let samples = vec![0.5f32, -0.5];
331 let frame = AudioFrame::new(samples.as_slice(), 16000);
332 let owned: AudioFrame<'static> = frame.into_owned();
333 assert_eq!(owned.samples(), &[0.5, -0.5]);
334 assert_eq!(owned.sample_rate(), 16000);
335 }
336
337 #[cfg(feature = "wav")]
338 #[test]
339 fn wav_read_i16() {
340 let path = std::env::temp_dir().join("wavekat_test_i16.wav");
342 let spec = hound::WavSpec {
343 channels: 1,
344 sample_rate: 16000,
345 bits_per_sample: 16,
346 sample_format: hound::SampleFormat::Int,
347 };
348 let i16_samples: &[i16] = &[0, i16::MAX, i16::MIN, 16384];
349 let mut writer = hound::WavWriter::create(&path, spec).unwrap();
350 for &s in i16_samples {
351 writer.write_sample(s).unwrap();
352 }
353 writer.finalize().unwrap();
354
355 let frame = AudioFrame::from_wav(&path).unwrap();
356 assert_eq!(frame.sample_rate(), 16000);
357 assert_eq!(frame.len(), 4);
358 let s = frame.samples();
359 assert!((s[0] - 0.0).abs() < 1e-6);
360 assert!((s[1] - (i16::MAX as f32 / 32768.0)).abs() < 1e-6);
361 assert!((s[2] - -1.0).abs() < 1e-6);
362 assert!((s[3] - 0.5).abs() < 1e-4);
363 }
364
365 #[cfg(feature = "wav")]
366 #[test]
367 fn wav_round_trip() {
368 let original = AudioFrame::from_vec(vec![0.5f32, -0.5, 0.0, 1.0], 16000);
369 let path = std::env::temp_dir().join("wavekat_test.wav");
370 original.write_wav(&path).unwrap();
371 let loaded = AudioFrame::from_wav(&path).unwrap();
372 assert_eq!(loaded.sample_rate(), 16000);
373 for (a, b) in original.samples().iter().zip(loaded.samples()) {
374 assert!((a - b).abs() < 1e-6, "sample mismatch: {a} vs {b}");
375 }
376 }
377
378 #[test]
379 fn from_vec_is_zero_copy() {
380 let samples = vec![0.5f32, -0.5];
381 let ptr = samples.as_ptr();
382 let frame = AudioFrame::from_vec(samples, 24000);
383 assert_eq!(frame.samples().as_ptr(), ptr);
384 assert_eq!(frame.sample_rate(), 24000);
385 }
386
387 #[test]
388 fn into_samples_vec_f32() {
389 let samples = vec![0.1f32, -0.2, 0.3];
390 let frame = AudioFrame::new(&samples, 16000);
391 assert!(matches!(frame.samples, Cow::Borrowed(_)));
392 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
393 }
394
395 #[test]
396 fn into_samples_array_f32() {
397 let samples = [0.1f32, -0.2, 0.3];
398 let frame = AudioFrame::new(&samples, 16000);
399 assert!(matches!(frame.samples, Cow::Borrowed(_)));
400 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
401 }
402
403 #[test]
404 fn into_samples_vec_i16() {
405 let samples: Vec<i16> = vec![0, 16384, i16::MIN];
406 let frame = AudioFrame::new(&samples, 16000);
407 assert!(matches!(frame.samples, Cow::Owned(_)));
408 let s = frame.samples();
409 assert!((s[0] - 0.0).abs() < f32::EPSILON);
410 assert!((s[1] - 0.5).abs() < 0.001);
411 assert!((s[2] - -1.0).abs() < f32::EPSILON);
412 }
413
414 #[test]
415 fn into_samples_array_i16() {
416 let samples: [i16; 3] = [0, 16384, i16::MIN];
417 let frame = AudioFrame::new(&samples, 16000);
418 assert!(matches!(frame.samples, Cow::Owned(_)));
419 let s = frame.samples();
420 assert!((s[0] - 0.0).abs() < f32::EPSILON);
421 assert!((s[1] - 0.5).abs() < 0.001);
422 assert!((s[2] - -1.0).abs() < f32::EPSILON);
423 }
424
425 #[cfg(feature = "resample")]
426 #[test]
427 fn resample_noop_same_rate() {
428 let samples = vec![0.1f32, -0.2, 0.3, 0.4, 0.5];
429 let frame = AudioFrame::from_vec(samples.clone(), 16000);
430 let resampled = frame.resample(16000).unwrap();
431 assert_eq!(resampled.sample_rate(), 16000);
432 assert_eq!(resampled.samples(), &samples[..]);
433 }
434
435 #[cfg(feature = "resample")]
436 #[test]
437 fn resample_empty_frame() {
438 let frame = AudioFrame::from_vec(Vec::new(), 44100);
439 let resampled = frame.resample(16000).unwrap();
440 assert_eq!(resampled.sample_rate(), 16000);
441 assert!(resampled.is_empty());
442 }
443
444 #[cfg(feature = "resample")]
445 #[test]
446 fn resample_downsample() {
447 let frame = AudioFrame::from_vec(vec![0.0f32; 48000], 48000);
449 let resampled = frame.resample(16000).unwrap();
450 assert_eq!(resampled.sample_rate(), 16000);
451 let expected = 16000;
453 let tolerance = 50;
454 assert!(
455 (resampled.len() as i64 - expected as i64).unsigned_abs() < tolerance,
456 "expected ~{expected} samples, got {}",
457 resampled.len()
458 );
459 }
460
461 #[cfg(feature = "resample")]
462 #[test]
463 fn resample_upsample() {
464 let frame = AudioFrame::from_vec(vec![0.0f32; 16000], 16000);
466 let resampled = frame.resample(24000).unwrap();
467 assert_eq!(resampled.sample_rate(), 24000);
468 let expected = 24000;
469 let tolerance = 50;
470 assert!(
471 (resampled.len() as i64 - expected as i64).unsigned_abs() < tolerance,
472 "expected ~{expected} samples, got {}",
473 resampled.len()
474 );
475 }
476
477 #[cfg(feature = "resample")]
478 #[test]
479 fn resample_preserves_sine_frequency() {
480 let sr_in: u32 = 44100;
484 let sr_out: u32 = 16000;
485 let duration_secs = 1.0;
486 let freq = 440.0;
487 let n = (sr_in as f64 * duration_secs) as usize;
488 let samples: Vec<f32> = (0..n)
489 .map(|i| (2.0 * std::f64::consts::PI * freq * i as f64 / sr_in as f64).sin() as f32)
490 .collect();
491
492 let frame = AudioFrame::from_vec(samples, sr_in);
493 let resampled = frame.resample(sr_out).unwrap();
494
495 let s = resampled.samples();
497 let crossings: usize = s
498 .windows(2)
499 .filter(|w| w[0].signum() != w[1].signum())
500 .count();
501 let measured_freq = crossings as f64 / (2.0 * duration_secs);
503 assert!(
504 (measured_freq - freq).abs() < 5.0,
505 "expected ~{freq} Hz, measured {measured_freq} Hz"
506 );
507 }
508}