| name | voice-meter |
| description | Trace, explain, diagnose, test, and modify Voice Layer's Live microphone level meter, including native RMS aggregation, the typed listeningLevel event, rolling level history, waveform transfer curve, responsive width, and Live control-row layout. Use when changing how the meter rises, moves, spikes, fills space, responds to voice volume, interacts with Mute/End controls, or needs visual/native validation. |
Voice Meter Development
Use this skill to work on the Live listening meter without confusing three separate concerns:
- input loudness: native RMS aggregated from every unmuted microphone buffer;
- waveform motion: the frontend history of those measured RMS windows;
- speech detection: Silero VAD's separate SpeechStart/turn lifecycle.
Read references/architecture.md when the request requires a source map, event contract, formula, or ownership boundary.
Source-to-screen workflow
-
Inspect the current worktree first. Preserve unrelated dirty changes and do not reset or rebuild over them.
-
Trace the current path with symbol searches before editing:
rg -n "ListeningLevel|listeningLevel|InputLevelMeter|levelHistorySteps|meterCellsForWidth|live-info-row|live-action-row|live-level|live-controls" src/live.ts src/style.css src/main.ts src-tauri/src/live tests
-
Classify the requested change before touching code:
| Request | Primary owner |
|---|
| Meter rises with louder/quieter input | src-tauri/src/live/mod.rs RMS source and src/live.ts transfer curve |
| Wave moves faster/slower | Native listeningLevel cadence; keep it audio-driven |
| More/less spiky shape | src/live.ts levelHistorySteps and transfer curve |
| More/less vertical amplitude | src/style.css .live-level height |
| Longer meter | meterCellsForWidth plus .live-level width |
| Arrange status/countdown or avoid control overlap | .live-info-row/.live-action-row layout and renderMeter remeasurement |
| Speech starts too early/late | Silero VAD settings in src-tauri/src/live/vad.rs; do not alter the meter |
-
Keep one owner for coupled changes. The native event contract and frontend rendering are coupled; change both only when the event meaning changes.
-
Add or update a focused test before broad validation.
Contracts to preserve
- Native observes every live microphone buffer and emits LiveEvent::ListeningLevel about every 50 ms while the Live microphone is unmuted. It carries a bounded scalar level in 0..=1.
- level is derived from the aggregate RMS of all microphone samples in that window, regardless of whether VAD is currently collecting an utterance. It is not Silero speech probability and it is not the SpeechStart event.
- TypeScript redraws only on a genuine listeningLevel event. Do not add a timer to fake microphone activity.
- frontend levelHistorySteps(levels, cells) owns the visible transfer curve and chronological rolling history. Every visible peak must originate in one native level event.
- A recorded bar's height is immutable. Never route the newest level through container-wide opacity, glow, scale, or another effect that changes historical bars.
- The display transfer uses a 0.34 visual noise floor, a 0.95 voice ceiling, a 0.9 contrast exponent, and 16 discrete bar heights. These are frontend presentation values only; they must not alter VAD or speech sensitivity.
- The meter uses at least 64 cells and expands from the actual .live-level width up to a bounded maximum. The .live-stage must remain flexible and .live-controls must remain fixed.
- The footer stacks an equal-column .live-info-row over .live-action-row: countdown stays left, Live status stays right, and the meter shares only its lower row with the fixed controls.
- The right-side control region reserves both Mute and End slots even when Mute is hidden. Re-render after state changes so the meter remeasures immediately when controls appear or disappear.
- VAD threshold/sensitivity changes speech detection, turn opening, and end-of-turn behavior. It must not be used as a substitute for an amplitude meter.
- Never send raw PCM through Tauri IPC, persist it, or log it.
Change recipes
Make the meter respond more to volume
Prefer changing the frontend transfer curve only after confirming native normalization is monotonic. Compare ambient, ordinary voice, and loud voice levels and require clear separation without making ambient input disappear from native processing. Keep zero input flat.
Change native normalized_level only when the capture scale or dB range is wrong. Preserve bounded output and add a Rust monotonicity/bounds test.
Make the wave move
Keep the native event cadence audio-driven and append each measured level to the frontend history. Never introduce an oscillator or phase to manufacture movement. Test that the meter remains unchanged without a native event and changes when a different measured level arrives.
Make the wave sharper or taller
Change the transfer curve in levelHistorySteps for peak contrast. A fixed .live-level height may change the whole meter's static visual size, but it must not depend on the newest level. Do not use either change to compensate for a broken volume scalar.
Make the wave longer without covering controls
Keep .live-control-bar as two stacked rows: an equal-column .live-info-row and a .live-action-row with flexible .live-stage plus fixed .live-controls. Adjust meterCellsForWidth bounds or the 9px cell estimate; keep .live-level width: 100% with hidden overflow. Never let the meter use viewport width directly.
Validation
Run the narrow checks first:
bun test tests/live-meter.test.ts tests/live.test.ts
bunx vite build
git diff --check
For native changes, also run the relevant Rust checks from the repository's supported justfile commands. For UI changes, inspect Live at the 960x680 minimum and the 1200x900 default size, with Mute hidden and visible. Verify:
- zero/silence produces a flat baseline;
- louder input produces taller peaks, not merely faster motion;
- changing input produces a moving history made only from measured events;
- the meter ends before the reserved Mute/End control region;
- mute, unmute, End Live, and resize do not leave stale width or stale level content.
Treat a Vite bundle or automated test as source/build evidence only. Packaged/native microphone behavior and subjective visual quality need separate native inspection.