Skip to content

Commit c04a344

Browse files
committed
add uv meter
1 parent f7dee50 commit c04a344

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

examples/wakeword_agent_dispatch/whisplay_wakeword_client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import signal
99
import subprocess
10+
import time
1011
import wave
1112
from collections import deque
1213
from dataclasses import dataclass
@@ -25,6 +26,9 @@
2526
OUTPUT_FRAME_SAMPLES = 480 # 10 ms at 48 kHz
2627
PRE_CONNECT_AUDIO_BUFFER_TOPIC = "lk.agent.pre-connect-audio-buffer"
2728
DEFAULT_WM8960_DEVICE_NAME = "wm8960"
29+
MIC_VU_INTERVAL_SECONDS = 0.5
30+
MIC_VU_BAR_WIDTH = 24
31+
MIC_VU_FLOOR_DBFS = -60.0
2832

2933
logger = logging.getLogger("whisplay-wakeword-client")
3034

@@ -213,6 +217,61 @@ class MicChunk:
213217
samples_per_channel: int
214218

215219

220+
class MicVuMeter:
221+
def __init__(self) -> None:
222+
self._sum_squares = 0.0
223+
self._sample_count = 0
224+
self._peak = 0.0
225+
self._next_log_at = time.monotonic() + MIC_VU_INTERVAL_SECONDS
226+
227+
def add(self, chunk: MicChunk) -> None:
228+
samples = np.frombuffer(chunk.data, dtype=np.int16).astype(np.float32)
229+
if samples.size == 0:
230+
return
231+
232+
self._sum_squares += float(np.dot(samples, samples))
233+
self._sample_count += int(samples.size)
234+
self._peak = max(self._peak, float(np.max(np.abs(samples))))
235+
236+
now = time.monotonic()
237+
if now < self._next_log_at:
238+
return
239+
240+
self._log_level()
241+
self._sum_squares = 0.0
242+
self._sample_count = 0
243+
self._peak = 0.0
244+
self._next_log_at = now + MIC_VU_INTERVAL_SECONDS
245+
246+
def _log_level(self) -> None:
247+
if self._sample_count == 0:
248+
return
249+
250+
rms = float(np.sqrt(self._sum_squares / self._sample_count))
251+
rms_dbfs = _amplitude_to_dbfs(rms)
252+
peak_dbfs = _amplitude_to_dbfs(self._peak)
253+
fill = _dbfs_to_bar_fill(rms_dbfs)
254+
bar = "#" * fill + "-" * (MIC_VU_BAR_WIDTH - fill)
255+
logger.info(
256+
"mic VU [%s] rms=%5.1f dBFS peak=%5.1f dBFS",
257+
bar,
258+
rms_dbfs,
259+
peak_dbfs,
260+
)
261+
262+
263+
def _amplitude_to_dbfs(amplitude: float) -> float:
264+
if amplitude <= 0:
265+
return MIC_VU_FLOOR_DBFS
266+
267+
return float(max(MIC_VU_FLOOR_DBFS, 20.0 * np.log10(amplitude / 32768.0)))
268+
269+
270+
def _dbfs_to_bar_fill(dbfs: float) -> int:
271+
normalized = (dbfs - MIC_VU_FLOOR_DBFS) / abs(MIC_VU_FLOOR_DBFS)
272+
return round(max(0.0, min(1.0, normalized)) * MIC_VU_BAR_WIDTH)
273+
274+
216275
class PreconnectAudioBuffer:
217276
def __init__(self, max_duration: float) -> None:
218277
self._max_samples = self._duration_to_samples(max_duration)
@@ -549,6 +608,7 @@ async def _run_audio_loop(
549608
preconnect_buffer = PreconnectAudioBuffer(config.wakeword_preroll_seconds)
550609
wakeword_window = WakeWordAudioWindow(WAKEWORD_WINDOW_SAMPLES)
551610
wakeword_resampler = rtc.AudioResampler(SAMPLE_RATE, WAKEWORD_SAMPLE_RATE)
611+
mic_vu_meter = MicVuMeter()
552612
state = ClientState()
553613

554614
def _reset_to_idle(reason: str, *, cancel_dispatch: bool = True) -> None:
@@ -636,6 +696,8 @@ def _on_dispatch_done(task: asyncio.Task[str]) -> None:
636696
logger.info("audio loop stopping (shutdown requested)")
637697
break
638698

699+
mic_vu_meter.add(chunk)
700+
639701
if state.mode in (ClientMode.IDLE, ClientMode.DISPATCHING):
640702
preconnect_buffer.append(chunk)
641703

0 commit comments

Comments
 (0)