Skip to main content
This guide covers the complete process of decoding audio from codec bitstreams into raw PCM audio samples.

Overview

Decoding audio in Symphonia involves:
  1. Obtaining packets from a FormatReader
  2. Feeding packets to a Decoder
  3. Converting decoded audio to the desired format
  4. Handling errors during decoding

Basic Decode Loop

The decode loop is the heart of audio decoding. It continuously fetches packets and decodes them.
1

Set up the decoder

First, create a decoder for your selected track:
use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions};

// Find the first audio track
let track = format.tracks()
    .iter()
    .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
    .expect("no supported audio tracks");

// Create a decoder for the track
let dec_opts: DecoderOptions = Default::default();
let mut decoder = symphonia::default::get_codecs()
    .make(&track.codec_params, &dec_opts)
    .expect("unsupported codec");

let track_id = track.id;
2

Implement the decode loop

Loop through packets and decode them:
use symphonia::core::errors::Error;

loop {
    // Get the next packet from the format reader
    let packet = match format.next_packet() {
        Ok(packet) => packet,
        Err(Error::ResetRequired) => {
            // Track list changed - recreate decoders
            unimplemented!();
        }
        Err(err) => {
            // Handle or propagate error
            break;
        }
    };

    // Filter packets by track ID
    if packet.track_id() != track_id {
        continue;
    }

    // Decode the packet
    match decoder.decode(&packet) {
        Ok(decoded) => {
            // Process decoded audio
        }
        Err(Error::IoError(_)) => {
            // Skip packet with IO error
            continue;
        }
        Err(Error::DecodeError(_)) => {
            // Skip packet with decode error
            continue;
        }
        Err(err) => {
            // Unrecoverable error
            panic!("{}" err);
        }
    }
}
3

Process metadata updates

Check for metadata updates during decoding:
// Consume any new metadata
while !format.metadata().is_latest() {
    format.metadata().pop();

    // Access new metadata if needed
    if let Some(rev) = format.metadata().current() {
        // Process metadata revision
    }
}

Handling Decode Errors

Symphonia provides specific error types for different failure scenarios.

Error Types

The track list has changed (e.g., chained OGG streams). You must:
  • Re-examine the track list
  • Create new decoders
  • Restart the decode loop
Err(Error::ResetRequired) => {
    // Get new track list
    let tracks = format.tracks();
    
    // Find and select new track
    let track = tracks.iter()
        .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
        .expect("no audio track");
    
    // Create new decoder
    decoder = symphonia::default::get_codecs()
        .make(&track.codec_params, &dec_opts)?;
    
    track_id = track.id;
    continue;
}
A packet failed to decode due to an I/O error. This is usually recoverable - skip the packet and continue:
Err(Error::IoError(err)) => {
    eprintln!("IO error decoding packet: {}", err);
    continue;  // Skip this packet
}
The packet contains invalid data and cannot be decoded. Skip and continue:
Err(Error::DecodeError(msg)) => {
    eprintln!("Decode error: {}", msg);
    continue;  // Skip this packet
}
When any call to decode() returns an error, decoders must clear their internal buffer. The returned AudioBufferRef will have a length of 0.

Accessing Audio Samples

Once decoded, you can access audio samples in several ways depending on your needs.

Direct Sample Access

Access samples directly from the AudioBufferRef:
use symphonia::core::audio::{AudioBufferRef, Signal};

let decoded = decoder.decode(&packet)?;

match decoded {
    AudioBufferRef::F32(buf) => {
        // Iterate over samples in the first channel
        for &sample in buf.chan(0) {
            // Process sample
        }
    }
    AudioBufferRef::S16(buf) => {
        // Handle 16-bit integer samples
        for &sample in buf.chan(0) {
            // Process sample
        }
    }
    _ => {
        // Handle other sample formats
    }
}

Planar Access

Access all channels as separate planes:
use symphonia::core::audio::{AudioBufferRef, Signal};

match decoded {
    AudioBufferRef::F32(buf) => {
        let planes = buf.planes();
        
        // Iterate over each channel
        for plane in planes.planes() {
            for &sample in plane.iter() {
                // Process sample from this channel
            }
        }
    }
    _ => { /* Handle other formats */ }
}

Last Decoded Buffer

Access the last successfully decoded buffer:
let last_decoded = decoder.last_decoded();
If the last call to decode() resulted in an error, last_decoded() will return a buffer with length 0.

Converting Sample Formats

Symphonia provides utilities to convert between sample formats and memory layouts.

Interleaved Samples with SampleBuffer

