Adds three new codec IDs (Opus32k=6, Opus48k=7, Opus64k=8) and corresponding STUDIO_32K, STUDIO_48K, STUDIO_64K quality profiles. All use 20ms frames with minimal FEC (10%) for maximum quality on good networks. Updated across: wire protocol (codec_id.rs), encoder/decoder (opus_enc/dec.rs), adaptive codec switch (call.rs), CLI (--profile studio-64k), desktop engine + UI slider (8 quality levels from Studio 64k green to Codec2 1.2k red). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.1 KiB
Rust
94 lines
3.1 KiB
Rust
//! Opus decoder wrapping the `audiopus` crate.
|
|
|
|
use audiopus::coder::Decoder;
|
|
use audiopus::{Channels, MutSignals, SampleRate};
|
|
use audiopus::packet::Packet;
|
|
use wzp_proto::{AudioDecoder, CodecError, CodecId, QualityProfile};
|
|
|
|
/// Opus decoder implementing `AudioDecoder`.
|
|
///
|
|
/// Operates at 48 kHz mono output.
|
|
pub struct OpusDecoder {
|
|
inner: Decoder,
|
|
codec_id: CodecId,
|
|
frame_duration_ms: u8,
|
|
}
|
|
|
|
// SAFETY: Same reasoning as OpusEncoder — exclusive access via &mut self.
|
|
unsafe impl Sync for OpusDecoder {}
|
|
|
|
impl OpusDecoder {
|
|
/// Create a new Opus decoder for the given quality profile.
|
|
pub fn new(profile: QualityProfile) -> Result<Self, CodecError> {
|
|
let decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono)
|
|
.map_err(|e| CodecError::DecodeFailed(format!("opus decoder init: {e}")))?;
|
|
|
|
Ok(Self {
|
|
inner: decoder,
|
|
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()
|
|
)));
|
|
}
|
|
let packet = Packet::try_from(encoded)
|
|
.map_err(|e| CodecError::DecodeFailed(format!("invalid packet: {e}")))?;
|
|
let signals = MutSignals::try_from(pcm)
|
|
.map_err(|e| CodecError::DecodeFailed(format!("output signals: {e}")))?;
|
|
let n = self
|
|
.inner
|
|
.decode(Some(packet), signals, false)
|
|
.map_err(|e| CodecError::DecodeFailed(format!("opus decode: {e}")))?;
|
|
Ok(n)
|
|
}
|
|
|
|
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()
|
|
)));
|
|
}
|
|
let signals = MutSignals::try_from(pcm)
|
|
.map_err(|e| CodecError::DecodeFailed(format!("output signals: {e}")))?;
|
|
let n = self
|
|
.inner
|
|
.decode(None, signals, false)
|
|
.map_err(|e| CodecError::DecodeFailed(format!("opus PLC: {e}")))?;
|
|
Ok(n)
|
|
}
|
|
|
|
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,
|
|
}),
|
|
}
|
|
}
|
|
}
|