Skip to main content

rff_codec_opus/
lib.rs

1//! Opus audio codec, backed by our pure-Rust [`rusty_opus`] (BSD-3-Clause, our
2//! performance fork of `opus-rs`, a port of libopus 1.6 — no FFI).
3//!
4//! Opus works in `f32` samples at one of 8/12/16/24/48 kHz, in fixed frame
5//! durations; we use 20 ms frames. The encoder buffers incoming audio and emits
6//! one Opus packet per frame; the decoder turns each packet back into an
7//! [`AudioFrame`]. Sample rate + channel count reach the decoder via
8//! [`configure`](rff_codec::Decoder::configure) (typically from an `OpusHead`).
9//!
10//! Frames may arrive as interleaved `s16` or `f32`; both are accepted (decoded
11//! output is `f32`).
12
13use std::collections::VecDeque;
14
15use rff_codec::{Codec, CodecParams, CodecRegistry, Decoder, Encoder};
16use rff_core::{AudioFrame, Dictionary, Error, Frame, MediaType, Packet, Result, SampleFormat};
17use rusty_opus::parallel::{encode_parallel, ParallelConfig};
18use rusty_opus::{Application, OpusDecoder, OpusEncoder};
19
20/// Opus frame duration in milliseconds (samples/frame = rate/1000 * this).
21const FRAME_MS: usize = 20;
22/// Opus supports only these sample rates.
23const RATES: [u32; 5] = [8000, 12000, 16000, 24000, 48000];
24
25/// Register the Opus codec into a [`CodecRegistry`].
26pub fn register(registry: &mut CodecRegistry) {
27    registry.register(Codec {
28        id: rff_core::CodecId::Opus,
29        name: "opus",
30        long_name: "Opus (Opus Interactive Audio Codec)",
31        media_type: MediaType::Audio,
32        decoder: Some(|| Box::new(OpusDec::default())),
33        encoder: Some(|| Box::new(OpusEnc::default())),
34    });
35}
36
37fn check_rate(rate: u32) -> Result<()> {
38    if RATES.contains(&rate) {
39        Ok(())
40    } else {
41        Err(Error::unsupported(format!(
42            "opus: sample rate {rate} (must be 8000/12000/16000/24000/48000)"
43        )))
44    }
45}
46
47/// Read interleaved `s16`/`f32` plane 0 into `f32` samples.
48fn frame_to_f32(af: &AudioFrame) -> Result<Vec<f32>> {
49    match af.format {
50        SampleFormat::F32 => Ok(af.planes[0]
51            .chunks_exact(4)
52            .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
53            .collect()),
54        SampleFormat::S16 => Ok(af.planes[0]
55            .chunks_exact(2)
56            .map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0)
57            .collect()),
58        other => Err(Error::unsupported(format!(
59            "opus encode: sample format `{}` (need interleaved s16/f32)",
60            other.name()
61        ))),
62    }
63}
64
65// ---------------------------------------------------------------------------
66// Decoder
67// ---------------------------------------------------------------------------
68
69#[derive(Default)]
70struct OpusDec {
71    dec: Option<OpusDecoder>,
72    sample_rate: u32,
73    channels: u16,
74    queue: VecDeque<Frame>,
75    eof: bool,
76}
77
78impl Decoder for OpusDec {
79    fn configure(&mut self, params: &CodecParams) -> Result<()> {
80        // Opus defaults to 48 kHz when the stream doesn't say otherwise.
81        let rate = if params.sample_rate == 0 {
82            48_000
83        } else {
84            params.sample_rate
85        };
86        check_rate(rate)?;
87        let channels = params.channels.max(1);
88        self.dec = Some(
89            OpusDecoder::new(rate as i32, channels as usize)
90                .map_err(|e| Error::invalid(format!("opus decode init: {e}")))?,
91        );
92        self.sample_rate = rate;
93        self.channels = channels;
94        Ok(())
95    }
96
97    fn send_packet(&mut self, packet: &Packet) -> Result<()> {
98        let channels = self.channels as usize;
99        // opus-rs wants the exact per-channel frame size and a buffer to match.
100        // Our encoder emits 20 ms frames, so decode at that size.
101        let frame_size = self.sample_rate as usize / 1000 * FRAME_MS;
102        let dec = self
103            .dec
104            .as_mut()
105            .ok_or_else(|| Error::invalid("opus decode: not configured"))?;
106
107        let mut out = vec![0f32; frame_size * channels];
108        let samples = dec
109            .decode(&packet.data, frame_size, &mut out)
110            .map_err(|e| Error::invalid(format!("opus decode: {e}")))?;
111        out.truncate(samples * channels);
112
113        let bytes: Vec<u8> = out.iter().flat_map(|s| s.to_le_bytes()).collect();
114        self.queue.push_back(Frame::Audio(AudioFrame {
115            sample_rate: self.sample_rate,
116            channels: self.channels,
117            format: SampleFormat::F32,
118            planes: vec![bytes],
119            samples,
120            pts: packet.pts,
121        }));
122        Ok(())
123    }
124
125    fn receive_frame(&mut self) -> Result<Frame> {
126        if let Some(frame) = self.queue.pop_front() {
127            return Ok(frame);
128        }
129        if self.eof {
130            Err(Error::Eof)
131        } else {
132            Err(Error::Again)
133        }
134    }
135
136    fn flush(&mut self) {
137        self.eof = true;
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Encoder
143// ---------------------------------------------------------------------------
144
145struct OpusEnc {
146    enc: Option<OpusEncoder>,
147    sample_rate: u32,
148    channels: u16,
149    /// Samples per channel in one Opus frame (20 ms).
150    frame_size: usize,
151    /// Interleaved f32 samples awaiting a full frame.
152    buffer: Vec<f32>,
153    /// PTS (in per-channel samples) for the next emitted frame.
154    next_pts: i64,
155    queue: VecDeque<Packet>,
156    eof: bool,
157    // --- rate-control / tuning (from `configure`, FFmpeg-style opts) ---
158    /// Target bitrate (bits/s), `-b:a`. Default 64 kbps (matches libopus's default).
159    bitrate: i32,
160    /// Encoder complexity 0–10 (`-compression_level`). Higher = better/slower.
161    complexity: i32,
162    /// Constant bitrate when set (`-vbr off` / `-b:a` with CBR). Default VBR.
163    use_cbr: bool,
164    /// `voip` vs `audio` application (`-application`). Default audio.
165    voip: bool,
166    /// Frame/chunk-parallel encoding (R1): buffer the whole stream and encode it
167    /// across cores at `flush`, beating single-threaded libopus wall-clock.
168    /// Default on (batch/file semantics); `-opus_parallel 0` forces serial for
169    /// low-latency/streaming. Chunk seams are PEAQ-neutral (see `warmup`).
170    parallel: bool,
171    /// Look-back frames each worker re-encodes to prime its state (PEAQ-neutral
172    /// at ≥4; default 8). `-opus_warmup N`.
173    warmup: usize,
174    /// Worker thread count (`0` = all cores). `-threads N`.
175    threads: usize,
176}
177
178impl Default for OpusEnc {
179    fn default() -> Self {
180        OpusEnc {
181            enc: None,
182            sample_rate: 0,
183            channels: 0,
184            frame_size: 0,
185            buffer: Vec::new(),
186            next_pts: 0,
187            queue: VecDeque::new(),
188            eof: false,
189            bitrate: 64_000,
190            complexity: 9,
191            use_cbr: false,
192            voip: false,
193            parallel: true,
194            warmup: 8,
195            threads: 0,
196        }
197    }
198}
199
200impl OpusEnc {
201    fn init(&mut self, af: &AudioFrame) -> Result<()> {
202        check_rate(af.sample_rate)?;
203        let channels = af.channels.max(1);
204        let app = if self.voip {
205            Application::Voip
206        } else {
207            Application::Audio
208        };
209        let mut enc = OpusEncoder::new(af.sample_rate as i32, channels as usize, app)
210            .map_err(|e| Error::invalid(format!("opus encode init: {e}")))?;
211        enc.bitrate_bps = self.bitrate;
212        enc.complexity = self.complexity.clamp(0, 10);
213        enc.use_cbr = self.use_cbr;
214        self.enc = Some(enc);
215        self.sample_rate = af.sample_rate;
216        self.channels = channels;
217        self.frame_size = af.sample_rate as usize / 1000 * FRAME_MS;
218        Ok(())
219    }
220
221    /// Encode as many whole frames as `buffer` currently holds.
222    fn drain_frames(&mut self) -> Result<()> {
223        let frame_samples = self.frame_size * self.channels as usize;
224        if frame_samples == 0 || self.buffer.len() < frame_samples {
225            return Ok(());
226        }
227        // Iterate whole frames in place with a cursor and drain the consumed
228        // prefix ONCE at the end. Draining a frame off the FRONT each iteration
229        // (`buffer.drain(..frame_samples)`) shifts the whole remaining buffer down
230        // per frame — O(n²) over a fully-buffered stream, and it dominated encode
231        // wall time (~5× the codec itself). Destructure `self` so the input slice
232        // (`buffer`) and the encoder (`enc`) borrow as distinct fields.
233        let OpusEnc {
234            enc,
235            buffer,
236            queue,
237            next_pts,
238            frame_size,
239            ..
240        } = self;
241        let enc = enc.as_mut().expect("encoder initialized");
242        let frame_size = *frame_size;
243        let n_full = buffer.len() / frame_samples;
244        for i in 0..n_full {
245            let chunk = &buffer[i * frame_samples..(i + 1) * frame_samples];
246            let mut out = vec![0u8; 4000]; // max Opus packet size
247            let n = enc
248                .encode(chunk, frame_size, &mut out)
249                .map_err(|e| Error::invalid(format!("opus encode: {e}")))?;
250            out.truncate(n);
251            // PTS in per-channel samples; each 20 ms frame advances by frame_size.
252            let mut packet = Packet::from_data(0, out);
253            packet.pts = Some(*next_pts);
254            *next_pts += frame_size as i64;
255            queue.push_back(packet);
256        }
257        buffer.drain(..n_full * frame_samples);
258        Ok(())
259    }
260
261    /// Encode the whole buffered stream in parallel (R1) at flush, emitting one
262    /// PTS-stamped packet per 20 ms frame in order.
263    fn drain_parallel(&mut self) {
264        let frame_samples = self.frame_size * self.channels as usize;
265        if frame_samples == 0 || self.buffer.is_empty() {
266            return;
267        }
268        let rem = self.buffer.len() % frame_samples;
269        if rem != 0 {
270            self.buffer
271                .extend(std::iter::repeat(0.0).take(frame_samples - rem));
272        }
273        let app = if self.voip {
274            Application::Voip
275        } else {
276            Application::Audio
277        };
278        let mut cfg = ParallelConfig::new(self.sample_rate as i32, self.channels as usize, app);
279        cfg.bitrate_bps = self.bitrate;
280        cfg.complexity = self.complexity;
281        cfg.use_cbr = self.use_cbr;
282        cfg.warmup = self.warmup;
283        cfg.threads = self.threads;
284        let packets = encode_parallel(&cfg, &self.buffer, self.frame_size);
285        self.buffer.clear();
286        for data in packets {
287            let mut packet = Packet::from_data(0, data);
288            packet.pts = Some(self.next_pts);
289            self.next_pts += self.frame_size as i64;
290            self.queue.push_back(packet);
291        }
292    }
293}
294
295impl Encoder for OpusEnc {
296    fn configure(&mut self, options: &Dictionary) -> Result<()> {
297        if let Some(b) = options.get_bitrate("b") {
298            if b > 0 {
299                self.bitrate = b as i32;
300            }
301        }
302        // `-compression_level 0..10` maps to Opus complexity (FFmpeg's mapping).
303        if let Some(c) = options.get_int("compression_level") {
304            self.complexity = (c as i32).clamp(0, 10);
305        }
306        if let Some(app) = options.get("application") {
307            self.voip = app.eq_ignore_ascii_case("voip") || app.eq_ignore_ascii_case("lowdelay");
308        }
309        // `-vbr off` / `-vbr 0` → CBR (FFmpeg's libopus `vbr` option).
310        if let Some(v) = options.get("vbr") {
311            self.use_cbr = v == "off" || v == "0" || v.eq_ignore_ascii_case("false");
312        }
313        // R1 frame-parallel controls.
314        if let Some(p) = options.get("opus_parallel") {
315            self.parallel = !(p == "0" || p == "off" || p.eq_ignore_ascii_case("false"));
316        }
317        if let Some(w) = options.get_int("opus_warmup") {
318            self.warmup = w.max(0) as usize;
319        }
320        if let Some(t) = options.get_int("threads") {
321            self.threads = t.max(0) as usize;
322        }
323        Ok(())
324    }
325
326    fn accepted_sample_rates(&self) -> Option<Vec<u32>> {
327        Some(RATES.to_vec())
328    }
329
330    fn send_frame(&mut self, frame: &Frame) -> Result<()> {
331        let af = match frame {
332            Frame::Audio(a) => a,
333            Frame::Video(_) => {
334                return Err(Error::unsupported(
335                    "opus encode: video frame on an audio codec",
336                ))
337            }
338        };
339        if self.enc.is_none() {
340            self.init(af)?;
341            // Seed output PTS from the first frame's timestamp (per-channel samples).
342            self.next_pts = af.pts.unwrap_or(0);
343        } else if af.sample_rate != self.sample_rate || af.channels != self.channels {
344            return Err(Error::unsupported(
345                "opus encode: stream layout changed mid-stream",
346            ));
347        }
348        self.buffer.extend(frame_to_f32(af)?);
349        // Parallel mode accumulates the whole stream and encodes it at `flush`;
350        // serial mode drains whole frames incrementally as they arrive.
351        if self.parallel {
352            Ok(())
353        } else {
354            self.drain_frames()
355        }
356    }
357
358    fn receive_packet(&mut self) -> Result<Packet> {
359        if let Some(packet) = self.queue.pop_front() {
360            return Ok(packet);
361        }
362        if self.eof {
363            Err(Error::Eof)
364        } else {
365            Err(Error::Again)
366        }
367    }
368
369    fn flush(&mut self) {
370        // `init` runs on the first frame, so `frame_size` is set iff we saw input.
371        if self.frame_size != 0 && !self.buffer.is_empty() {
372            if self.parallel {
373                self.drain_parallel();
374            } else {
375                // Pad a trailing partial frame with silence so it still encodes.
376                let frame_samples = self.frame_size * self.channels as usize;
377                let rem = self.buffer.len() % frame_samples;
378                if rem != 0 {
379                    self.buffer
380                        .extend(std::iter::repeat(0.0).take(frame_samples - rem));
381                }
382                let _ = self.drain_frames();
383            }
384        }
385        self.eof = true;
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    /// Encode a tone to Opus and decode it back; check the pipeline runs and a
394    /// plausible number of samples come out (Opus is lossy — no exact compare).
395    #[test]
396    fn opus_encode_then_decode() {
397        let (rate, channels) = (48_000u32, 1u16);
398        let frame = rate as usize / 1000 * FRAME_MS; // 960
399        let mut samples = Vec::new();
400        for i in 0..frame * 10 {
401            samples.push(((i as f32 * 0.05).sin() * 0.3) as f32);
402        }
403        let bytes: Vec<u8> = samples.iter().flat_map(|s| s.to_le_bytes()).collect();
404
405        let mut enc = OpusEnc::default();
406        enc.send_frame(&Frame::Audio(AudioFrame {
407            sample_rate: rate,
408            channels,
409            format: SampleFormat::F32,
410            planes: vec![bytes],
411            samples: samples.len(),
412            pts: Some(0),
413        }))
414        .unwrap();
415        enc.flush();
416
417        let mut packets = Vec::new();
418        loop {
419            match enc.receive_packet() {
420                Ok(p) => {
421                    assert!(!p.data.is_empty());
422                    packets.push(p);
423                }
424                Err(Error::Eof) | Err(Error::Again) => break,
425                Err(e) => panic!("encode: {e}"),
426            }
427        }
428        assert!(
429            packets.len() >= 8,
430            "expected ~10 frames, got {}",
431            packets.len()
432        );
433
434        let mut dec = OpusDec::default();
435        dec.configure(&CodecParams {
436            sample_rate: rate,
437            channels,
438            ..Default::default()
439        })
440        .unwrap();
441        let mut decoded = 0usize;
442        let mut energy = 0f64;
443        for p in &packets {
444            dec.send_packet(p).unwrap();
445            if let Ok(Frame::Audio(af)) = dec.receive_frame() {
446                assert_eq!(af.format, SampleFormat::F32);
447                decoded += af.samples;
448                for b in af.planes[0].chunks_exact(4) {
449                    let s = f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64;
450                    energy += s * s;
451                }
452            }
453        }
454        assert!(decoded >= frame * 8, "decoded too few samples: {decoded}");
455        let rms = (energy / decoded.max(1) as f64).sqrt();
456        assert!(rms > 0.02, "decoded audio is silent: rms {rms:.4}");
457    }
458}