Convert to interleaved format with a specific sample type:
use symphonia::core::audio::SampleBuffer;

// Create a sample buffer (reuse for efficiency)
let mut sample_buf = None;

loop {
    let packet = format.next_packet()?;
    if packet.track_id() != track_id { continue; }
    
    match decoder.decode(&packet) {
        Ok(decoded) => {
            // Create sample buffer on first decode
            if sample_buf.is_none() {
                let spec = *decoded.spec();
                let duration = decoded.capacity() as u64;
                sample_buf = Some(SampleBuffer::<f32>::new(duration, spec));
            }
            
            // Copy and convert to interleaved f32
            if let Some(buf) = &mut sample_buf {
                buf.copy_interleaved_ref(decoded);
                
                // Access interleaved samples
                let samples = buf.samples();
                // Send to audio output...
            }
        }
        Err(Error::DecodeError(_)) => continue,
        Err(_) => break,
    }
}
Reuse SampleBuffer for the life of the decoder to avoid allocations. Only recreate if Error::ResetRequired occurs.

Byte-Oriented Interleaved Samples

For APIs requiring raw bytes:
use symphonia::core::audio::RawSampleBuffer;

let mut byte_buf = None;

loop {
    let packet = format.next_packet()?;
    if packet.track_id() != track_id { continue; }
    
    match decoder.decode(&packet) {
        Ok(decoded) => {
            if byte_buf.is_none() {
                let spec = *decoded.spec();
                let duration = decoded.capacity() as u64;
                byte_buf = Some(RawSampleBuffer::<f32>::new(duration, spec));
            }
            
            if let Some(buf) = &mut byte_buf {
                buf.copy_interleaved_ref(decoded);
                
                // Get raw bytes
                let bytes: &[u8] = buf.as_bytes();
                // Write to file, send to audio device, etc.
            }
        }
        Err(Error::DecodeError(_)) => continue,
        Err(_) => break,
    }
}

Planar Sample Conversion

Convert to planar format instead:
// For SampleBuffer
sample_buf.copy_planar_ref(decoded);

// For RawSampleBuffer
byte_buf.copy_planar_ref(decoded);

Complete Example

Here’s a complete working example that decodes an audio file and counts samples:
use std::env;
use std::fs::File;
use std::path::Path;

use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::errors::Error;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;

fn main() {
    let args: Vec<String> = env::args().collect();
    let file = Box::new(File::open(Path::new(&args[1])).unwrap());
    
    let mss = MediaSourceStream::new(file, Default::default());
    let hint = Hint::new();
    
    let format_opts: FormatOptions = Default::default();
    let metadata_opts: MetadataOptions = Default::default();
    let decoder_opts: DecoderOptions = Default::default();
    
    // Probe and open format
    let probed = symphonia::default::get_probe()
        .format(&hint, mss, &format_opts, &metadata_opts)
        .unwrap();
    
    let mut format = probed.format;
    let track = format.default_track().unwrap();
    
    // Create decoder
    let mut decoder = symphonia::default::get_codecs()
        .make(&track.codec_params, &decoder_opts)
        .unwrap();
    
    let track_id = track.id;
    let mut sample_count = 0;
    let mut sample_buf = None;
    
    // Decode loop
    loop {
        let packet = format.next_packet().unwrap();
        
        if packet.track_id() != track_id {
            continue;
        }
        
        match decoder.decode(&packet) {
            Ok(decoded) => {
                if sample_buf.is_none() {
                    let spec = *decoded.spec();
                    let duration = decoded.capacity() as u64;
                    sample_buf = Some(SampleBuffer::<f32>::new(duration, spec));
                }
                
                if let Some(buf) = &mut sample_buf {
                    buf.copy_interleaved_ref(decoded);
                    sample_count += buf.samples().len();
                    print!("\rDecoded {} samples", sample_count);
                }
            }
            Err(Error::DecodeError(_)) => (),
            Err(_) => break,
        }
    }
    
    println!("\nTotal samples decoded: {}", sample_count);
}

Best Practices

Reuse Buffers

Create SampleBuffer and RawSampleBuffer once and reuse them throughout decoding to minimize allocations.

Handle Errors Gracefully

Skip packets with IoError or DecodeError, but handle ResetRequired by recreating decoders.

Filter by Track ID

Always check packet.track_id() matches your selected track before decoding.

Check Metadata

Regularly check for metadata updates using format.metadata().is_latest().

Next Steps

Working with Metadata

Learn how to read and manage metadata

Gapless Playback

Enable seamless track transitions

Build docs developers (and LLMs) love