Phase 0 of the DRED integration (docs/PRD-dred-integration.md). No behavior change: inband FEC stays ON, no DRED, same bitrate, same quality. This commit unblocks Phase 1+ by getting us onto libopus 1.5.2 where DRED lives. Rationale for going straight to a custom DecoderHandle: opusic-c::Decoder's inner *mut OpusDecoder pointer is pub(crate), so we cannot reach it for the Phase 3 DRED reconstruction path. Running two parallel decoders (one for audio, one for DRED) would drift because the DRED decoder wouldn't see normal decode calls. Single unified DecoderHandle over raw opusic-sys is the only correct architecture, so we build it in Phase 0 rather than rewriting opus_dec.rs twice. Changes: - Cargo.toml (workspace + wzp-codec): remove audiopus 0.3.0-rc.0, add opusic-c 1.5.5 (bundled + dred features), opusic-sys 0.6.0 (bundled), bytemuck 1. Pinned exactly for reproducible libopus 1.5.2. - opus_enc.rs: rewritten against opusic_c::Encoder. Argument order for Encoder::new swapped (Channels first). set_inband_fec(bool) now maps to InbandFec::Mode1 (the libopus 1.5 equivalent of 1.3's LBRR). encode uses bytemuck::cast_slice<i16,u16> at the &[u16] boundary. - dred_ffi.rs (new): DecoderHandle wrapping *mut OpusDecoder directly via opusic-sys. Owns the allocation, frees on Drop. Exposes decode, decode_lost, and a pub(crate) as_raw_ptr() for the future Phase 3 DRED reconstruction. Send+Sync justified via &mut self access discipline. - opus_dec.rs: rewritten as a thin AudioDecoder impl over DecoderHandle. Behavior identical to pre-swap. Verification (Phase 0 acceptance gates): - cargo check --workspace: clean (30 pre-existing warnings in jni_bridge.rs unrelated to this work; zero in changed files). - cargo test -p wzp-codec: 53 tests pass (50 pre-swap + 6 new: 3 in dred_ffi.rs for DecoderHandle lifecycle, 3 in opus_enc.rs for version check and roundtrip). - linked_libopus_is_1_5 test asserts opusic_c::version() contains "1.5" — hard signal that the swap landed correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
82 lines
2.7 KiB
Rust
82 lines
2.7 KiB
Rust
//! Opus decoder built on top of the raw opusic-sys `DecoderHandle`.
|
|
//!
|
|
//! Phase 0 of the DRED integration: we went straight to a custom
|
|
//! `DecoderHandle` instead of `opusic_c::Decoder` because the latter's
|
|
//! inner pointer is `pub(crate)` and we need to reach it in Phase 3 for
|
|
//! `opus_decoder_dred_decode`. See `dred_ffi.rs` for the rationale and
|
|
//! `docs/PRD-dred-integration.md` for the full plan.
|
|
|
|
use crate::dred_ffi::DecoderHandle;
|
|
use wzp_proto::{AudioDecoder, CodecError, CodecId, QualityProfile};
|
|
|
|
/// Opus decoder implementing [`AudioDecoder`].
|
|
///
|
|
/// Operates at 48 kHz mono output. 20 ms and 40 ms frames supported via
|
|
/// the active `QualityProfile`. Behavior is intentionally identical to
|
|
/// the pre-swap audiopus-based decoder at this phase — DRED reconstruction
|
|
/// lands in Phase 3.
|
|
pub struct OpusDecoder {
|
|
inner: DecoderHandle,
|
|
codec_id: CodecId,
|
|
frame_duration_ms: u8,
|
|
}
|
|
|
|
impl OpusDecoder {
|
|
/// Create a new Opus decoder for the given quality profile.
|
|
pub fn new(profile: QualityProfile) -> Result<Self, CodecError> {
|
|
let inner = DecoderHandle::new()?;
|
|
Ok(Self {
|
|
inner,
|
|
codec_id: profile.codec,
|
|
frame_duration_ms: profile.frame_duration_ms,
|
|
})
|
|
}
|
|
|
|
/// Expected number of output PCM samples per frame.
|
|
pub fn frame_samples(&self) -> usize {
|
|
(48_000 * self.frame_duration_ms as usize) / 1000
|
|
}
|
|
}
|
|
|
|
impl AudioDecoder for OpusDecoder {
|
|
fn decode(&mut self, encoded: &[u8], pcm: &mut [i16]) -> Result<usize, CodecError> {
|
|
let expected = self.frame_samples();
|
|
if pcm.len() < expected {
|
|
return Err(CodecError::DecodeFailed(format!(
|
|
"output buffer too small: need {expected}, got {}",
|
|
pcm.len()
|
|
)));
|
|
}
|
|
self.inner.decode(encoded, pcm)
|
|
}
|
|
|
|
fn decode_lost(&mut self, pcm: &mut [i16]) -> Result<usize, CodecError> {
|
|
let expected = self.frame_samples();
|
|
if pcm.len() < expected {
|
|
return Err(CodecError::DecodeFailed(format!(
|
|
"output buffer too small: need {expected}, got {}",
|
|
pcm.len()
|
|
)));
|
|
}
|
|
self.inner.decode_lost(pcm)
|
|
}
|
|
|
|
fn codec_id(&self) -> CodecId {
|
|
self.codec_id
|
|
}
|
|
|
|
fn set_profile(&mut self, profile: QualityProfile) -> Result<(), CodecError> {
|
|
match profile.codec {
|
|
c if c.is_opus() => {
|
|
self.codec_id = profile.codec;
|
|
self.frame_duration_ms = profile.frame_duration_ms;
|
|
Ok(())
|
|
}
|
|
other => Err(CodecError::UnsupportedTransition {
|
|
from: self.codec_id,
|
|
to: other,
|
|
}),
|
|
}
|
|
}
|
|
}
|