fix(recognition): play stop sound after audio thread joins to prevent capture on short recordings (#436)

Moves play_stop_sound() to after audio_thread.join() but before the
final buffer processing. This closes the race where the audio thread's
in-flight chunk could capture the stop cue for short (≤ ~192 ms) recordings,
while still feeling immediate compared to the original placement after all
recognition cleanup.
This commit is contained in:
Jatin K Malik
2026-05-30 14:41:08 -07:00
committed by GitHub
parent a267f500dd
commit c39f9ce431
2 changed files with 7 additions and 6 deletions
@@ -1768,16 +1768,16 @@ class SpeechRecognitionManager:
# Stop recording FIRST to prevent capturing the stop sound
self.should_record = False
# Give immediate release feedback. The recorder may still finish one
# in-flight chunk, so the stop-sound guard below keeps the cue out.
play_stop_sound()
# Wait for audio thread to finish recording and enqueue any pending audio
# This is critical to prevent race condition where recognition thread exits
# before the final audio segment is enqueued
if self.audio_thread and self.audio_thread.is_alive():
self.audio_thread.join(timeout=2.0)
# Play stop sound now that the audio thread is done and cannot capture it.
# Kept before buffer processing so the cue still feels immediate.
play_stop_sound()
# Trim only a small tail to avoid the stop sound without clipping the user's final word.
with self._buffer_lock:
stop_sound_guard_chunks = self._get_stop_sound_guard_chunks()
+3 -2
View File
@@ -226,7 +226,7 @@ class TestStartStopRecognition(unittest.TestCase):
mgr.stop_recognition()
self.assertFalse(mgr.should_record)
def test_stop_sound_plays_before_audio_thread_join(self):
def test_stop_sound_plays_after_audio_thread_join(self):
mgr = _make_manager()
mgr.state = RecognitionState.LISTENING
mgr.should_record = True
@@ -241,7 +241,8 @@ class TestStartStopRecognition(unittest.TestCase):
):
mgr.stop_recognition()
self.assertEqual(events[:2], ["sound", "join"])
# sound must play after the audio thread has joined so the cue cannot be captured
self.assertEqual(events[:2], ["join", "sound"])
def test_stop_sound_guard_chunk_calculation(self):
mgr = _make_manager()