fix(android): a mute is a pause, not a hole the host tries to conceal

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 23:15:03 +02:00
co-authored by Claude Fable 5
parent 0c4a543831
commit 8e877ad25f
+8 -9
View File
@@ -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;