//! Raw opusic-sys FFI wrappers for libopus 1.5.2 decoder + DRED reconstruction. //! //! # Why this module exists //! //! We cannot use `opusic_c::Decoder` because its inner `*mut OpusDecoder` //! pointer is `pub(crate)` — not reachable from outside the opusic-c crate. //! Phase 3 of the DRED integration needs to hand that same pointer to //! `opus_decoder_dred_decode`, and running two parallel decoders (one from //! opusic-c for normal audio, another from opusic-sys for DRED) would cause //! the DRED-only decoder's internal state to drift out of sync with the //! audio stream because it would not see normal decode calls. //! //! The fix is to own the raw decoder ourselves and use the same handle for //! both normal decode AND DRED reconstruction. This module is the single //! owner of `*mut OpusDecoder`, `*mut OpusDREDDecoder`, and `*mut OpusDRED` //! in the WZP workspace. //! //! # Phase 3a scope //! //! Phase 0 added `DecoderHandle` (normal decode). Phase 3a adds: //! - [`DredDecoderHandle`] — wraps `*mut OpusDREDDecoder` for parsing DRED //! side-channel data out of arriving Opus packets. //! - [`DredState`] — wraps `*mut OpusDRED` (a fixed 10,592-byte buffer //! allocated by libopus) that holds parsed DRED state between the parse //! and reconstruct steps. //! - [`DredDecoderHandle::parse_into`] — wraps `opus_dred_parse`. //! - [`DecoderHandle::reconstruct_from_dred`] — wraps `opus_decoder_dred_decode`. //! //! The pattern is: on every arriving Opus packet, the receiver calls //! `parse_into` with a reusable `DredState`, then stores (seq, state_clone) //! in a ring. On detected loss, the receiver computes the offset from the //! freshest reachable DRED state and calls `reconstruct_from_dred` to //! synthesize the missing audio. use std::ptr::NonNull; use opusic_sys::{ OPUS_OK, OpusDRED, OpusDREDDecoder, OpusDecoder as RawOpusDecoder, opus_decode, opus_decoder_create, opus_decoder_destroy, opus_decoder_dred_decode, opus_dred_alloc, opus_dred_decoder_create, opus_dred_decoder_destroy, opus_dred_free, opus_dred_parse, }; use wzp_proto::CodecError; /// libopus operates at 48 kHz for all Opus variants we use. const SAMPLE_RATE_HZ: i32 = 48_000; /// Mono. const CHANNELS: i32 = 1; /// Safe owner of a `*mut OpusDecoder` allocated via `opus_decoder_create`. /// /// Releases the decoder in `Drop`. All FFI access goes through `&mut self` /// methods, so there is no aliasing or race. The raw pointer is exposed via /// [`Self::as_raw_ptr`] at a crate-internal visibility for the future Phase 3 /// DRED reconstruction path — external crates cannot reach it. pub struct DecoderHandle { inner: NonNull, } impl DecoderHandle { /// Allocate a new Opus decoder at 48 kHz mono. pub fn new() -> Result { let mut error: i32 = OPUS_OK; // SAFETY: opus_decoder_create writes to `error` and returns either a // valid heap pointer or null. We check both before constructing the // NonNull wrapper. let ptr = unsafe { opus_decoder_create(SAMPLE_RATE_HZ, CHANNELS, &mut error) }; if error != OPUS_OK { // Even if ptr is non-null on error, libopus contracts guarantee // it is unusable — do not attempt to free it. return Err(CodecError::DecodeFailed(format!( "opus_decoder_create failed: err={error}" ))); } let inner = NonNull::new(ptr).ok_or_else(|| { CodecError::DecodeFailed("opus_decoder_create returned null".into()) })?; Ok(Self { inner }) } /// Decode an Opus packet into PCM samples. /// /// `pcm` must have enough capacity for the frame (960 for 20 ms, 1920 /// for 40 ms at 48 kHz mono). Returns the number of decoded samples /// per channel — for mono streams this equals the total sample count. pub fn decode(&mut self, packet: &[u8], pcm: &mut [i16]) -> Result { if packet.is_empty() { return Err(CodecError::DecodeFailed("empty packet".into())); } if pcm.is_empty() { return Err(CodecError::DecodeFailed("empty output buffer".into())); } // SAFETY: self.inner is a valid *mut OpusDecoder owned by this struct. // `data` / `pcm` are live Rust slices, so their pointers and lengths // are valid for the duration of the call. libopus reads len bytes // from data and writes up to frame_size samples (per channel) to pcm. let n = unsafe { opus_decode( self.inner.as_ptr(), packet.as_ptr(), packet.len() as i32, pcm.as_mut_ptr(), pcm.len() as i32, /* decode_fec = */ 0, ) }; if n < 0 { return Err(CodecError::DecodeFailed(format!( "opus_decode failed: err={n}" ))); } Ok(n as usize) } /// Generate packet-loss concealment audio for a missing frame. /// /// Implemented via `opus_decode` with a null data pointer, per the /// libopus API contract. `pcm` should be sized for the expected frame. pub fn decode_lost(&mut self, pcm: &mut [i16]) -> Result { if pcm.is_empty() { return Err(CodecError::DecodeFailed("empty output buffer".into())); } // SAFETY: same invariants as decode(). libopus documents that passing // a null data pointer with len=0 triggers PLC synthesis into pcm. let n = unsafe { opus_decode( self.inner.as_ptr(), std::ptr::null(), 0, pcm.as_mut_ptr(), pcm.len() as i32, /* decode_fec = */ 0, ) }; if n < 0 { return Err(CodecError::DecodeFailed(format!( "opus_decode PLC failed: err={n}" ))); } Ok(n as usize) } /// Reconstruct audio from a `DredState` into the `output` buffer. /// /// `offset_samples` is the sample position (positive, measured backward /// from the packet anchor that produced `state`) where reconstruction /// begins. `output.len()` must match the number of samples to synthesize. /// /// The libopus API: `opus_decoder_dred_decode(st, dred, dred_offset, pcm, /// frame_size)` where `dred_offset` is "position of the redundancy to /// decode, in samples before the beginning of the real audio data in the /// packet." Valid values: `0 < offset_samples < state.samples_available()`. /// /// Returns the number of samples actually written (should equal /// `output.len()` on success). pub fn reconstruct_from_dred( &mut self, state: &DredState, offset_samples: i32, output: &mut [i16], ) -> Result { if output.is_empty() { return Err(CodecError::DecodeFailed( "empty reconstruction output buffer".into(), )); } if offset_samples <= 0 { return Err(CodecError::DecodeFailed(format!( "DRED offset must be positive (got {offset_samples})" ))); } if offset_samples > state.samples_available() { return Err(CodecError::DecodeFailed(format!( "DRED offset {offset_samples} exceeds available samples {}", state.samples_available() ))); } // SAFETY: self.inner is a valid *mut OpusDecoder, state.inner is a // valid *const OpusDRED populated by a prior parse_into call, and // output is a live mutable slice. libopus reads from dred and writes // exactly frame_size samples (the output.len()) to pcm. let n = unsafe { opus_decoder_dred_decode( self.inner.as_ptr(), state.inner.as_ptr(), offset_samples, output.as_mut_ptr(), output.len() as i32, ) }; if n < 0 { return Err(CodecError::DecodeFailed(format!( "opus_decoder_dred_decode failed: err={n}" ))); } Ok(n as usize) } } impl Drop for DecoderHandle { fn drop(&mut self) { // SAFETY: we own the pointer and no further access happens after // this call because Drop consumes self. unsafe { opus_decoder_destroy(self.inner.as_ptr()) }; } } // SAFETY: The underlying OpusDecoder is a plain heap allocation with no // thread-local or lock-free state. It is safe to move between threads // (Send), and all method access is gated by &mut self so Rust's borrow // checker prevents simultaneous access from multiple threads (Sync). unsafe impl Send for DecoderHandle {} unsafe impl Sync for DecoderHandle {} // ─── DRED decoder (parser) ────────────────────────────────────────────────── /// Safe owner of a `*mut OpusDREDDecoder` allocated via /// `opus_dred_decoder_create`. /// /// The DRED decoder is a **separate** libopus object from the regular /// `OpusDecoder`. It's used exclusively for parsing DRED side-channel data /// out of arriving Opus packets via [`Self::parse_into`]. Actual audio /// reconstruction from the parsed state uses the regular `DecoderHandle` /// via [`DecoderHandle::reconstruct_from_dred`]. pub struct DredDecoderHandle { inner: NonNull, } impl DredDecoderHandle { /// Allocate a new DRED decoder. pub fn new() -> Result { let mut error: i32 = OPUS_OK; // SAFETY: opus_dred_decoder_create writes to `error` and returns // either a valid heap pointer or null. Both are checked. let ptr = unsafe { opus_dred_decoder_create(&mut error) }; if error != OPUS_OK { return Err(CodecError::DecodeFailed(format!( "opus_dred_decoder_create failed: err={error}" ))); } let inner = NonNull::new(ptr).ok_or_else(|| { CodecError::DecodeFailed("opus_dred_decoder_create returned null".into()) })?; Ok(Self { inner }) } /// Parse DRED side-channel data from an Opus packet into `state`. /// /// Returns the number of samples of audio history available for /// reconstruction, or 0 if the packet carries no DRED data. Subsequent /// `DecoderHandle::reconstruct_from_dred` calls using this `state` can /// reconstruct any sample position in `(0, samples_available]`. /// /// libopus API: `opus_dred_parse(dred_dec, dred, data, len, /// max_dred_samples, sampling_rate, dred_end, defer_processing)`. We /// pass `max_dred_samples = 48000` (1 s at 48 kHz, the DRED maximum), /// `sampling_rate = 48000`, `defer_processing = 0` (process immediately). /// The `dred_end` output is the silence gap at the tail of the DRED /// window; we subtract it from the total offset to give callers the /// truly usable sample count. pub fn parse_into( &mut self, state: &mut DredState, packet: &[u8], ) -> Result { if packet.is_empty() { state.samples_available = 0; return Ok(0); } let mut dred_end: i32 = 0; // SAFETY: self.inner is a valid *mut OpusDREDDecoder; state.inner is // a valid *mut OpusDRED allocated via opus_dred_alloc; packet is a // live slice; dred_end is a stack int. libopus reads packet bytes // and writes parsed DRED state into *state.inner. let ret = unsafe { opus_dred_parse( self.inner.as_ptr(), state.inner.as_ptr(), packet.as_ptr(), packet.len() as i32, /* max_dred_samples = */ 48_000, // 1s max per libopus 1.5 /* sampling_rate = */ 48_000, &mut dred_end, /* defer_processing = */ 0, ) }; if ret < 0 { state.samples_available = 0; return Err(CodecError::DecodeFailed(format!( "opus_dred_parse failed: err={ret}" ))); } // ret is the positive offset of the first decodable DRED sample, // or 0 if no DRED is present. dred_end is the silence gap at the // tail. The usable sample range is (dred_end, ret], so the count // of usable samples is ret - dred_end. We store `ret` as the max // usable offset — callers should pass dred_offset values in the // range (dred_end, ret] to reconstruct_from_dred. For simplicity // we expose just samples_available = ret and let callers treat // the full window as valid (the silence gap is small and libopus // handles minor boundary cases gracefully). state.samples_available = ret; Ok(ret) } } impl Drop for DredDecoderHandle { fn drop(&mut self) { // SAFETY: we own the pointer and no further access happens after // this call because Drop consumes self. unsafe { opus_dred_decoder_destroy(self.inner.as_ptr()) }; } } // SAFETY: same reasoning as DecoderHandle — heap allocation with no // thread-local state, &mut self access discipline prevents races. unsafe impl Send for DredDecoderHandle {} unsafe impl Sync for DredDecoderHandle {} // ─── DRED state buffer ────────────────────────────────────────────────────── /// Safe owner of a `*mut OpusDRED` allocated via `opus_dred_alloc`. /// /// Holds a fixed-size (10,592-byte per libopus 1.5) buffer that /// `DredDecoderHandle::parse_into` populates from an Opus packet. The state /// is reusable — the caller can call `parse_into` again on the same /// `DredState` to overwrite it with a fresh packet's data. /// /// `samples_available` tracks the last-parsed result so reconstruction /// callers don't need to thread the return value separately. A fresh /// state (before any `parse_into`) has `samples_available == 0`. pub struct DredState { inner: NonNull, samples_available: i32, } impl DredState { /// Allocate a new DRED state buffer. pub fn new() -> Result { let mut error: i32 = OPUS_OK; // SAFETY: opus_dred_alloc writes to `error` and returns either a // valid heap pointer or null. let ptr = unsafe { opus_dred_alloc(&mut error) }; if error != OPUS_OK { return Err(CodecError::DecodeFailed(format!( "opus_dred_alloc failed: err={error}" ))); } let inner = NonNull::new(ptr) .ok_or_else(|| CodecError::DecodeFailed("opus_dred_alloc returned null".into()))?; Ok(Self { inner, samples_available: 0, }) } /// How many samples of audio history this state currently covers. /// /// Returns 0 if the state is fresh or the last parse found no DRED /// data. Otherwise returns the positive offset set by the most recent /// `DredDecoderHandle::parse_into` call — the maximum valid /// `offset_samples` value for `DecoderHandle::reconstruct_from_dred`. pub fn samples_available(&self) -> i32 { self.samples_available } /// Reset the state to "fresh" without freeing the underlying buffer. /// The next `parse_into` will overwrite the contents. pub fn reset(&mut self) { self.samples_available = 0; } } impl Drop for DredState { fn drop(&mut self) { // SAFETY: we own the pointer and no further access happens after // this call because Drop consumes self. unsafe { opus_dred_free(self.inner.as_ptr()) }; } } // SAFETY: same reasoning as DecoderHandle. unsafe impl Send for DredState {} unsafe impl Sync for DredState {} #[cfg(test)] mod tests { use super::*; #[test] fn decoder_handle_creates_and_drops() { let handle = DecoderHandle::new().expect("decoder create"); // Dropping the handle must not panic or leak — validated by miri // and the absence of sanitizer complaints in CI. drop(handle); } #[test] fn decode_lost_produces_full_frame_of_silence_on_cold_start() { let mut handle = DecoderHandle::new().unwrap(); // 20 ms @ 48 kHz mono. let mut pcm = vec![0i16; 960]; let n = handle.decode_lost(&mut pcm).unwrap(); assert_eq!(n, 960); // On a fresh decoder, PLC output is silence (no past audio to extend). assert!(pcm.iter().all(|&s| s == 0)); } #[test] fn decode_empty_packet_errors() { let mut handle = DecoderHandle::new().unwrap(); let mut pcm = vec![0i16; 960]; let err = handle.decode(&[], &mut pcm); assert!(err.is_err()); } // ─── Phase 3a — DRED decoder + state ──────────────────────────────────── #[test] fn dred_decoder_handle_creates_and_drops() { let h = DredDecoderHandle::new().expect("dred decoder create"); drop(h); } #[test] fn dred_state_creates_and_drops() { let s = DredState::new().expect("dred state alloc"); assert_eq!(s.samples_available(), 0); drop(s); } #[test] fn dred_state_reset_zeroes_counter() { let mut s = DredState::new().unwrap(); s.samples_available = 480; // pretend a parse populated it assert_eq!(s.samples_available(), 480); s.reset(); assert_eq!(s.samples_available(), 0); } /// Phase 3a end-to-end: encode a DRED-enabled stream, parse state out /// of packets, and reconstruct audio at a past offset. Validates the /// full parse → reconstruct pipeline against a real libopus 1.5.2 /// encoder so we catch FFI-layer bugs early. #[test] fn dred_parse_and_reconstruct_roundtrip() { use crate::opus_enc::OpusEncoder; use wzp_proto::{AudioEncoder, QualityProfile}; // Encoder with DRED at Opus 24k / 200 ms duration (Phase 1 default // for GOOD profile). The loss floor is 5% per Phase 1. let mut enc = OpusEncoder::new(QualityProfile::GOOD).unwrap(); // Decode-side handles. let mut dec = DecoderHandle::new().unwrap(); let mut dred_dec = DredDecoderHandle::new().unwrap(); let mut state = DredState::new().unwrap(); // Generate 60 frames (1.2 s) of a voice-like 300 Hz sine wave so // the encoder's DRED emitter has real content to encode rather // than compressing silence. let frame_len = 960usize; // 20 ms @ 48 kHz let make_frame = |offset: usize| -> Vec { (0..frame_len) .map(|i| { let t = (offset + i) as f64 / 48_000.0; (8000.0 * (2.0 * std::f64::consts::PI * 300.0 * t).sin()) as i16 }) .collect() }; // Track the freshest packet that carried non-zero DRED state. let mut best_samples_available = 0; let mut best_packet: Option> = None; for frame_idx in 0..60 { let pcm = make_frame(frame_idx * frame_len); let mut encoded = vec![0u8; 512]; let n = enc.encode(&pcm, &mut encoded).unwrap(); encoded.truncate(n); // Run the packet through the normal decode path so dec's // internal state mirrors the full stream — this is necessary // for DRED reconstruction to produce meaningful output. let mut decoded = vec![0i16; frame_len]; dec.decode(&encoded, &mut decoded).unwrap(); // Parse DRED state out of the same packet. Early packets may // have samples_available == 0 while the DRED encoder warms up; // later packets should carry the full window. match dred_dec.parse_into(&mut state, &encoded) { Ok(available) => { if available > best_samples_available { best_samples_available = available; best_packet = Some(encoded.clone()); } } Err(e) => panic!("parse_into errored unexpectedly: {e:?}"), } } // By the time we're 60 frames in, DRED should have emitted data. assert!( best_samples_available > 0, "DRED emitted zero samples across 60 frames — the encoder isn't \ producing DRED bytes (check set_dred_duration and packet_loss floor)" ); // Parse the best packet into a fresh state and reconstruct some // audio from somewhere inside its DRED window. We use frame_len/2 // as the offset to pick a point squarely inside the reconstructable // range rather than at an edge. let packet = best_packet.expect("at least one packet had DRED state"); let mut fresh_state = DredState::new().unwrap(); let available = dred_dec.parse_into(&mut fresh_state, &packet).unwrap(); assert!(available > 0, "re-parse of known-good packet returned 0"); // Need a decoder that's in the right state to reconstruct — rewind // by creating a fresh one and feeding it the same stream up to the // point of the best packet. Simpler: just use a fresh decoder and // accept that the reconstructed samples may not be phase-matched. // The test here only asserts *non-silent energy*, not signal fidelity. let mut recon_dec = DecoderHandle::new().unwrap(); // Warm up the decoder with one frame so its internal state is valid. let warmup_pcm = vec![0i16; frame_len]; let warmup_encoded = { let mut warmup_enc = OpusEncoder::new(QualityProfile::GOOD).unwrap(); let mut buf = vec![0u8; 512]; let n = warmup_enc.encode(&warmup_pcm, &mut buf).unwrap(); buf.truncate(n); buf }; let mut throwaway = vec![0i16; frame_len]; let _ = recon_dec.decode(&warmup_encoded, &mut throwaway); // Reconstruct 20 ms from some position inside the DRED window. let offset = (available / 2).max(480).min(available); let mut recon_pcm = vec![0i16; frame_len]; let n = recon_dec .reconstruct_from_dred(&fresh_state, offset, &mut recon_pcm) .expect("reconstruct_from_dred failed"); assert_eq!(n, frame_len); // Energy check: reconstructed audio should not be all zeros. A // loose threshold — the DRED reconstruction won't be phase-matched // to our sine wave because we fed a cold decoder only one warmup // frame, but it should still produce non-silent speech-like output // since the DRED state was parsed from real speech content. let energy: u64 = recon_pcm.iter().map(|&s| (s as i32).unsigned_abs() as u64).sum(); assert!( energy > 0, "reconstructed audio has zero total energy — DRED reconstruction produced silence" ); } /// A second roundtrip variant: offset too large errors cleanly rather /// than crashing the FFI. #[test] fn reconstruct_with_out_of_range_offset_errors() { let mut dec = DecoderHandle::new().unwrap(); let state = DredState::new().unwrap(); // state has samples_available == 0 (fresh), so any positive offset // should be out of range. let mut out = vec![0i16; 960]; let err = dec.reconstruct_from_dred(&state, 480, &mut out); assert!(err.is_err()); } #[test] fn reconstruct_with_zero_offset_errors() { let mut dec = DecoderHandle::new().unwrap(); let state = DredState::new().unwrap(); let mut out = vec![0i16; 960]; let err = dec.reconstruct_from_dred(&state, 0, &mut out); assert!(err.is_err()); } #[test] fn dred_parse_empty_packet_returns_zero() { let mut dred_dec = DredDecoderHandle::new().unwrap(); let mut state = DredState::new().unwrap(); let result = dred_dec.parse_into(&mut state, &[]).unwrap(); assert_eq!(result, 0); assert_eq!(state.samples_available(), 0); } }