audio-opus-bsd 0.1.1

Opus codec (RFC 6716) encoder/decoder — libopus binding, worker-thread encode/decode emitting planar audio-core-bsd AudioFrames
# audio-opus-bsd

[![License: BSD-2-Clause](https://img.shields.io/badge/license-BSD--2--Clause-blue.svg)](./LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](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?

| Layer | What runs | Allocation |
|-------|-----------|------------|
| **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

| Type               | Description                                                                                   |
|--------------------|-----------------------------------------------------------------------------------------------|
| `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

| Dependency       | Version | Purpose                                              | License           |
|------------------|---------|------------------------------------------------------|-------------------|
| `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).