Skip to main content

ez_ffmpeg/core/frame_export/
chunk.rs

1//! The owned, interleaved [`AudioChunk`] output type.
2
3/// One exported block of decoded audio: owned, interleaved 32-bit float samples
4/// plus metadata.
5///
6/// Samples are packed native-endian `f32`, interleaved:
7/// [`as_slice`](AudioChunk::as_slice)`.len() == frames * channels`, channels
8/// interleaved (`[L0, R0, L1, R1, …]` for stereo) — the buffer layout
9/// whisper / candle / ort pipelines consume, at whatever sample rate and
10/// channel shape this chunk reports (normalize via the extractor's
11/// `sample_rate`/`channels` options when a model needs a fixed shape).
12/// One chunk corresponds to one filtered `AVFrame`; the number of frames per
13/// chunk is not contractual (typically ~1024) and must not be relied upon.
14pub struct AudioChunk {
15    pts_us: Option<i64>,
16    index: u64,
17    sample_rate: u32,
18    channels: u16,
19    channel_layout: String,
20    data: Vec<f32>,
21}
22
23impl AudioChunk {
24    /// Builds a chunk from an already-interleaved sample buffer. Crate-internal:
25    /// the sink guarantees `data.len()` is a whole multiple of `channels` and
26    /// that `channel_layout` describes the exported frame's layout.
27    pub(crate) fn new(
28        pts_us: Option<i64>,
29        index: u64,
30        sample_rate: u32,
31        channels: u16,
32        channel_layout: String,
33        data: Vec<f32>,
34    ) -> Self {
35        // `%` (not `is_multiple_of`) keeps this MSRV-1.80 safe, matching the
36        // sibling video sink; `usize::is_multiple_of` is only stable since 1.87.
37        #[allow(clippy::manual_is_multiple_of)]
38        {
39            debug_assert!(
40                channels != 0 && data.len() % channels as usize == 0,
41                "AudioChunk buffer must hold whole interleaved frames"
42            );
43        }
44        Self {
45            pts_us,
46            index,
47            sample_rate,
48            channels,
49            channel_layout,
50            data,
51        }
52    }
53
54    /// Presentation time in microseconds, passed through from the source
55    /// frame and normalized to the start of the extraction window (the stream
56    /// start when no `start_time_us` was set). `None` when the frame carried
57    /// no usable timestamp.
58    pub fn pts_us(&self) -> Option<i64> {
59        self.pts_us
60    }
61
62    /// 0-based export index (counts delivered chunks in order).
63    pub fn index(&self) -> u64 {
64        self.index
65    }
66
67    /// Samples per second.
68    pub fn sample_rate(&self) -> u32 {
69        self.sample_rate
70    }
71
72    /// Number of interleaved channels (1 for mono, 2 for stereo).
73    pub fn channels(&self) -> u16 {
74        self.channels
75    }
76
77    /// FFmpeg's textual channel-layout description of this chunk (e.g.
78    /// `"mono"`, `"stereo"`, `"5.1"`), read from the exported frame: the
79    /// source layout under the default passthrough, or the converted layout
80    /// when [`channels`](super::SampleExtractor::channels) requested one.
81    /// Above two channels this distinguishes layouts a bare count cannot
82    /// (6 channels may be `"5.1"` or `"6.0"`). Empty when FFmpeg cannot
83    /// describe the layout.
84    pub fn channel_layout(&self) -> &str {
85        &self.channel_layout
86    }
87
88    /// The interleaved `f32` samples. Length is `frames * channels`.
89    pub fn as_slice(&self) -> &[f32] {
90        &self.data
91    }
92
93    /// Consumes the chunk and returns the owned interleaved buffer (no copy).
94    pub fn into_vec(self) -> Vec<f32> {
95        self.data
96    }
97}
98
99impl std::fmt::Debug for AudioChunk {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_struct("AudioChunk")
102            .field("pts_us", &self.pts_us)
103            .field("index", &self.index)
104            .field("sample_rate", &self.sample_rate)
105            .field("channels", &self.channels)
106            .field("channel_layout", &self.channel_layout)
107            .field("samples", &self.data.len())
108            .finish()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn metadata_accessors_round_trip() {
118        let c = AudioChunk::new(Some(40_000), 2, 48_000, 6, "5.1".to_string(), vec![0.0; 12]);
119        assert_eq!(c.pts_us(), Some(40_000));
120        assert_eq!(c.index(), 2);
121        assert_eq!(c.sample_rate(), 48_000);
122        assert_eq!(c.channels(), 6);
123        assert_eq!(c.channel_layout(), "5.1");
124        assert_eq!(c.as_slice().len(), 12);
125        assert_eq!(c.into_vec().len(), 12);
126    }
127}