From 8e9c4614245bbaddfe882cb8c2d77a8e413911db Mon Sep 17 00:00:00 2001 From: ericek111 Date: Fri, 25 Sep 2026 10:49:09 +0000 Subject: [PATCH] Wait for the capture loop when the microphone stops stop() closed the encoder and cleared the transmit state while the capture loop could still be in its last frame, which could turn the transmit indicator back on, or overlap the loop of a quick restart on the same speech detector and processing chain. It now closes the line to wake the loop and waits for it to finish before tearing down. Co-Authored-By: Claude Opus 5.5 --- .../ts3client/audio/CaptureVoiceInput.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/ts3-client/core/src/main/java/com/ts3client/audio/CaptureVoiceInput.java b/ts3-client/core/src/main/java/com/ts3client/audio/CaptureVoiceInput.java index eb45ec9..df08660 100644 --- a/ts3-client/core/src/main/java/com/ts3client/audio/CaptureVoiceInput.java +++ b/ts3-client/core/src/main/java/com/ts3client/audio/CaptureVoiceInput.java @@ -46,6 +46,8 @@ public final class CaptureVoiceInput implements VoiceInput { Math.max(1, HANGOVER_MS * VoiceFormat.SAMPLE_RATE / 1000 / VoiceFormat.FRAME_SIZE); private static final int PREROLL_FRAMES = Math.max(1, PREROLL_MS * VoiceFormat.SAMPLE_RATE / 1000 / VoiceFormat.FRAME_SIZE); + /** How long stop() waits for the capture loop to finish its frame. */ + private static final long STOP_TIMEOUT_MS = 500; private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); private final AtomicBoolean muted = new AtomicBoolean(false); @@ -281,9 +283,20 @@ public final class CaptureVoiceInput implements VoiceInput { public synchronized void stop() { running.set(false); - if (captureThread != null) { - captureThread.interrupt(); - captureThread = null; + Thread capture = captureThread; + captureThread = null; + if (line != null) { + line.close(); // wakes the capture loop's blocking read + line = null; + } + if (capture != null) { + // Let the loop finish its frame, so it cannot reopen the gate or overlap a restart. + capture.interrupt(); + try { + capture.join(STOP_TIMEOUT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } cleanup(); queue.clear();