# audio-opus-bsd
[](./LICENSE)
[](https://www.rust-lang.org)
Opus codec ([RFC 6716](https://datatracker.ietf.org/doc/html/rfc6716)) encoder
and decoder. It wraps system **libopus** through the `opus` crate and
emits/consumes planar `AudioFrame`s (from the `audio-core-bsd` dependency).
Encode and decode run on **worker threads** — this crate is deliberately **not**
real-time safe; decoded frames are handed to the RT audio thread through a
lock-free `rtrb` ring buffer.
> The doc comments on each public item are the primary reference. This README is
> an overview only.
## Overview
`audio-opus-bsd` is a **standalone** Opus codec crate: it depends only on
`audio-core-bsd` (the shared planar `AudioFrame` type) and the system libopus
library, and can be built and used independently of any larger audio framework.
It turns planar `AudioFrame`s into Opus packets and back. Because
the `opus` crate is a thin FFI binding over libopus (which allocates and may
make syscalls), all encode/decode work is moved onto a dedicated worker thread.
The real-time audio thread touches only the lock-free `rtrb::Consumer::pop` —
never the codec. This is the standard RT-safety boundary for FFI-based audio
codecs: the real-time path stays wait-free and allocation-free.
### Why a worker thread?
| **Worker thread** | libopus encode/decode, planar↔interleaved conversion, packet framing | allowed |
| **RT audio thread** | `rtrb::Consumer::pop` of a pre-decoded `AudioFrame` | **none** (wait-free) |
The `rt_alloc_free` integration test asserts the RT `pop` path performs **zero**
heap allocations.
## Core types
| `AudioEncoder` | The public trait: `encode(&[f32]) -> Result<Vec<u8>>` (interleaved PCM in) + `set_bitrate`. |
| `AudioDecoder` | The public trait: `decode(&[u8]) -> Result<Vec<f32>>` (interleaved PCM out). |
| `OpusEncoder` | libopus-backed encoder; also offers `encode_frame(&AudioFrame)` (planar in). |
| `OpusDecoder` | libopus-backed decoder; also offers `decode_frame(&[u8]) -> AudioFrame` (planar out). |
| `OpusDecodeWorker` | Spawns a decode worker thread; returns a `rtrb::Consumer<AudioFrame>` for the RT side. |
| `OpusEncodeWorker` | Spawns an encode worker thread; returns a `rtrb::Consumer<Vec<u8>>` for the sender side. |
| `OpusError` | Error enum (thiserror-backed): `Encode`, `Decode`, `InvalidSampleRate`, `UnsupportedChannels`, `BufferTooSmall`, `LayoutMismatch`, `WorkerThread`. |
| `Result<T>` | Alias for `core::result::Result<T, OpusError>`. |
Opus supports only **mono (1)** or **stereo (2)** channels and the standard
8/12/16/24/48 kHz sample rates. The canonical encode/decode block is **960
samples/channel (20 ms @ 48 kHz)**.
## Dependencies
| `audio-core-bsd` | 0.1.0 | `AudioFrame` (planar) I/O type | BSD-2-Clause |
| `opus` | 0.3 | libopus FFI binding (`encode_float`/`decode_float`) | MIT OR Apache-2.0 |
| `rtrb` | 0.3 | lock-free SPSC ring buffer (RT↔worker channel) | MIT OR Apache-2.0 |
| `thiserror` | 2.0 | `OpusError` derive | MIT OR Apache-2.0 |
| `proptest` (dev) | 1.11.0 | property tests | MIT OR Apache-2.0 |
### System dependency — libopus
This crate links the system **libopus** (`audio/opus` port on FreeBSD,
`libopus-dev` on Debian/Ubuntu). On FreeBSD:
```sh
sudo pkg install -y opus
```
On Debian/Ubuntu: `sudo apt-get install -y libopus-dev`.
**Licensing:** the `opus` Rust crate is **MIT OR Apache-2.0**; the system
library it links, **libopus**, is **BSD-3-Clause**. Both are permissive and
**compatible** with this crate's BSD-2-Clause license — no copyleft
contamination.
## Status
**0.x — experimental.** The API is not yet frozen (pre-1.0 breaking changes are
allowed before 1.0).
- edition: 2021
- MSRV: 1.85
- license: BSD-2-Clause
## Example
Encode planar `AudioFrame`s and decode them back through the worker-thread
boundary:
```rust,no_run
use audio_opus_bsd::{OpusEncoder, OpusDecoder, AudioEncoder, AudioDecoder};
use audio_core_bsd::AudioFrame;
// 20 ms mono block @ 48 kHz.
let mut enc = OpusEncoder::new(48_000, 1, opus::Application::Audio)?;
enc.set_bitrate(64_000);
let frame = AudioFrame::silence(1, 960, 48_000);
let packet = enc.encode_frame(&frame)?;
let mut dec = OpusDecoder::new(48_000, 1)?;
let back = dec.decode_frame(&packet)?;
assert_eq!(back.num_frames(), 960);
# Ok::<(), audio_opus_bsd::OpusError>(())
```
For the RT-safe path (decode on a worker, consume wait-free on the RT thread),
see `OpusDecodeWorker` and the `examples/` directory.
## Worker-thread safety boundary
`OpusEncoder` / `OpusDecoder` are **not** real-time safe — they perform FFI and
heap allocation, which is correct and intended. Always move them onto a worker
thread (`OpusDecodeWorker` / `OpusEncodeWorker`) and let the RT audio thread
consume only from the returned `rtrb::Consumer`. The `rt_alloc_free` and
`worker_xrun` integration tests verify this contract.
## License
BSD-2-Clause. See [`LICENSE`](./LICENSE).