Skip to main content
Symphonia is designed around a clear separation of concerns, with distinct boundaries between demuxing (reading container formats) and decoding (decompressing audio codecs). This architectural approach provides flexibility, modularity, and performance.

Core Design Principles

Symphonia’s architecture follows these key principles:
  1. Separation of demuxing and decoding - Container formats and codecs are completely independent
  2. Trait-based abstractions - Core functionality is defined through Rust traits
  3. Modular crate structure - Features can be selectively enabled via crate features
  4. Type safety - 100% safe Rust with strong type guarantees
  5. Zero-cost abstractions - Generic programming with no runtime overhead

Architectural Overview

The Demuxing Layer

FormatReader Trait

The FormatReader trait (symphonia-core/src/formats.rs:168) is the core abstraction for container format demuxers. All container formats (MP4, MKV, OGG, WAV, etc.) implement this trait.
pub trait FormatReader: Send + Sync {
    // Instantiate a format reader
    fn try_new(source: MediaSourceStream, options: &FormatOptions) -> Result<Self>;
    
    // Get the next packet from the container
    fn next_packet(&mut self) -> Result<Packet>;
    
    // Get track information
    fn tracks(&self) -> &[Track];
    
    // Seek to a timestamp
    fn seek(&mut self, mode: SeekMode, to: SeekTo) -> Result<SeekedTo>;
    
    // Get metadata
    fn metadata(&mut self) -> Metadata<'_>;
}

Container Format Flow

  1. Initialization: FormatReader::try_new() probes the container header and extracts track information
  2. Demuxing: next_packet() returns the next encoded packet from any track
  3. Track Selection: Caller filters packets by track_id to process specific tracks
  4. Seeking: seek() moves to a different position in the stream
The FormatReader returns packets from all tracks in the order they appear in the container. Your application must filter packets by track_id to process specific tracks.

The Decoding Layer

Decoder Trait

The Decoder trait (symphonia-core/src/codecs.rs:460) abstracts audio codec decoders. Each codec (MP3, FLAC, Vorbis, AAC, etc.) implements this trait.
pub trait Decoder: Send + Sync {
    // Instantiate a decoder
    fn try_new(params: &CodecParameters, options: &DecoderOptions) -> Result<Self>;
    
    // Decode a packet into audio samples
    fn decode(&mut self, packet: &Packet) -> Result<AudioBufferRef<'_>>;
    
    // Reset decoder state after a seek
    fn reset(&mut self);
    
    // Get the last decoded buffer
    fn last_decoded(&self) -> AudioBufferRef<'_>;
}

Decoder Flow

  1. Instantiation: Create decoder using CodecParameters from the track
  2. Decoding: Feed Packets to decode(), receive AudioBufferRefs
  3. Reset: Call reset() after seeking to clear decoder state
  4. Export: Convert AudioBufferRef to SampleBuffer for output

Format and Codec Discovery

The Probe

The Probe (symphonia-core/src/probe.rs:194) automatically detects container formats by scanning the byte stream for format markers.
let mut probe = Probe::default();

// Register all enabled formats
probe.register_all::<WavReader>();
probe.register_all::<Mp4Reader>();
probe.register_all::<OggReader>();

// Probe the stream
let probed = probe.format(&hint, mss, &fmt_opts, &meta_opts)?;
let mut format = probed.format; // FormatReader instance

The CodecRegistry

The CodecRegistry (symphonia-core/src/codecs.rs:524) maps codec types to decoder implementations.
let mut registry = CodecRegistry::new();

// Register decoders
registry.register_all::<Mp3Decoder>();
registry.register_all::<FlacDecoder>();
registry.register_all::<VorbisDecoder>();

// Create decoder for a track
let decoder = registry.make(&track.codec_params, &dec_opts)?;
Use symphonia::default::get_probe() and symphonia::default::get_codecs() to get pre-populated registries with all enabled formats and codecs.

Modular Crate Structure

Symphonia is organized into multiple crates to allow selective compilation:
symphonia-core - The foundation library containing:
  • Trait definitions (FormatReader, Decoder)
  • Audio primitives (AudioBuffer, SampleBuffer)
  • I/O abstractions (MediaSourceStream)
  • Codec and format registries (Probe, CodecRegistry)
  • No actual format or codec implementations

Feature Flags

Enable specific formats and codecs via Cargo features:
[dependencies]
symphonia = { version = "0.5", features = ["mp3", "aac", "flac"] }

# Or enable everything:
symphonia = { version = "0.5", features = ["all"] }

# Or by category:
symphonia = { version = "0.5", features = ["all-codecs", "all-formats"] }

Complete Decode Pipeline

Here’s how all the components work together:

Key Architectural Benefits

Any codec bitstream can be stored in any compatible container. For example:
  • Vorbis audio in OGG, MKV, or WebM
  • AAC audio in MP4, ADTS, or MKV
This separation means adding a new format automatically supports all existing codecs.
Rust’s trait system provides:
  • Compile-time polymorphism via generics (zero overhead)
  • Runtime polymorphism via trait objects when needed
  • Memory safety without garbage collection
  • Thread safety via Send + Sync bounds
The modular crate structure allows:
  • Reduced binary size by excluding unused formats/codecs
  • Faster compile times for development
  • License compliance (exclude non-free codecs)
  • Platform-specific builds (WASM, embedded)
Adding new functionality is straightforward:
  • Implement FormatReader for a new container format
  • Implement Decoder for a new audio codec
  • Implement MediaSource for custom I/O sources
  • All existing code continues to work unchanged

Next Steps

Formats and Codecs

Learn about container formats vs codecs and how they work together

Audio Primitives

Understand AudioBuffer, sample formats, and audio data handling

Media Sources

Deep dive into MediaSource, streaming, and buffering

Getting Started

See the architecture in action with a complete example

Build docs developers (and LLMs) love