Skip to main content

wavekat_core/
audio.rs

1use std::borrow::Cow;
2
3/// A frame of audio samples with associated sample rate.
4///
5/// `AudioFrame` is the standard audio input type across the WaveKat ecosystem.
6/// It stores samples as f32 normalized to `[-1.0, 1.0]`, regardless of the
7/// original input format.
8///
9/// Construct via [`AudioFrame::new`], which accepts both `&[f32]` (zero-copy)
10/// and `&[i16]` (converts once) through the [`IntoSamples`] trait.
11///
12/// # Examples
13///
14/// ```
15/// use wavekat_core::AudioFrame;
16///
17/// // f32 input — zero-copy via Cow::Borrowed
18/// let samples = [0.1f32, -0.2, 0.3];
19/// let frame = AudioFrame::new(&samples, 16000);
20/// assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
21///
22/// // i16 input — normalized to f32 [-1.0, 1.0]
23/// let samples = [i16::MAX, 0, i16::MIN];
24/// let frame = AudioFrame::new(&samples, 16000);
25/// assert!((frame.samples()[0] - 1.0).abs() < 0.001);
26/// ```
27#[derive(Debug, Clone)]
28pub struct AudioFrame<'a> {
29    samples: Cow<'a, [f32]>,
30    sample_rate: u32,
31}
32
33impl<'a> AudioFrame<'a> {
34    /// Create a new audio frame from any supported sample type.
35    ///
36    /// Accepts `&[f32]` (zero-copy) or `&[i16]` (converts to normalized f32).
37    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    /// The audio samples as f32 normalized to `[-1.0, 1.0]`.
45    pub fn samples(&self) -> &[f32] {
46        &self.samples
47    }
48
49    /// Sample rate in Hz (e.g. 16000).
50    pub fn sample_rate(&self) -> u32 {
51        self.sample_rate
52    }
53
54    /// Number of samples in the frame.
55    pub fn len(&self) -> usize {
56        self.samples.len()
57    }
58
59    /// Returns `true` if the frame contains no samples.
60    pub fn is_empty(&self) -> bool {
61        self.samples.is_empty()
62    }
63
64    /// Duration of this frame in seconds.
65    pub fn duration_secs(&self) -> f64 {
66        self.samples.len() as f64 / self.sample_rate as f64
67    }
68
69    /// Consume the frame and return the owned samples.
70    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    /// Construct an owned frame directly from a `Vec<f32>`.
80    ///
81    /// Zero-copy — wraps the vec as `Cow::Owned` without cloning.
82    /// Intended for audio producers (TTS, ASR) that generate owned data.
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// use wavekat_core::AudioFrame;
88    ///
89    /// let samples = vec![0.5f32, -0.5, 0.3];
90    /// let frame = AudioFrame::from_vec(samples, 24000);
91    /// assert_eq!(frame.sample_rate(), 24000);
92    /// assert_eq!(frame.len(), 3);
93    /// ```
94    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/// Trait for types that can be converted into audio samples.
103///
104/// Implemented for `&[f32]` (zero-copy) and `&[i16]` (normalized conversion).
105pub trait IntoSamples<'a> {
106    /// Convert into f32 samples normalized to `[-1.0, 1.0]`.
107    fn into_samples(self) -> Cow<'a, [f32]>;
108}
109
110impl<'a> IntoSamples<'a> for &'a [f32] {
111    #[inline]
112    fn into_samples(self) -> Cow<'a, [f32]> {
113        Cow::Borrowed(self)
114    }
115}
116
117impl<'a> IntoSamples<'a> for &'a Vec<f32> {
118    #[inline]
119    fn into_samples(self) -> Cow<'a, [f32]> {
120        Cow::Borrowed(self.as_slice())
121    }
122}
123
124impl<'a, const N: usize> IntoSamples<'a> for &'a [f32; N] {
125    #[inline]
126    fn into_samples(self) -> Cow<'a, [f32]> {
127        Cow::Borrowed(self.as_slice())
128    }
129}
130
131impl<'a> IntoSamples<'a> for &'a [i16] {
132    #[inline]
133    fn into_samples(self) -> Cow<'a, [f32]> {
134        Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
135    }
136}
137
138impl<'a> IntoSamples<'a> for &'a Vec<i16> {
139    #[inline]
140    fn into_samples(self) -> Cow<'a, [f32]> {
141        Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
142    }
143}
144
145impl<'a, const N: usize> IntoSamples<'a> for &'a [i16; N] {
146    #[inline]
147    fn into_samples(self) -> Cow<'a, [f32]> {
148        Cow::Owned(self.iter().map(|&s| s as f32 / 32768.0).collect())
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn f32_is_zero_copy() {
158        let samples = vec![0.1f32, -0.2, 0.3];
159        let frame = AudioFrame::new(samples.as_slice(), 16000);
160        // Cow::Borrowed — the pointer should be the same
161        assert!(matches!(frame.samples, Cow::Borrowed(_)));
162        assert_eq!(frame.samples(), &[0.1, -0.2, 0.3]);
163    }
164
165    #[test]
166    fn i16_normalizes_to_f32() {
167        let samples: Vec<i16> = vec![0, 16384, -16384, i16::MAX, i16::MIN];
168        let frame = AudioFrame::new(samples.as_slice(), 16000);
169        assert!(matches!(frame.samples, Cow::Owned(_)));
170
171        let s = frame.samples();
172        assert!((s[0] - 0.0).abs() < f32::EPSILON);
173        assert!((s[1] - 0.5).abs() < 0.001);
174        assert!((s[2] - -0.5).abs() < 0.001);
175        assert!((s[3] - (i16::MAX as f32 / 32768.0)).abs() < f32::EPSILON);
176        assert!((s[4] - -1.0).abs() < f32::EPSILON);
177    }
178
179    #[test]
180    fn metadata() {
181        let samples = vec![0.0f32; 160];
182        let frame = AudioFrame::new(samples.as_slice(), 16000);
183        assert_eq!(frame.sample_rate(), 16000);
184        assert_eq!(frame.len(), 160);
185        assert!(!frame.is_empty());
186        assert!((frame.duration_secs() - 0.01).abs() < 1e-9);
187    }
188
189    #[test]
190    fn empty_frame() {
191        let samples: &[f32] = &[];
192        let frame = AudioFrame::new(samples, 16000);
193        assert!(frame.is_empty());
194        assert_eq!(frame.len(), 0);
195    }
196
197    #[test]
198    fn into_owned() {
199        let samples = vec![0.5f32, -0.5];
200        let frame = AudioFrame::new(samples.as_slice(), 16000);
201        let owned: AudioFrame<'static> = frame.into_owned();
202        assert_eq!(owned.samples(), &[0.5, -0.5]);
203        assert_eq!(owned.sample_rate(), 16000);
204    }
205
206    #[test]
207    fn from_vec_is_zero_copy() {
208        let samples = vec![0.5f32, -0.5];
209        let ptr = samples.as_ptr();
210        let frame = AudioFrame::from_vec(samples, 24000);
211        assert_eq!(frame.samples().as_ptr(), ptr);
212        assert_eq!(frame.sample_rate(), 24000);
213    }
214}