Skip to main content
Symphonia provides a sophisticated I/O layer that abstracts over different input sources while optimizing for performance. Understanding this layer is key to efficiently reading audio from files, network streams, or custom sources.

MediaSource Trait

The MediaSource trait (symphonia-core/src/io/mod.rs:42) is the foundation of Symphonia’s I/O system.
pub trait MediaSource: io::Read + io::Seek + Send + Sync {
    /// Returns if the source is seekable
    fn is_seekable(&self) -> bool;
    
    /// Returns the length in bytes, if available
    fn byte_len(&self) -> Option<u64>;
}
Despite requiring io::Seek, seeking is optional. The is_seekable() method indicates at runtime whether seeking is supported.

Built-in MediaSource Implementations

File Sources

The most common source is std::fs::File:
use std::fs::File;
use symphonia::core::io::MediaSourceStream;

// Open file
let file = File::open("audio.mp3")?;

// Wrap in MediaSourceStream
let mss = MediaSourceStream::new(Box::new(file), Default::default());

Memory Sources

Use io::Cursor for in-memory data:
use std::io::Cursor;

let data: Vec<u8> = download_audio()?;
let cursor = Cursor::new(data);
let mss = MediaSourceStream::new(Box::new(cursor), Default::default());

Non-Seekable Sources (ReadOnlySource)

For sources that only implement Read (like stdin, network streams), use ReadOnlySource (symphonia-core/src/io/mod.rs:94):
use symphonia::core::io::ReadOnlySource;
use std::io::stdin;

let source = ReadOnlySource::new(stdin());
let mss = MediaSourceStream::new(Box::new(source), Default::default());
Non-seekable sources have limitations:
  • Cannot use seek operations
  • Format detection must rely on sequential markers
  • Some formats may fail to parse without seeking

MediaSourceStream

MediaSourceStream (symphonia-core/src/io/media_source_stream.rs:52) is Symphonia’s supercharged buffered reader. It wraps any MediaSource and provides:
  1. Exponential read-ahead buffering
  2. Ring buffer for backward seeking
  3. Efficient byte and bit reading
  4. Vectored I/O for efficiency

Architecture

Buffering Strategy

MediaSourceStream uses exponential growth for read-ahead:
  • Initial: 1 KB per fetch
  • Maximum: 32 KB per fetch
  • Growth: Doubles on each sequential read
This minimizes:
  • System call overhead on sequential reads
  • Excess buffering on frequent seeks
// Starts at 1KB
const MIN_BLOCK_LEN: usize = 1 * 1024;
// Grows to 32KB
const MAX_BLOCK_LEN: usize = 32 * 1024;

MediaSourceStreamOptions

pub struct MediaSourceStreamOptions {
    /// The maximum buffer size. Must be a power of 2. Must be > 32kB.
    pub buffer_len: usize,
}
let mss = MediaSourceStream::new(
    Box::new(file),
    Default::default()  // 64KB buffer
);

Seekable vs Non-Seekable Sources

Checking Seekability

let mss = MediaSourceStream::new(Box::new(source), Default::default());

if mss.is_seekable() {
    println!("Source supports seeking");
    if let Some(len) = mss.byte_len() {
        println!("Source length: {} bytes", len);
    }
} else {
    println!("Source is stream-only (no seeking)");
}

Behavior Differences

Sources: File, Cursor<Vec<u8>>, Cursor<&[u8]>Capabilities:
  • Full seeking support via seek()
  • Known byte length via byte_len()
  • Format readers can use seek indexes
  • Can jump to any position in the stream
Use Cases:
  • Local audio files
  • Embedded audio resources
  • Downloaded audio in memory

Buffered Seeking

For efficient local seeking within the buffered region, use SeekBuffered trait methods:
use symphonia::core::io::SeekBuffered;

// Seek to absolute position (within buffer)
let new_pos = mss.seek_buffered(1024);

// Seek relative to current position
let new_pos = mss.seek_buffered_rel(512);   // Forward 512 bytes
let new_pos = mss.seek_buffered_rel(-256);  // Backward 256 bytes

// Seek backward (convenience method)
mss.seek_buffered_rev(128);  // Go back 128 bytes

// Check buffer state
let can_seek_back = mss.read_buffer_len();    // Bytes available backward
let can_seek_fwd = mss.unread_buffer_len();   // Bytes available forward

// Ensure seekback capacity
mss.ensure_seekback_buffer(4096);  // Ensure 4KB seekback
seek_buffered() operations are clamped to the buffered region. If you seek beyond the buffer, the position is clamped to the buffer boundary.

