bbx_dsp/
frame.rs

1//! Audio frame type for inter-thread communication.
2
3use bbx_core::StackVec;
4
5/// Maximum number of interleaved samples per frame.
6/// Supports up to 512 samples per channel for stereo (1024 total).
7pub const MAX_FRAME_SAMPLES: usize = 1024;
8
9/// A frame of audio data for visualization or inter-thread communication.
10///
11/// Uses stack-allocated storage for realtime-safe operation on audio threads.
12#[derive(Clone)]
13pub struct Frame {
14    /// Interleaved audio samples (stack-allocated).
15    pub samples: StackVec<f32, MAX_FRAME_SAMPLES>,
16    /// Sample rate in Hz.
17    pub sample_rate: u32,
18    /// Number of audio channels.
19    pub num_channels: usize,
20}
21
22impl Frame {
23    /// Create a new frame from a slice of samples.
24    ///
25    /// # Panics
26    ///
27    /// Panics if `samples.len() > MAX_FRAME_SAMPLES`.
28    pub fn new(samples: &[f32], sample_rate: u32, num_channels: usize) -> Self {
29        assert!(
30            samples.len() <= MAX_FRAME_SAMPLES,
31            "Frame exceeds maximum size of {MAX_FRAME_SAMPLES} samples"
32        );
33
34        let mut stack_samples = StackVec::new();
35        for &sample in samples {
36            stack_samples.push_unchecked(sample);
37        }
38
39        Self {
40            samples: stack_samples,
41            sample_rate,
42            num_channels,
43        }
44    }
45
46    /// Returns an iterator over samples for a specific channel (de-interleaved).
47    ///
48    /// Returns `None` if the channel index is out of bounds.
49    pub fn channel_samples(&self, channel: usize) -> Option<impl Iterator<Item = f32> + '_> {
50        if channel >= self.num_channels || self.num_channels == 0 {
51            return None;
52        }
53        Some(
54            self.samples
55                .as_slice()
56                .iter()
57                .skip(channel)
58                .step_by(self.num_channels)
59                .copied(),
60        )
61    }
62
63    /// Get the number of samples per channel.
64    pub fn samples_per_channel(&self) -> usize {
65        if self.num_channels == 0 {
66            0
67        } else {
68            self.samples.len() / self.num_channels
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_frame_new() {
79        let samples = [0.5f32, -0.5, 0.25, -0.25];
80        let frame = Frame::new(&samples, 44100, 2);
81
82        assert_eq!(frame.samples.len(), 4);
83        assert_eq!(frame.sample_rate, 44100);
84        assert_eq!(frame.num_channels, 2);
85    }
86
87    #[test]
88    fn test_channel_samples() {
89        let samples = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
90        let frame = Frame::new(&samples, 44100, 2);
91
92        let left: Vec<f32> = frame.channel_samples(0).unwrap().collect();
93        let right: Vec<f32> = frame.channel_samples(1).unwrap().collect();
94
95        assert_eq!(left, vec![1.0, 3.0, 5.0]);
96        assert_eq!(right, vec![2.0, 4.0, 6.0]);
97    }
98
99    #[test]
100    fn test_channel_samples_invalid() {
101        let samples = [1.0f32, 2.0];
102        let frame = Frame::new(&samples, 44100, 2);
103
104        assert!(frame.channel_samples(2).is_none());
105        assert!(frame.channel_samples(10).is_none());
106    }
107
108    #[test]
109    fn test_samples_per_channel() {
110        let samples = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
111        let frame = Frame::new(&samples, 44100, 2);
112
113        assert_eq!(frame.samples_per_channel(), 3);
114    }
115
116    #[test]
117    fn test_samples_per_channel_zero_channels() {
118        let frame = Frame {
119            samples: {
120                let mut s = StackVec::new();
121                s.push_unchecked(1.0);
122                s.push_unchecked(2.0);
123                s
124            },
125            sample_rate: 44100,
126            num_channels: 0,
127        };
128
129        assert_eq!(frame.samples_per_channel(), 0);
130    }
131}