From 8e877ad25fe2bbfd7cbad311153ae8059b776e3d Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 31 Jul 2026 23:15:03 +0200 Subject: [PATCH] fix(android): a mute is a pause, not a hole the host tries to conceal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Android uplink kept advancing `seq` while muted, so the first frame after an unmute looked to the host like loss the width of the mute. The de-jitter reads that as a gap: up to five concealment frames of stale voice, and a seq gap counted in the uplink-health line. Past 600 ms the pump's stale flush resets the chain first and hides it, which is why the usual long mute looks fine — a quick toggle does not. Freeze `seq` while muted, as the desktop uplink already does, so the frame after an unmute continues the chain. `reset_stream` says it plainly: a pause is not loss, and must not conceal or count a gap. Co-Authored-By: Claude Fable 5 --- clients/android/native/src/mic.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/clients/android/native/src/mic.rs b/clients/android/native/src/mic.rs index 45d0547f..4db552b9 100644 --- a/clients/android/native/src/mic.rs +++ b/clients/android/native/src/mic.rs @@ -328,17 +328,16 @@ fn encode_loop( } while ring.len() >= frame { // Muted: drop the frame at the last point before it would become an Opus packet — - // room audio is never encoded and nothing goes on the wire. `seq` still advances, - // because it numbers the captured 10 ms TIMELINE rather than the datagrams: the gap - // the host then sees is exactly the audio that never came, which its de-jitter reads - // as at most a few concealment frames before the pump's 600 ms stale-gap flush resets - // the chain outright — the right reading of a mute. (Encoding silence instead would - // keep a pointless uplink and a host-side ring alive for the whole mute.) `peak` is - // the loudest sample the UPLINK carried since the last log, so a dropped frame resets - // rather than raises it. + // room audio is never encoded and nothing goes on the wire. `seq` does NOT advance: + // it numbers the datagrams the host de-jitters, and that side reads a seq jump as + // loss (conceal + a counted gap) where a mute is a pause. Freezing it means the + // frame after an unmute continues the chain, which is what the host's own + // `reset_stream` doc calls for and what the desktop uplink does. (Encoding silence + // instead would keep a pointless uplink and a host-side ring alive for the whole + // mute.) `peak` is the loudest sample the UPLINK carried since the last log, so a + // dropped frame resets rather than raises it. if muted.load(Ordering::Relaxed) { ring.drain(..frame); - seq = seq.wrapping_add(1); muted_frames += 1; peak = 0.0; continue;