Advanced Reading Methods

Symphonia provides extensive I/O methods via the ReadBytes trait:
use symphonia::core::io::ReadBytes;

// Read single bytes
let byte = mss.read_byte()?;          // u8
let signed = mss.read_i8()?;          // i8

// Read integers (little-endian by default)
let val16 = mss.read_u16()?;          // u16 LE
let val32 = mss.read_u32()?;          // u32 LE
let val64 = mss.read_u64()?;          // u64 LE

// Read integers (big-endian)
let val16_be = mss.read_be_u16()?;    // u16 BE
let val32_be = mss.read_be_u32()?;    // u32 BE

// Read 24-bit integers
let val24 = mss.read_u24()?;          // u24 as u32
let val24_be = mss.read_be_u24()?;    // u24 BE as u32

Bit Reading

Some codecs (like MP3, AAC) require bit-level reading. Symphonia provides bit readers:
use symphonia::core::io::{BitReaderLtr, ReadBitsLtr};

let mut br = BitReaderLtr::new(&packet.data);

let bits = br.read_bits_leq32(5)?;   // Read 5 bits
let flag = br.read_bool()?;          // Read 1 bit as bool
let signed = br.read_bits_leq32_signed(8)?;  // Read signed

// Ignore bits
br.ignore_bits(3)?;

// Check alignment
if !br.is_aligned() {
    br.realign();  // Advance to byte boundary
}

Custom MediaSource Implementation

You can implement MediaSource for custom I/O scenarios:
use std::io::{self, Read, Seek, SeekFrom};
use symphonia::core::io::MediaSource;

struct CustomSource {
    data: Vec<u8>,
    pos: usize,
}

impl Read for CustomSource {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let remaining = self.data.len() - self.pos;
        let to_read = buf.len().min(remaining);
        
        buf[..to_read].copy_from_slice(&self.data[self.pos..self.pos + to_read]);
        self.pos += to_read;
        
        Ok(to_read)
    }
}

impl Seek for CustomSource {
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
        let new_pos = match pos {
            SeekFrom::Start(n) => n as i64,
            SeekFrom::Current(n) => self.pos as i64 + n,
            SeekFrom::End(n) => self.data.len() as i64 + n,
        };
        
        if new_pos < 0 || new_pos > self.data.len() as i64 {
            return Err(io::Error::new(io::ErrorKind::InvalidInput, "seek out of bounds"));
        }
        
        self.pos = new_pos as usize;
        Ok(self.pos as u64)
    }
}

impl MediaSource for CustomSource {
    fn is_seekable(&self) -> bool {
        true
    }
    
    fn byte_len(&self) -> Option<u64> {
        Some(self.data.len() as u64)
    }
}

Performance Considerations

Choose buffer size based on use case:
Use CaseRecommended SizeReason
Local files64-128 KBDefault is fine
Network streams256 KB - 1 MBReduce network overhead
Low latency32-64 KBMinimize buffering delay
Embedded/WASM32-64 KBReduce memory usage
Large files128-256 KBAmortize syscall overhead
Remember: Must be power of 2, > 32 KB.
The seekback buffer allows efficient backward seeking:
// Ensure you can seek back 16KB
mss.ensure_seekback_buffer(16 * 1024);

// Now you can efficiently seek backward
mss.seek_buffered_rev(8 * 1024);  // No I/O needed
Useful for:
  • Format probing that needs to retry
  • Decoders that need lookahead/lookbehind
  • Error recovery with retries
Each seek() call via io::Seek invalidates the buffer:
// Bad - buffer invalidated each time
for offset in offsets {
    mss.seek(SeekFrom::Start(offset))?;
    let byte = mss.read_byte()?;
}

// Better - use buffered seeking if possible
for offset in offsets {
    mss.seek_buffered(offset);
    let byte = mss.read_byte()?;
}
The exponential read-ahead buffer optimizes for sequential access:
// Optimal - sequential reading grows buffer to 32KB
loop {
    let packet = format.next_packet()?;
    // Process...
}

// Suboptimal - seeking resets buffer to 1KB
for ts in timestamps {
    format.seek(SeekMode::Accurate, SeekTo::Time { time, track_id })?;
    let packet = format.next_packet()?;
}

Next Steps

Architecture

See how MediaSource fits into Symphonia’s overall architecture

Decoding Audio

Use MediaSourceStream to decode audio files

Streaming Audio

Handle network streams and live audio

Custom I/O

Implement custom MediaSource for special use cases

Build docs developers (and LLMs) love