opus-decoder 0.1.1

Pure-Rust Opus decoder — RFC 8251 conformant, no unsafe, no FFI
Documentation

opus-decoder

Crates.io Docs.rs CI RFC 8251 License

A pure-Rust Opus decoder with no unsafe code and no FFI. Passes all 12 RFC 8251 conformance test vectors.

Features

  • RFC 8251 conformant — all 12 official test vectors pass opus_compare
  • All Opus modes — CELT, SILK, and Hybrid (SWB/FB)
  • All output sample rates — 8, 12, 16, 24, 48 kHz
  • Mono and stereo — including up/downmix
  • Packet Loss Concealment (PLC) — CELT fade-out, SILK pitch extrapolation
  • MultistreamOpusMultistreamDecoder for surround (5.1, 7.1) and call recording
  • #![forbid(unsafe_code)] — throughout the correctness implementation
  • No FFI, no C dependencies — pure Rust

Quick Start

Add to Cargo.toml:

[dependencies]
opus-decoder = "0.1"

Decode a single packet

use opus_decoder::{OpusDecoder, OpusError};

fn main() -> Result<(), OpusError> {
    let mut decoder = OpusDecoder::new(48_000, 2)?; // 48 kHz, stereo
    
    let packet: &[u8] = /* your Opus packet bytes */;
    let mut pcm = vec![0i16; 960 * 2]; // 20 ms at 48 kHz, stereo interleaved
    
    let samples = decoder.decode(packet, &mut pcm, false)?;
    println!("Decoded {} samples per channel", samples);
    Ok(())
}

Packet Loss Concealment

// Pass an empty slice to trigger PLC
let mut pcm = vec![0i16; 960 * 2];
decoder.decode(&[], &mut pcm, false)?; // generates concealment frame

Float output

let mut pcm_f32 = vec![0f32; 960 * 2];
decoder.decode_float(packet, &mut pcm_f32, false)?;

Multistream — call recording (2 speakers → stereo)

use opus_decoder::OpusMultistreamDecoder;

// Two independent mono streams → stereo output
// Left channel: caller (Stream A), Right channel: callee (Stream B)
let mut decoder = OpusMultistreamDecoder::new(
    48_000,  // sample rate
    2,       // output channels
    2,       // nb_streams
    0,       // nb_coupled_streams (both mono)
    &[0, 1], // mapping: stream 0 → L, stream 1 → R
)?;

let mut pcm = vec![0i16; 960 * 2];
decoder.decode(&multistream_packet, &mut pcm, false)?;

Multistream — 5.1 surround

let mut decoder = OpusMultistreamDecoder::new(
    48_000,
    6,             // 5.1 output channels
    4,             // 4 streams total
    2,             // 2 coupled (stereo) streams
    &[0, 1, 2, 3, 4, 255], // mapping (255 = silence)
)?;

API Reference

OpusDecoder

impl OpusDecoder {
    pub fn new(sample_rate: u32, channels: usize) -> Result<Self, OpusError>;
    pub fn decode(&mut self, packet: &[u8], pcm: &mut [i16], fec: bool) -> Result<usize, OpusError>;
    pub fn decode_float(&mut self, packet: &[u8], pcm: &mut [f32], fec: bool) -> Result<usize, OpusError>;
    pub fn reset(&mut self);
}

sample_rate must be one of: 8000, 12000, 16000, 24000, 48000.
channels must be 1 or 2.
decode returns the number of samples decoded per channel.
An empty packet or fec: true triggers Packet Loss Concealment (PLC).

OpusMultistreamDecoder

impl OpusMultistreamDecoder {
    pub fn new(sample_rate: u32, nb_channels: usize, nb_streams: usize,
               nb_coupled_streams: usize, mapping: &[u8]) -> Result<Self, OpusError>;
    pub fn decode(&mut self, packet: &[u8], pcm: &mut [i16], fec: bool) -> Result<usize, OpusError>;
    pub fn decode_float(&mut self, packet: &[u8], pcm: &mut [f32], fec: bool) -> Result<usize, OpusError>;
    pub fn reset(&mut self);
}

mapping[i] = 255 silences output channel i.
The first nb_coupled_streams streams are stereo-coupled; the rest are mono.

OpusError

pub enum OpusError {
    InvalidPacket,
    InternalError,
    BufferTooSmall,
    InvalidArgument(&'static str),
}

Conformance

All 12 RFC 8251 test vectors pass the opus_compare quality metric:

Vector Description Result
tv01 CELT stereo FB ✅ PASS
tv02 SILK NB 8 kHz mono ✅ PASS
tv03 SILK MB 12 kHz mono ✅ PASS
tv04 SILK WB 16 kHz mono ✅ PASS
tv05 Hybrid SWB transitions ✅ PASS
tv06 Hybrid FB transitions ✅ PASS
tv07 CELT mono FB ✅ PASS
tv08 SILK stereo + CELT NB stereo ✅ PASS
tv09 SILK→CELT transitions ✅ PASS
tv10 CeltOnly + Hybrid mix ✅ PASS
tv11 CELT stereo FB ✅ PASS
tv12 SILK bandwidth transitions ✅ PASS

Run conformance tests locally:

# Download test vectors
cargo xtask fetch-vectors

# Run all 12 vectors
for v in 01 02 03 04 05 06 07 08 09 10 11 12; do
  OPUS_TESTVECTORS_DIR="testdata/opus_testvectors" \
  OPUS_VECTOR=testvector${v} \
  cargo test --release -p opus-decoder rfc_conformance -- --ignored --nocapture \
    2>&1 | grep -E "PASS|FAIL"
done

Minimum Supported Rust Version (MSRV)

Rust 1.85 — enforced in CI.

Uses: std, thiserror. No nightly features.

Out of Scope

The following are intentionally not implemented in this crate:

  • Encoder — decoder only
  • Container parsing — Ogg/WebM demux belongs in a separate crate
  • SIMD optimizations — correctness first; performance is a future phase

Implementation Notes

This crate is a pure Rust port of libopus based on RFC 6716 and RFC 8251, implemented function-by-function and verified against the reference libopus implementation. The floating-point decode path is used throughout (matching libopus float semantics).

Internal architecture:

lib.rs                  — public API, mode routing, mode transitions
├── celt/               — CELT decoder (MDCT, PVQ, band decode, anti-collapse, PLC)
├── silk/               — SILK decoder (NLSF→LPC, excitation, IIR/FIR resampler, PLC)
├── multistream.rs      — OpusMultistreamDecoder
├── entropy.rs          — Range coder (EcDec)
└── compare.rs          — opus_compare quality metric port

Known Limitations

  • Hybrid mode PLC uses SILK-only concealment (no CELT highband PLC)
  • No fuzz harness yet (planned)

License

Licensed under either of:

at your option.

This crate ports algorithms from libopus, which is licensed under the BSD 3-Clause License.

Contributing

Conformance is the top priority. Any change must pass all 12 RFC 8251 test vectors before merging. Run:

cargo test --release -p opus-decoder
cargo clippy -p opus-decoder --all-targets -- -D warnings