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 = "wav")]
103impl AudioFrame<'_> {
104 pub fn write_wav(&self, path: impl AsRef<std::path::Path>) -> Result<(), crate::CoreError> {
117 let spec = hound::WavSpec {
118 channels: 1,
119 sample_rate: self.sample_rate,
120 bits_per_sample: 32,
121 sample_format: hound::SampleFormat::Float,
122 };
123 let mut writer = hound::WavWriter::create(path, spec)?;
124 for &sample in self.samples() {
125 writer.write_sample(sample)?;
126 }
127 writer.finalize()?;
128 Ok(())
129 }
130}
131
132#[cfg(feature = "wav")]
133impl AudioFrame<'static> {
134 pub fn from_wav(path: impl AsRef<std::path::Path>) -> Result<Self, crate::CoreError> {
148 let mut reader = hound::WavReader::open(path)?;
149 let spec = reader.spec();
150 let sample_rate = spec.sample_rate;
151 let samples: Vec<f32> = match spec.sample_format {
152 hound::SampleFormat::Float => reader.samples::<f32>().collect::<Result<_, _>>()?,
153 hound::SampleFormat::Int => reader
154 .samples::<i16>()
155 .map(|s| s.map(|v| v as f32 / 32768.0))
156 .collect::<Result<_, _>>()?,
157 };
158 Ok(AudioFrame::from_vec(samples, sample_rate))
159 }
160}
161
162pub trait IntoSamples<'a> {
166 fn into_samples(self) -> Cow<'a, [f32]>;
168}
169
170impl<'a> IntoSamples<'a> for &'a [f32] {
171 #[inline]
172 fn into_samples(self) -> Cow<'a, [f32]> {
173 Cow::Borrowed(self)
174 }
175}
176
177impl<'a> IntoSamples<'a> for &'a Vec<f32> {
178 #[inline]
179 fn into_samples(self) -> Cow<'a, [f32]> {
180 Cow::Borrowed(self.as_slice())
181 }
182}
183
184impl<'a, const N: usize> IntoSamples<'a> for &'a [f32; N] {
185 #[inline]
186 fn into_samples(self) -> Cow<'a, [f32]> {
187 Cow::Borrowed(self.as_slice())
188 }
189}
190
191impl<'a> IntoSamples<'a> for &'a [i16] {
192 #[inline]
193 fn into_samples(self) -> Cow<'a, [f32]> {
194 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
195 }
196}
197
198impl<'a> IntoSamples<'a> for &'a Vec<i16> {
199 #[inline]
200 fn into_samples(self) -> Cow<'a, [f32]> {
201 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
202 }
203}
204
205impl<'a, const N: usize> IntoSamples<'a> for &'a [i16; N] {
206 #[inline]
207 fn into_samples(self) -> Cow<'a, [f32]> {
208 Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn f32_is_zero_copy() {
218 let samples = vec![0.1f32, -0.2, 0.3];
219 let frame = AudioFrame::new(samples.as_slice(), 16000);
220 assert!(matches!(frame.samples, Cow::Borrowed(_)));
222 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
223 }
224
225 #[test]
226 fn i16_normalizes_to_f32() {
227 let samples: Vec<i16> = vec![0, 16384, -16384, i16::MAX, i16::MIN];
228 let frame = AudioFrame::new(samples.as_slice(), 16000);
229 assert!(matches!(frame.samples, Cow::Owned(_)));
230
231 let s = frame.samples();
232 assert!((s[0] - 0.0).abs() < f32::EPSILON);
233 assert!((s[1] - 0.5).abs() < 0.001);
234 assert!((s[2] - -0.5).abs() < 0.001);
235 assert!((s[3] - (i16::MAX as f32 / 32768.0)).abs() < f32::EPSILON);
236 assert!((s[4] - -1.0).abs() < f32::EPSILON);
237 }
238
239 #[test]
240 fn metadata() {
241 let samples = vec![0.0f32; 160];
242 let frame = AudioFrame::new(samples.as_slice(), 16000);
243 assert_eq!(frame.sample_rate(), 16000);
244 assert_eq!(frame.len(), 160);
245 assert!(!frame.is_empty());
246 assert!((frame.duration_secs() - 0.01).abs() < 1e-9);
247 }
248
249 #[test]
250 fn empty_frame() {
251 let samples: &[f32] = &[];
252 let frame = AudioFrame::new(samples, 16000);
253 assert!(frame.is_empty());
254 assert_eq!(frame.len(), 0);
255 }
256
257 #[test]
258 fn into_owned() {
259 let samples = vec![0.5f32, -0.5];
260 let frame = AudioFrame::new(samples.as_slice(), 16000);
261 let owned: AudioFrame<'static> = frame.into_owned();
262 assert_eq!(owned.samples(), &[0.5, -0.5]);
263 assert_eq!(owned.sample_rate(), 16000);
264 }
265
266 #[cfg(feature = "wav")]
267 #[test]
268 fn wav_read_i16() {
269 let path = std::env::temp_dir().join("wavekat_test_i16.wav");
271 let spec = hound::WavSpec {
272 channels: 1,
273 sample_rate: 16000,
274 bits_per_sample: 16,
275 sample_format: hound::SampleFormat::Int,
276 };
277 let i16_samples: &[i16] = &[0, i16::MAX, i16::MIN, 16384];
278 let mut writer = hound::WavWriter::create(&path, spec).unwrap();
279 for &s in i16_samples {
280 writer.write_sample(s).unwrap();
281 }
282 writer.finalize().unwrap();
283
284 let frame = AudioFrame::from_wav(&path).unwrap();
285 assert_eq!(frame.sample_rate(), 16000);
286 assert_eq!(frame.len(), 4);
287 let s = frame.samples();
288 assert!((s[0] - 0.0).abs() < 1e-6);
289 assert!((s[1] - (i16::MAX as f32 / 32768.0)).abs() < 1e-6);
290 assert!((s[2] - -1.0).abs() < 1e-6);
291 assert!((s[3] - 0.5).abs() < 1e-4);
292 }
293
294 #[cfg(feature = "wav")]
295 #[test]
296 fn wav_round_trip() {
297 let original = AudioFrame::from_vec(vec![0.5f32, -0.5, 0.0, 1.0], 16000);
298 let path = std::env::temp_dir().join("wavekat_test.wav");
299 original.write_wav(&path).unwrap();
300 let loaded = AudioFrame::from_wav(&path).unwrap();
301 assert_eq!(loaded.sample_rate(), 16000);
302 for (a, b) in original.samples().iter().zip(loaded.samples()) {
303 assert!((a - b).abs() < 1e-6, "sample mismatch: {a} vs {b}");
304 }
305 }
306
307 #[test]
308 fn from_vec_is_zero_copy() {
309 let samples = vec![0.5f32, -0.5];
310 let ptr = samples.as_ptr();
311 let frame = AudioFrame::from_vec(samples, 24000);
312 assert_eq!(frame.samples().as_ptr(), ptr);
313 assert_eq!(frame.sample_rate(), 24000);
314 }
315
316 #[test]
317 fn into_samples_vec_f32() {
318 let samples = vec![0.1f32, -0.2, 0.3];
319 let frame = AudioFrame::new(&samples, 16000);
320 assert!(matches!(frame.samples, Cow::Borrowed(_)));
321 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
322 }
323
324 #[test]
325 fn into_samples_array_f32() {
326 let samples = [0.1f32, -0.2, 0.3];
327 let frame = AudioFrame::new(&samples, 16000);
328 assert!(matches!(frame.samples, Cow::Borrowed(_)));
329 assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
330 }
331
332 #[test]
333 fn into_samples_vec_i16() {
334 let samples: Vec<i16> = vec![0, 16384, i16::MIN];
335 let frame = AudioFrame::new(&samples, 16000);
336 assert!(matches!(frame.samples, Cow::Owned(_)));
337 let s = frame.samples();
338 assert!((s[0] - 0.0).abs() < f32::EPSILON);
339 assert!((s[1] - 0.5).abs() < 0.001);
340 assert!((s[2] - -1.0).abs() < f32::EPSILON);
341 }
342
343 #[test]
344 fn into_samples_array_i16() {
345 let samples: [i16; 3] = [0, 16384, i16::MIN];
346 let frame = AudioFrame::new(&samples, 16000);
347 assert!(matches!(frame.samples, Cow::Owned(_)));
348 let s = frame.samples();
349 assert!((s[0] - 0.0).abs() < f32::EPSILON);
350 assert!((s[1] - 0.5).abs() < 0.001);
351 assert!((s[2] - -1.0).abs() < f32::EPSILON);
352 }
353}