Skip to main content

ez_ffmpeg/core/frame_export/
collected.rs

1//! The self-describing [`CollectedAudio`] output type.
2
3/// One whole audio extraction, collected: owned, interleaved 32-bit float
4/// samples plus the sample rate, channel count, and channel layout that
5/// describe them.
6///
7/// Returned by [`collect_audio`](super::SampleExtractor::collect_audio). The
8/// buffer is exactly what
9/// [`collect_samples`](super::SampleExtractor::collect_samples) returns for
10/// the same run — packed native-endian `f32`, channels interleaved
11/// (`[L0, R0, L1, R1, …]` for stereo) — with the shape attached, so a WAV
12/// writer, resampler, or model can consume the buffer without out-of-band
13/// knowledge of the source.
14pub struct CollectedAudio {
15    samples: Vec<f32>,
16    sample_rate: u32,
17    channels: u16,
18    channel_layout: String,
19}
20
21impl CollectedAudio {
22    /// Builds a collected result from an already-flattened sample buffer.
23    /// Crate-internal: `collect_audio` guarantees the buffer holds whole
24    /// interleaved frames, with zeroed metadata only for an empty run.
25    pub(crate) fn new(
26        samples: Vec<f32>,
27        sample_rate: u32,
28        channels: u16,
29        channel_layout: String,
30    ) -> Self {
31        // `%` (not `is_multiple_of`) keeps this MSRV-1.80 safe, matching the
32        // sibling chunk type; `usize::is_multiple_of` is only stable since 1.87.
33        #[allow(clippy::manual_is_multiple_of)]
34        {
35            debug_assert!(
36                if channels == 0 {
37                    samples.is_empty()
38                } else {
39                    samples.len() % channels as usize == 0
40                },
41                "CollectedAudio buffer must hold whole interleaved frames"
42            );
43        }
44        Self {
45            samples,
46            sample_rate,
47            channels,
48            channel_layout,
49        }
50    }
51
52    /// Samples per second. `0` only when the run delivered no samples at all.
53    pub fn sample_rate(&self) -> u32 {
54        self.sample_rate
55    }
56
57    /// Number of interleaved channels (1 for mono, 2 for stereo). `0` only
58    /// when the run delivered no samples at all.
59    pub fn channels(&self) -> u16 {
60        self.channels
61    }
62
63    /// FFmpeg's textual channel-layout description (e.g. `"mono"`,
64    /// `"stereo"`, `"5.1"`) — the same vocabulary as
65    /// [`AudioChunk::channel_layout`](super::AudioChunk::channel_layout).
66    /// Above two channels this distinguishes layouts a bare count cannot
67    /// (6 channels may be `"5.1"` or `"6.0"`). Empty when the run delivered
68    /// no samples, or when FFmpeg cannot describe the layout.
69    pub fn channel_layout(&self) -> &str {
70        &self.channel_layout
71    }
72
73    /// The interleaved `f32` samples. Length is `frames * channels`.
74    pub fn as_slice(&self) -> &[f32] {
75        &self.samples
76    }
77
78    /// Consumes the collected audio and returns the owned interleaved buffer
79    /// (no copy), discarding the metadata.
80    pub fn into_vec(self) -> Vec<f32> {
81        self.samples
82    }
83}
84
85impl std::fmt::Debug for CollectedAudio {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("CollectedAudio")
88            .field("sample_rate", &self.sample_rate)
89            .field("channels", &self.channels)
90            .field("channel_layout", &self.channel_layout)
91            .field("samples", &self.samples.len())
92            .finish()
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn accessors_round_trip_the_shape() {
102        let a = CollectedAudio::new(vec![0.25; 12], 48_000, 6, "5.1".to_string());
103        assert_eq!(a.sample_rate(), 48_000);
104        assert_eq!(a.channels(), 6);
105        assert_eq!(a.channel_layout(), "5.1");
106        assert_eq!(a.as_slice().len(), 12);
107        assert_eq!(a.into_vec(), vec![0.25; 12]);
108    }
109
110    #[test]
111    fn empty_run_is_fully_zeroed() {
112        let a = CollectedAudio::new(Vec::new(), 0, 0, String::new());
113        assert!(a.as_slice().is_empty());
114        assert_eq!(a.sample_rate(), 0);
115        assert_eq!(a.channels(), 0);
116        assert_eq!(a.channel_layout(), "");
117    }
118}