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}