Skip to main content

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}