kithara_decode/codec.rs
1use kithara_bufpool::PcmBuf;
2use kithara_platform::time::Duration;
3use kithara_stream::AudioCodec;
4
5use crate::{error::DecodeResult, types::PcmSpec};
6
7#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
8#[non_exhaustive]
9pub struct CodecPriming {
10 pub frames: u64,
11 pub packets: u32,
12 pub byte_margin: u64,
13}
14
15/// PCM frames per coded access unit (AAC 1024, MP3 1152). `0` for codecs
16/// without a fixed AU size. Converts a seek warm-up packet count into a
17/// back-off duration (`packets * access_unit_frames / sample_rate`).
18pub(crate) fn access_unit_frames(codec: AudioCodec) -> u32 {
19 match codec {
20 AudioCodec::Mp3 => 1152,
21 AudioCodec::AacLc | AudioCodec::AacHe | AudioCodec::AacHeV2 => 1024,
22 _ => 0,
23 }
24}
25
26/// Frame-level codec contract paired with a [`crate::demuxer::Demuxer`] in
27/// `ComposedDecoder<D, C>`.
28///
29/// Implementations consume one demuxed frame at a time and write
30/// interleaved f32 PCM into the caller-provided pool buffer (`out`).
31/// They never see container bytes — container parsing is the demuxer's
32/// job. They never allocate their own output `Vec<f32>` — output flows
33/// through the injected `PcmPool`, which keeps the hot path zero-alloc
34/// once the pool is warm.
35pub(crate) trait FrameCodec: Send + 'static {
36 /// Decode one demuxed frame, writing interleaved f32 samples into
37 /// `out` (which the caller acquired from the shared `PcmPool`).
38 /// Returns the frame count actually written (each frame is
39 /// `spec.channels` samples). `0` means the codec consumed the input
40 /// but produced no PCM (warm-up packet, codec backpressure, etc.) —
41 /// caller should pull the next frame.
42 ///
43 /// `frame_data` is the raw bytes for this frame. `pts` is the
44 /// presentation time supplied by the demuxer; codecs may use it
45 /// for diagnostics — decoded sample count + sample rate are the
46 /// authoritative duration source.
47 ///
48 /// `packet_desc` carries opaque per-packet metadata from the
49 /// demuxer for VBR codecs (Apple MP3/ALAC pass a serialized
50 /// `AudioStreamPacketDescription` here). Empty when the demuxer
51 /// produces CBR frames or doesn't model per-packet descriptors;
52 /// codecs that don't need it ignore the slot.
53 ///
54 /// # Errors
55 ///
56 /// Returns a [`crate::DecodeError`] when the underlying decoder
57 /// produces an unrecoverable error or when growing `out` would
58 /// exceed the pool's byte budget.
59 fn decode_frame(
60 &mut self,
61 frame_data: &[u8],
62 pts: Duration,
63 packet_desc: &[u8],
64 out: &mut PcmBuf,
65 ) -> DecodeResult<u32>;
66
67 /// Decoder-side algorithmic delay in PCM frames for `codec` — the
68 /// silent lead-in this concrete decoder emits **in addition to**
69 /// the encoder-declared priming. LAME-convention MP3 decoders
70 /// (Symphonia `mpa` and Apple `AudioConverter`) report 529 here;
71 /// other codecs default to 0.
72 ///
73 /// `kithara_audio::pipeline::gapless::resolve_codec_priming`
74 /// combines this with [`AudioCodec::encoder_priming_frames`] for
75 /// the [`crate::GaplessMode::CodecPriming`] fallback when probing
76 /// yields no metadata; codec `open_with_config` impls fold it into
77 /// the probed `GaplessInfo` so callers downstream see a single
78 /// fully-resolved trim.
79 fn decoder_algo_delay(&self, _codec: AudioCodec) -> u64 {
80 0
81 }
82
83 /// Reset internal codec state — called after seek. Backends that
84 /// can fail to reset (Android `AMediaCodec_flush`) propagate the
85 /// error through `DecodeResult` so the seek path can surface it.
86 ///
87 /// # Errors
88 ///
89 /// Returns [`crate::DecodeError`] when the codec rejects the
90 /// reset.
91 fn flush(&mut self) -> DecodeResult<()>;
92
93 /// PCM output specification.
94 fn spec(&self) -> PcmSpec;
95
96 /// Codec-owned playback contract — currently the captured
97 /// [`crate::GaplessInfo`] (encoder priming + trailing padding in
98 /// PCM frames). Default returns the empty contract; per-platform
99 /// implementations override when their codec exposes
100 /// priming numbers (Apple `kAudioConverterPrimeInfo`, Symphonia
101 /// `AudioDecoderOptions::gapless`, Android via the demuxer's
102 /// container probe).
103 ///
104 /// `ComposedDecoder<D, C>` forwards this through
105 /// [`crate::Decoder::track_info`] so the audio pipeline can build
106 /// a [`crate::GaplessTrimmer`] without knowing the concrete codec
107 /// type.
108 fn track_info(&self) -> crate::DecoderTrackInfo {
109 crate::DecoderTrackInfo::default()
110 }
111
112 /// Seek priming requirements for `codec` — packets/frames/bytes the
113 /// demuxer must back off before the seek target so this codec can
114 /// fully prime its MDCT/SBR overlap-add state. Default returns
115 /// [`CodecPriming::default()`] (no priming required). Backends that
116 /// manage priming internally (e.g. Symphonia fdk-aac / mpa) keep the
117 /// default; Apple `AudioConverter` overrides with per-codec values.
118 fn priming(&self, _codec: AudioCodec) -> CodecPriming {
119 CodecPriming::default()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn codec_priming_default_is_all_zero() {
129 let p = CodecPriming::default();
130 assert_eq!(p.frames, 0);
131 assert_eq!(p.packets, 0);
132 assert_eq!(p.byte_margin, 0);
133 }
134}