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}