"You're muted" nudge never fires: the shared mic sampler restarts every ~1 s and each restart resets speakingWhileMuted #45

Open
opened 2026-10-05 01:34:34 -04:00 by jared · 0 comments
Owner

Symptom: in a voice room with a continuous tone as the fake mic (--use-file-for-fake-audio-capture, 220 Hz, about −8 dBFS), I muted and waited 10 s. The host's "You're muted — click to unmute" toast (cinny #117, useMutedTalkWarning) never appeared. The setting mutedTalkWarning is on, PTT is off, and the call is joined.

Cause. observeLocalMicSample$ (src/lotus/lotusMicLevel.ts) is vm.allConnections$.pipe(switchMap(() => new Observable(...)), share()). Every emission of allConnections$ (connectionManager.connectionManagerData$.pipe(map(d => d.value))) tears the tap down: stopTap() stops the clone, closes the AudioContext and emits null. It then builds a new clone and a new AudioContext. I traced MediaStreamTrack.clone/stop inside the call frame:

1.9 stop c9c29f · clone of 675609 → 36ba85      (before mute)
2.3 stop 36ba85 · clone → 2c4ebd
2.7 stop 2c4ebd · clone → 65903b
3.5 stop 65903b · clone → f2fb6a
4.2 enabled=false on 675609   (mute)  · stop f2fb6a · clone → 7764c2 · clone → 5011da
5.1 stop 7764c2 · clone → f09951
6.1 stop f09951 · clone → 9448e1

The source track (675609) never changes, yet the sampler restarts every 0.4–1 s.

  • In observeSpeakingWhileMuted$ (lotusMutedSpeech.ts), a null sample resets the MutedSpeechGate, so the flag is dropped on every restart. It needs 300 ms of voice to come back on.
  • The host needs speakingWhileMuted to stay true for 1.5 s straight (HOLD_MS, polled every 250 ms, and call_state is throttled to 500 ms).
  • The flag therefore never stays true long enough, and the nudge can't fire.

The same churn creates and closes an AudioContext about once a second for the whole call. The host's mic meter mostly hides it thanks to its hysteresis, but it dips to one bar now and then.

The nudge was verified when it landed (c2267800). The shared sampler (cee6bab9, cinny #146) is where the null-on-restart path sits. This stack also logs Failed to set E2EE enabled on room … e2ee not configured on join, which may be why connectionManagerData$ re-emits so often here. Prod may differ in rate, but any re-emission resets the gate.

Fix:

  • Restart only when the set of LiveKit rooms changes: map(d => d.getConnections().map(c => c.livekitRoom)), then distinctUntilChanged(sameRooms), before the switchMap.
  • Don't emit null when re-tapping the same source track (keep tapped across restarts, or have reconcile reuse it).
  • Optionally let the gate tolerate a single null.
  • Test: a connectionManagerData$ re-emission with the same rooms doesn't stop the clone, and a continuous tone while muted keeps speakingWhileMuted true for more than 2 s.

Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny 30e2dbcd, cinny-desktop 8f67ec7, element-call 3b0050e8.

**Symptom:** in a voice room with a continuous tone as the fake mic (`--use-file-for-fake-audio-capture`, 220 Hz, about −8 dBFS), I muted and waited 10 s. The host's **"You're muted — click to unmute"** toast (cinny #117, `useMutedTalkWarning`) never appeared. The setting `mutedTalkWarning` is on, PTT is off, and the call is joined. **Cause.** `observeLocalMicSample$` (`src/lotus/lotusMicLevel.ts`) is `vm.allConnections$.pipe(switchMap(() => new Observable(...)), share())`. Every emission of `allConnections$` (`connectionManager.connectionManagerData$.pipe(map(d => d.value))`) tears the tap down: `stopTap()` stops the clone, closes the `AudioContext` and emits `null`. It then builds a new clone and a new `AudioContext`. I traced `MediaStreamTrack.clone/stop` inside the call frame: ``` 1.9 stop c9c29f · clone of 675609 → 36ba85 (before mute) 2.3 stop 36ba85 · clone → 2c4ebd 2.7 stop 2c4ebd · clone → 65903b 3.5 stop 65903b · clone → f2fb6a 4.2 enabled=false on 675609 (mute) · stop f2fb6a · clone → 7764c2 · clone → 5011da 5.1 stop 7764c2 · clone → f09951 6.1 stop f09951 · clone → 9448e1 ``` The source track (`675609`) never changes, yet the sampler restarts every 0.4–1 s. - In `observeSpeakingWhileMuted$` (`lotusMutedSpeech.ts`), a `null` sample resets the `MutedSpeechGate`, so the flag is dropped on every restart. It needs 300 ms of voice to come back on. - The host needs `speakingWhileMuted` to stay true for 1.5 s straight (`HOLD_MS`, polled every 250 ms, and `call_state` is throttled to 500 ms). - The flag therefore never stays true long enough, and the nudge can't fire. The same churn creates and closes an `AudioContext` about once a second for the whole call. The host's mic meter mostly hides it thanks to its hysteresis, but it dips to one bar now and then. The nudge was verified when it landed (c2267800). The shared sampler (cee6bab9, cinny #146) is where the `null`-on-restart path sits. This stack also logs `Failed to set E2EE enabled on room … e2ee not configured` on join, which may be why `connectionManagerData$` re-emits so often here. Prod may differ in rate, but any re-emission resets the gate. **Fix:** - Restart only when the set of LiveKit rooms changes: `map(d => d.getConnections().map(c => c.livekitRoom))`, then `distinctUntilChanged(sameRooms)`, before the `switchMap`. - Don't emit `null` when re-tapping the same source track (keep `tapped` across restarts, or have `reconcile` reuse it). - Optionally let the gate tolerate a single `null`. - Test: a `connectionManagerData$` re-emission with the same rooms doesn't stop the clone, and a continuous tone while muted keeps `speakingWhileMuted` true for more than 2 s. --- _Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny `30e2dbcd`, cinny-desktop `8f67ec7`, element-call `3b0050e8`._
jared added the bugpriority: mediumarea: audio-pipeline labels 2026-10-05 01:34:34 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/element-call#45