From a8c201144592f9ad9bd7f97e4186cfd144cdd5e8 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Tue, 7 Apr 2026 18:31:05 +0400 Subject: [PATCH] feat: add Opus 32k/48k/64k studio quality tiers 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) --- crates/wzp-client/src/call.rs | 3 ++ crates/wzp-client/src/cli.rs | 5 +++- crates/wzp-codec/src/opus_dec.rs | 2 +- crates/wzp-codec/src/opus_enc.rs | 2 +- crates/wzp-proto/src/codec_id.rs | 48 ++++++++++++++++++++++++++++++-- desktop/index.html | 13 +++++---- desktop/src-tauri/src/engine.rs | 6 ++++ desktop/src/main.ts | 14 ++++------ 8 files changed, 74 insertions(+), 19 deletions(-) diff --git a/crates/wzp-client/src/call.rs b/crates/wzp-client/src/call.rs index b6520c0..9605230 100644 --- a/crates/wzp-client/src/call.rs +++ b/crates/wzp-client/src/call.rs @@ -532,6 +532,9 @@ impl CallDecoder { frames_per_block: 5, }, CodecId::Opus6k => QualityProfile::DEGRADED, + CodecId::Opus32k => QualityProfile::STUDIO_32K, + CodecId::Opus48k => QualityProfile::STUDIO_48K, + CodecId::Opus64k => QualityProfile::STUDIO_64K, CodecId::Codec2_3200 => QualityProfile { codec: CodecId::Codec2_3200, fec_ratio: 0.5, diff --git a/crates/wzp-client/src/cli.rs b/crates/wzp-client/src/cli.rs index 6b767bb..144d6df 100644 --- a/crates/wzp-client/src/cli.rs +++ b/crates/wzp-client/src/cli.rs @@ -133,9 +133,12 @@ fn resolve_profile(name: &str) -> wzp_proto::QualityProfile { frame_duration_ms: 20, frames_per_block: 5, }, + "studio-32k" | "opus32k" | "32k" => QualityProfile::STUDIO_32K, + "studio-48k" | "opus48k" | "48k" | "studio" => QualityProfile::STUDIO_48K, + "studio-64k" | "opus64k" | "64k" | "studio-high" => QualityProfile::STUDIO_64K, other => { eprintln!("unknown profile: {other}"); - eprintln!("valid: good, degraded, catastrophic, codec2-3200, codec2-1200"); + eprintln!("valid: good, degraded, catastrophic, codec2-3200, codec2-1200, studio-32k, studio-48k, studio-64k"); std::process::exit(1); } } diff --git a/crates/wzp-codec/src/opus_dec.rs b/crates/wzp-codec/src/opus_dec.rs index 36593af..c8b6cd4 100644 --- a/crates/wzp-codec/src/opus_dec.rs +++ b/crates/wzp-codec/src/opus_dec.rs @@ -79,7 +79,7 @@ impl AudioDecoder for OpusDecoder { fn set_profile(&mut self, profile: QualityProfile) -> Result<(), CodecError> { match profile.codec { - CodecId::Opus24k | CodecId::Opus16k | CodecId::Opus6k => { + c if c.is_opus() => { self.codec_id = profile.codec; self.frame_duration_ms = profile.frame_duration_ms; Ok(()) diff --git a/crates/wzp-codec/src/opus_enc.rs b/crates/wzp-codec/src/opus_enc.rs index 41534de..1a5dca1 100644 --- a/crates/wzp-codec/src/opus_enc.rs +++ b/crates/wzp-codec/src/opus_enc.rs @@ -100,7 +100,7 @@ impl AudioEncoder for OpusEncoder { fn set_profile(&mut self, profile: QualityProfile) -> Result<(), CodecError> { match profile.codec { - CodecId::Opus24k | CodecId::Opus16k | CodecId::Opus6k => { + c if c.is_opus() => { self.codec_id = profile.codec; self.frame_duration_ms = profile.frame_duration_ms; self.apply_bitrate(profile.codec)?; diff --git a/crates/wzp-proto/src/codec_id.rs b/crates/wzp-proto/src/codec_id.rs index 2c09cc5..d90c3a0 100644 --- a/crates/wzp-proto/src/codec_id.rs +++ b/crates/wzp-proto/src/codec_id.rs @@ -18,6 +18,12 @@ pub enum CodecId { Codec2_1200 = 4, /// Comfort noise descriptor (silence suppression) ComfortNoise = 5, + /// Opus at 32kbps (studio low) + Opus32k = 6, + /// Opus at 48kbps (studio) + Opus48k = 7, + /// Opus at 64kbps (studio high) + Opus64k = 8, } impl CodecId { @@ -27,6 +33,9 @@ impl CodecId { Self::Opus24k => 24_000, Self::Opus16k => 16_000, Self::Opus6k => 6_000, + Self::Opus32k => 32_000, + Self::Opus48k => 48_000, + Self::Opus64k => 64_000, Self::Codec2_3200 => 3_200, Self::Codec2_1200 => 1_200, Self::ComfortNoise => 0, @@ -36,8 +45,7 @@ impl CodecId { /// Preferred frame duration in milliseconds. pub const fn frame_duration_ms(self) -> u8 { match self { - Self::Opus24k => 20, - Self::Opus16k => 20, + Self::Opus24k | Self::Opus16k | Self::Opus32k | Self::Opus48k | Self::Opus64k => 20, Self::Opus6k => 40, Self::Codec2_3200 => 20, Self::Codec2_1200 => 40, @@ -48,7 +56,8 @@ impl CodecId { /// Sample rate expected by this codec. pub const fn sample_rate_hz(self) -> u32 { match self { - Self::Opus24k | Self::Opus16k | Self::Opus6k => 48_000, + Self::Opus24k | Self::Opus16k | Self::Opus6k + | Self::Opus32k | Self::Opus48k | Self::Opus64k => 48_000, Self::Codec2_3200 | Self::Codec2_1200 => 8_000, Self::ComfortNoise => 48_000, } @@ -63,6 +72,9 @@ impl CodecId { 3 => Some(Self::Codec2_3200), 4 => Some(Self::Codec2_1200), 5 => Some(Self::ComfortNoise), + 6 => Some(Self::Opus32k), + 7 => Some(Self::Opus48k), + 8 => Some(Self::Opus64k), _ => None, } } @@ -71,6 +83,12 @@ impl CodecId { pub const fn to_wire(self) -> u8 { self as u8 } + + /// Returns true if this is an Opus variant. + pub const fn is_opus(self) -> bool { + matches!(self, Self::Opus6k | Self::Opus16k | Self::Opus24k + | Self::Opus32k | Self::Opus48k | Self::Opus64k) + } } /// Describes the complete quality configuration for a call session. @@ -111,6 +129,30 @@ impl QualityProfile { frames_per_block: 8, }; + /// Studio low: Opus 32kbps, minimal FEC. + pub const STUDIO_32K: Self = Self { + codec: CodecId::Opus32k, + fec_ratio: 0.1, + frame_duration_ms: 20, + frames_per_block: 5, + }; + + /// Studio: Opus 48kbps, minimal FEC. + pub const STUDIO_48K: Self = Self { + codec: CodecId::Opus48k, + fec_ratio: 0.1, + frame_duration_ms: 20, + frames_per_block: 5, + }; + + /// Studio high: Opus 64kbps, minimal FEC. + pub const STUDIO_64K: Self = Self { + codec: CodecId::Opus64k, + fec_ratio: 0.1, + frame_duration_ms: 20, + frames_per_block: 5, + }; + /// Estimated total bandwidth in kbps including FEC overhead. pub fn total_bitrate_kbps(&self) -> f32 { let base = self.codec.bitrate_bps() as f32 / 1000.0; diff --git a/desktop/index.html b/desktop/index.html index ea99676..963da01 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -96,13 +96,16 @@ QUALITY Auto - +
+ 64k + 48k + 32k Auto - Opus 24k - Opus 6k - C2 3.2k - C2 1.2k + 24k + 6k + C2 + 1.2k