# opus-decoder
[](https://crates.io/crates/opus-decoder)
[](https://docs.rs/opus-decoder)
[](https://github.com/tadeuszwojcik/rasopus/actions)
[](https://www.rfc-editor.org/rfc/rfc8251)
[](LICENSE-APACHE)
A pure-Rust [Opus](https://opus-codec.org/) 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
- **Multistream** — `OpusMultistreamDecoder` 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`:
```toml
[dependencies]
opus-decoder = "0.1"
```
### Decode a single packet
```rust
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
```rust
// Pass an empty slice to trigger PLC
let mut pcm = vec![0i16; 960 * 2];
decoder.decode(&[], &mut pcm, false)?; // generates concealment frame
```
### Float output
```rust
let mut pcm_f32 = vec![0f32; 960 * 2];
decoder.decode_float(packet, &mut pcm_f32, false)?;
```
### Multistream — call recording (2 speakers → stereo)
```rust
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
```rust
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`
```rust
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`
```rust
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`
```rust
pub enum OpusError {
InvalidPacket,
InternalError,
BufferTooSmall,
InvalidArgument(&'static str),
}
```
## Conformance
All 12 RFC 8251 test vectors pass the `opus_compare` quality metric:
| 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:
```bash
# 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:
- [Apache License, Version 2.0](LICENSE-APACHE)
- [MIT License](LICENSE-MIT)
at your option.
This crate ports algorithms from [libopus](https://gitlab.xiph.org/xiph/opus),
which is licensed under the [BSD 3-Clause License](https://gitlab.xiph.org/xiph/opus/-/blob/master/COPYING).
## Contributing
Conformance is the top priority. Any change must pass all 12 RFC 8251 test vectors
before merging. Run:
```bash
cargo test --release -p opus-decoder
cargo clippy -p opus-decoder --all-targets -- -D warnings
```