From c39f9ce431237f69ffd0fefe4286ce3fca39a20b Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Sat, 30 May 2026 14:41:08 -0700 Subject: [PATCH] fix(recognition): play stop sound after audio thread joins to prevent capture on short recordings (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/vocalinux/speech_recognition/recognition_manager.py | 8 ++++---- tests/test_recognition_manager_advanced.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index ece93a9..3e7294d 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -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() diff --git a/tests/test_recognition_manager_advanced.py b/tests/test_recognition_manager_advanced.py index 94752f4..b921012 100644 --- a/tests/test_recognition_manager_advanced.py +++ b/tests/test_recognition_manager_advanced.py @@ -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()