From bcf4513cb32a462bb90d8a50611b198b7e38ff16 Mon Sep 17 00:00:00 2001 From: 0xbyt4 <35742124+0xbyt4@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:11:18 +0300 Subject: [PATCH] fix: add timeout to play_beep sd.wait and wrap silence callback in try-except MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace sd.wait() with a poll loop + sd.stop() in play_beep(). sd.wait() calls Event.wait() without timeout — hangs forever if the audio device stalls. Poll with a 2s ceiling and force-stop instead. - Wrap _on_silence callback in try-except so exceptions are logged instead of silently lost in the daemon thread. Prevents recording state from becoming inconsistent on unexpected errors. --- tools/voice_mode.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 2c3a168bd..151d81983 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -140,7 +140,12 @@ def play_beep(frequency: int = 880, duration: float = 0.12, count: int = 1) -> N audio = np.concatenate(parts) sd.play(audio, samplerate=SAMPLE_RATE) - sd.wait() + # sd.wait() calls Event.wait() without timeout — hangs forever if the + # audio device stalls. Poll with a 2s ceiling and force-stop. + deadline = time.monotonic() + 2.0 + while sd.get_stream() and sd.get_stream().active and time.monotonic() < deadline: + time.sleep(0.01) + sd.stop() except Exception as e: logger.debug("Beep playback failed: %s", e) @@ -289,7 +294,12 @@ class AudioRecorder: cb = self._on_silence_stop self._on_silence_stop = None # fire only once if cb: - threading.Thread(target=cb, daemon=True).start() + def _safe_cb(): + try: + cb() + except Exception as e: + logger.error("Silence callback failed: %s", e, exc_info=True) + threading.Thread(target=_safe_cb, daemon=True).start() try: self._stream = sd.InputStream(