From 073756ed4b879929a5b0b9a5a3189889810d2777 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Tue, 7 Apr 2026 15:25:24 +0400 Subject: [PATCH] fix: auto-switch decoder codec to match incoming packets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CallDecoder now inspects each incoming packet's codec_id and automatically switches the audio decoder if it differs from the current profile. This enables cross-codec interop where one client sends Opus and the other sends Codec2 — previously the receiver would try to decode with the wrong codec, producing garbled audio. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-client/src/call.rs | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/wzp-client/src/call.rs b/crates/wzp-client/src/call.rs index 6f764e9..b6520c0 100644 --- a/crates/wzp-client/src/call.rs +++ b/crates/wzp-client/src/call.rs @@ -500,6 +500,49 @@ impl CallDecoder { } } + /// Switch the decoder to match an incoming packet's codec if it differs + /// from the current profile. This enables cross-codec interop (e.g. one + /// client sends Opus, the other sends Codec2). + fn switch_decoder_if_needed(&mut self, incoming_codec: CodecId) { + if incoming_codec == self.profile.codec || incoming_codec == CodecId::ComfortNoise { + return; + } + let new_profile = Self::profile_for_codec(incoming_codec); + info!( + from = ?self.profile.codec, + to = ?incoming_codec, + "decoder switching codec to match incoming packet" + ); + if let Err(e) = self.audio_dec.set_profile(new_profile) { + warn!("failed to switch decoder profile: {e}"); + return; + } + self.fec_dec = wzp_fec::create_decoder(&new_profile); + self.profile = new_profile; + } + + /// Map a `CodecId` to a reasonable `QualityProfile` for decoding. + fn profile_for_codec(codec: CodecId) -> QualityProfile { + match codec { + CodecId::Opus24k => QualityProfile::GOOD, + CodecId::Opus16k => QualityProfile { + codec: CodecId::Opus16k, + fec_ratio: 0.3, + frame_duration_ms: 20, + frames_per_block: 5, + }, + CodecId::Opus6k => QualityProfile::DEGRADED, + CodecId::Codec2_3200 => QualityProfile { + codec: CodecId::Codec2_3200, + fec_ratio: 0.5, + frame_duration_ms: 20, + frames_per_block: 5, + }, + CodecId::Codec2_1200 => QualityProfile::CATASTROPHIC, + CodecId::ComfortNoise => QualityProfile::GOOD, + } + } + /// Decode the next audio frame from the jitter buffer. /// /// Returns PCM samples (48kHz mono) or None if not ready. @@ -514,6 +557,9 @@ impl CallDecoder { return Some(pcm.len()); } + // Auto-switch decoder if incoming codec differs from current. + self.switch_decoder_if_needed(pkt.header.codec_id); + self.last_was_cn = false; let result = match self.audio_dec.decode(&pkt.payload, pcm) { Ok(n) => Some(n),