kithara-decode
Audio decoding library with explicit, typed backend selection. DecoderFactory creates synchronous Decoder instances that convert compressed audio (MP3, AAC, FLAC, WAV, ALAC, …) into pool-backed PCM (Vec<f32>). No threading, no channels — just decoding.
The public surface centres on one trait — Decoder. Concrete backends (Symphonia / Apple / Android) implement it directly. Internally, container parsing and codec stepping are split: the Demuxer trait owns container framing, the FrameCodec trait owns codec decoding, and ComposedDecoder<D, C> (internal) wires them together so backends can be mixed and matched. The factory hides this detail — callers only ever see Box<dyn Decoder>.
Usage
use Cursor;
use ;
let reader = new;
let config = DecoderConfig ;
let mut decoder = create_with_probe?;
let spec = decoder.spec; // sample_rate, channels
loop
For HLS / cross-codec recreate paths, prefer DecoderFactory::create_from_media_info(reader, &media_info, config) — it skips probing and uses the carried MediaInfo to pick the backend.
Backends
Initialization Paths
- Direct reader creation (
containerspecified): Creates format reader directly without probing. Used for HLS fMP4 where format is known but byte length is unknown. Seek is disabled during init to preventIsoMp4Readerfrom seeking to end. - Probe (
containernot specified): Uses Symphonia's auto-detection. Supportsprobe_no_seekfor ABR variant switches where reported byte length may not match.
Decoder recreate strategy
create_for_recreateis used for seek-time decoder rebuild.- It is a thin wrapper over
create_from_media_info: callers must supply abase_offsetthat lines up with the container's init region (for fMP4/MP4/WAV/MKV/CAF theftyp/RIFF/EBML header; for MPEG-ES / ADTS / FLAC / Ogg / MPEG-TS any valid packet start). - No fallback: when the metadata-driven path fails the error is
propagated verbatim. Probing mid-segment bytes at a mismatched
offset can silently match an unrelated codec (e.g. MP3 frame sync
in raw AAC-in-fMP4 bytes) and drive the rest of the pipeline off a
session.media_infothe decoder never actually realised.
Gapless playback
DecoderConfig::gapless is enabled by default. Decoders report engine-level trim
metadata through DecoderTrackInfo::gapless: Option<GaplessInfo>, where
leading_frames and trailing_frames are PCM frame counts.
The contract has one owner for actual trimming:
Some(GaplessInfo)means the backend decoded the untrimmed PCM region and thekithara-audiopipeline must applyGaplessTrimmerbefore effects.Nonemeans no engine trim should run. This covers files with no gapless metadata and backend paths that already applied gapless trim internally.GaplessTrimmer::notify_seek()drops only the leading trim state; tail trim is still applied at EOF for the current track.
When metadata is absent, kithara-audio's AudioConfig::gapless_mode can select
heuristic behaviour via GaplessMode:
GaplessMode::CodecPriming—GaplessTrimmer::codec_priming(frames, sample_rate)is built from a static codec table (codec_priming_frames). AAC LC is 2112, HE-AAC 3072, MP3 LAME-default 1105, Opus 312, and lossless codecs are 0. Predictable and zero-latency.GaplessMode::SilenceTrim(SilenceTrimParams)—GaplessTrimmer::silence_trimwalks the leading buffer until the first sample above a configurable dB threshold and trims everything before it. Optionally trims the trailing silence at EOF too.
See also GaplessMode::Disabled and GaplessMode::MediaOnly on AudioConfig.
Both fallbacks apply a short raised-cosine fade-in (~3 ms) at the trim boundary. The metadata-driven path does not — the boundary lands on a sample-accurate count.
GaplessTrimmer::notify_seek() drops both the leading-trim state and
any pending fade-in; tail trim continues to be applied at EOF.
Current metadata sources:
- AAC in MP4/M4A/fMP4: MP4 probe reads
edts/elstfirst, then falls back toiTunSMPB. - MP3, FLAC, Vorbis, and Opus through Symphonia rely on the backend's own
gapless behavior and therefore expose
Nonefor engine trim. - Apple AudioToolbox captures
AudioConverterPrimeInfowhen available. - Android MediaCodec reads
encoder-delay/encoder-paddingfromMediaFormatand falls back to the MP4 probe for AAC MP4 containers.
Feature Flags
When symphonia is disabled (default-features = false + only apple / android), the factory has no software fallback — it errors if the active hardware backend cannot handle a codec/container.
Module layout
src/traits.rs— publicDecodertrait plus typed outcomes (DecoderChunkOutcome,DecoderSeekOutcome,InputReadOutcome) and theDecoderInputsource supertrait.src/factory/—DecoderConfig,DecoderFactory, and theDecoderBackendselector enum. The factory boxes every backend intoBox<dyn Decoder>so callers stay codec-agnostic.src/composed.rs— internalComposedDecoder<D: Demuxer, C: FrameCodec>that implementsDecoderby pairing a demuxer with a frame-level codec.src/demuxer/— internalDemuxertrait and concrete demuxers (fMP4, …).src/fmp4/,src/mp4.rs— fMP4/MP4 container helpers.src/symphonia/(featuresymphonia) — SymphoniaDecoderimplementation; probe and direct paths;ReadSeekAdapter.src/apple/(featureapple, macOS / iOS) — AppleAudioToolboxbackend overAudioFile/AudioConverterFFI.src/android/(featureandroid, Android) —MediaExtractor/MediaCodecbackend over JNI.src/gapless.rs—GaplessInfo,GaplessMode,GaplessTrimmer,SilenceTrimParams, pluscodec_priming_framesand the MP4 gapless probe.src/pcm_time.rs— timeline math (duration_for_frames,frames_for_duration, PTS helpers) shared across backends.src/types.rs,src/error.rs— shared types andDecodeError/ErrorClass.
Cross-decoder protocol test
The cross-decoder protocol test lives in kithara-integration-tests under tests/tests/kithara_decode/. It decodes the same MP3 with every available backend and asserts agreement on spec(), duration(), total frame count, post-seek timestamp, EOF semantics, and (when the apple feature is enabled on macOS/iOS) the full-decode PCM L2 norm within 2 %.
To exercise the same protocol per backend in isolation (no cross-feature unification):
Integration
Consumed by kithara-audio which wraps it in a threaded pipeline with effects and resampling. Accepts any R: Read + Seek + Send + Sync + 'static -- works with Stream<File>, Stream<Hls>, Cursor<Vec<u8>>, or plain files.