[research] Mic level meter in the call bar while unmuted — what exists today? #146

Closed
opened 2026-09-17 16:51:58 -04:00 by jared · 2 comments
Owner

Inventory: does the EC footer show a local mic level (upstream has a speaking indicator on the self tile); does cinny's call bar / PiP show anything; does io.lotus.call_state carry a level or only a boolean speaking? Then propose the smallest addition (likely: a 3-bar glyph on the cinny mute button fed by a level field, only while unmuted). Decide after the write-up.

Inventory: does the EC footer show a local mic level (upstream has a speaking indicator on the self tile); does cinny's call bar / PiP show anything; does `io.lotus.call_state` carry a level or only a boolean `speaking`? Then propose the smallest addition (likely: a 3-bar glyph on the cinny mute button fed by a `level` field, only while unmuted). Decide after the write-up.
jared added this to the Features 2026-Q4 milestone 2026-09-17 16:51:58 -04:00
jared added the enhancementpriority: lowarea: callsresearch labels 2026-09-17 16:51:58 -04:00
jared self-assigned this 2026-09-17 16:51:58 -04:00
Author
Owner

Inventory (2026-09-20)

Element Call (fork) — what exists

  • Self tile: vm.speaking$ → green speaking border (GridTile/MediaView.speakingBorder), sound-wave glyph in spotlight. Source is LiveKit's boolean participant.isSpeaking (SFU active-speaker updates, ~300 ms cadence, delta-only).
  • LiveKit also exposes participant.audioLevel (0–1) but it is only refreshed by the same active-speaker updates — coarse, not a meter.
  • src/lotus/lotusMutedSpeech.ts (#117) already runs a local AnalyserNode RMS sampler on the mic track — but only while muted, feeding speakingWhileMuted.
  • io.lotus.call_state participants carry speaking: boolean (+ speakingWhileMuted for self). No level field. The stream is throttled to 500 ms trailing (#20) — far too slow for a meter even if a level were added.

Cinny — what exists

  • Call bar MicrophoneButton (features/call-status/CallControl.tsx): plain Mic/MicMute icon, no activity feedback at all.
  • Call bar text line MemberSpeaking: "Name is speaking…" driven by the boolean stream (includes yourself), and MemberGlance promotes speaking avatars.
  • PiP and the room header call chip: nothing per-level.
  • Muted-talk toast (#117) is the only self-mic feedback, and only while muted.

So today: boolean only, 500 ms late, and nothing on the mute button itself.

Smallest useful addition (proposal)

  1. Fork: generalise the #117 sampler into observeLocalMicLevel$ that runs whenever a mic track exists (muted or not), RMS → quantised 0–3 bars with a little hysteresis. Emit a new fromWidget action io.lotus.mic_level { bars } only when the quantised value changes, capped at 10 Hz. Silence and steady speech therefore cost ~0 messages; active talking costs a handful per second. Keep call_state untouched (its 500 ms throttle stays right for what it does).
  2. Cinny: CallEmbed handles io.lotus.mic_level → micLevelAtom. MicrophoneButton overlays a 3-bar glyph (three 2×{3,5,7}px bars in the icon's bottom-right corner, Primary colour) only while enabled && bars > 0. Nothing else changes; PiP could reuse the same atom later.
  3. Reuse: the same level lets #117 drop its separate muted-only sampler (same signal, bars>0 && muted = talking while muted).

Cost: ~60 lines fork + ~50 lines cinny + a fork release. Alternative "zero-fork" option: pulse the mic icon from the existing boolean speaking for self — free, but it is a yes/no indicator, not a meter, and lags ~0.5 s.

Recommendation: do (1)+(2). Waiting for a go/no-go before building — this is the "decide after the write-up" gate from the issue.

## Inventory (2026-09-20) **Element Call (fork) — what exists** - Self tile: `vm.speaking$` → green speaking border (`GridTile`/`MediaView.speakingBorder`), sound-wave glyph in spotlight. Source is LiveKit's **boolean** `participant.isSpeaking` (SFU active-speaker updates, ~300 ms cadence, delta-only). - LiveKit also exposes `participant.audioLevel` (0–1) but it is only refreshed by the same active-speaker updates — coarse, not a meter. - `src/lotus/lotusMutedSpeech.ts` (#117) already runs a **local AnalyserNode RMS sampler** on the mic track — but only while *muted*, feeding `speakingWhileMuted`. - `io.lotus.call_state` participants carry `speaking: boolean` (+ `speakingWhileMuted` for self). **No level field.** The stream is throttled to 500 ms trailing (#20) — far too slow for a meter even if a level were added. **Cinny — what exists** - Call bar `MicrophoneButton` (`features/call-status/CallControl.tsx`): plain Mic/MicMute icon, no activity feedback at all. - Call bar text line `MemberSpeaking`: "*Name* is speaking…" driven by the boolean stream (includes yourself), and `MemberGlance` promotes speaking avatars. - PiP and the room header call chip: nothing per-level. - Muted-talk toast (#117) is the only self-mic feedback, and only while muted. So today: **boolean only, 500 ms late, and nothing on the mute button itself.** ## Smallest useful addition (proposal) 1. **Fork:** generalise the #117 sampler into `observeLocalMicLevel$` that runs whenever a mic track exists (muted or not), RMS → quantised **0–3 bars** with a little hysteresis. Emit a new fromWidget action `io.lotus.mic_level { bars }` **only when the quantised value changes**, capped at 10 Hz. Silence and steady speech therefore cost ~0 messages; active talking costs a handful per second. Keep `call_state` untouched (its 500 ms throttle stays right for what it does). 2. **Cinny:** `CallEmbed` handles `io.lotus.mic_level` → `micLevelAtom`. `MicrophoneButton` overlays a 3-bar glyph (three 2×{3,5,7}px bars in the icon's bottom-right corner, Primary colour) only while `enabled && bars > 0`. Nothing else changes; PiP could reuse the same atom later. 3. Reuse: the same level lets #117 drop its separate muted-only sampler (same signal, `bars>0 && muted` = talking while muted). Cost: ~60 lines fork + ~50 lines cinny + a fork release. Alternative "zero-fork" option: pulse the mic icon from the existing boolean `speaking` for self — free, but it is a yes/no indicator, not a meter, and lags ~0.5 s. **Recommendation:** do (1)+(2). Waiting for a go/no-go before building — this is the "decide after the write-up" gate from the issue.
jared added the needs-human-review label 2026-09-20 00:31:04 -04:00
Author
Owner

Built (fork v0.25.0-lotus.16, cinny 3449338b + 06bb896a)

Fork

  • One shared local mic sampler (lotusMicLevel.ts). It taps a clone of the published track at about 10 Hz whenever a mic is published, muted or not. There is one AudioContext per call.
  • The level is quantised to 0–3 bars (about −46, −36 and −26 dBFS). A bar lights immediately and drops by one per sample, so the meter doesn't flicker between words.
  • fromWidget io.lotus.mic_level { bars } is sent only when the value changes. It is 0 while muted or with no mic. call_state is untouched.
  • "Talking while muted" (#117) now reads the same sampler, with the same gate and behaviour.

Lotus

  • Three small green bars appear in the corner of the mic button, in both the call view and the bottom call bar, only while unmuted and speaking. They are decorative, so the button's label is unchanged.
  • Per Jared's call, you are no longer listed in the "… is speaking" line, since your own voice is on the meter now. Avatar speaking rings still include you, and the "You're muted" notice is unchanged.

Verified in a local call with a fake-tone mic:

  • The bars cycle 3/2/1 with the tone on both buttons, disappear on mute and come back on unmute.
  • Messages go out only on change, about 4 a second while the level moves.
  • The status line reads "bob is speaking…" only.
  • Muting while the tone plays still raises speakingWhileMuted and "You're muted — click to unmute".

Tests: quantiser unit tests. The fork's lotus and room suites pass on Node 22 (148), and cinny's unit tests pass (1,235).

meter

## Built (fork `v0.25.0-lotus.16`, cinny `3449338b` + `06bb896a`) **Fork** - One shared local mic sampler (`lotusMicLevel.ts`). It taps a clone of the published track at about 10 Hz whenever a mic is published, muted or not. There is one AudioContext per call. - The level is quantised to **0–3 bars** (about −46, −36 and −26 dBFS). A bar lights immediately and drops by one per sample, so the meter doesn't flicker between words. - fromWidget `io.lotus.mic_level { bars }` is sent **only when the value changes**. It is 0 while muted or with no mic. `call_state` is untouched. - "Talking while muted" (#117) now reads the same sampler, with the same gate and behaviour. **Lotus** - Three small green bars appear in the corner of the mic button, in both the call view and the bottom call bar, only while unmuted and speaking. They are decorative, so the button's label is unchanged. - Per Jared's call, **you are no longer listed in the "… is speaking" line**, since your own voice is on the meter now. Avatar speaking rings still include you, and the "You're muted" notice is unchanged. **Verified** in a local call with a fake-tone mic: - The bars cycle 3/2/1 with the tone on both buttons, disappear on mute and come back on unmute. - Messages go out only on change, about 4 a second while the level moves. - The status line reads "bob is speaking…" only. - Muting while the tone plays still raises `speakingWhileMuted` and "You're muted — click to unmute". **Tests:** quantiser unit tests. The fork's lotus and room suites pass on Node 22 (148), and cinny's unit tests pass (1,235). ![meter](https://code.lotusguild.org/attachments/527ef15e-cb78-4b97-a47a-11c2bd76dda8)
jared closed this issue 2026-09-25 12:14:43 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#146