@@ -147,11 +147,15 @@ class SpeechRecognitionManager(private val context: Context) {
147147 return
148148 }
149149 recordingThread = null
150+ thread.stopRequested = true
150151 val session = synchronized(preloadLock) { sessionId }
151152 thread.interrupt()
153+ thread.forceStopAudio()
152154 Thread {
153155 try {
154- thread.join()
156+ // join 超时兜底:录音线程若卡在 Lua 调用中(最坏 CALL_TIMEOUT_MS=180s),
157+ // 不能让后端释放无限期阻塞(否则 WebSocket 与麦克风一直占着)
158+ thread.join(3000 )
155159 } catch (_: InterruptedException ) {
156160 Thread .currentThread().interrupt()
157161 }
@@ -190,11 +194,13 @@ class SpeechRecognitionManager(private val context: Context) {
190194 return
191195 }
192196 recordingThread = null
197+ thread.stopRequested = true
193198 val session = synchronized(preloadLock) { sessionId }
194199 thread.interrupt()
200+ thread.forceStopAudio()
195201 Thread {
196202 try {
197- thread.join()
203+ thread.join(3000 )
198204 } catch (_: InterruptedException ) {
199205 Thread .currentThread().interrupt()
200206 }
@@ -378,6 +384,21 @@ class SpeechRecognitionManager(private val context: Context) {
378384
379385 private val spectrumAnalyzer = SpectrumAnalyzer ()
380386
387+ /* *
388+ * 停止请求标志:不能只依赖线程中断标志停止循环。
389+ *
390+ * processAudioChunk 会经 LuaScriptRuntime.runGuarded 的 FutureTask.get 执行,
391+ * FutureTask.awaitDone 内部用 Thread.interrupted() 检查中断状态并**清除中断标志**,
392+ * 随后 LuaScriptRuntime.call 吞掉 InterruptedException 正常返回 NIL——
393+ * 于是中断标志被消费后 while (!interrupted()) 永远为真,录音线程无法停止,
394+ * stopRecognition 的 join() 永不返回,后端(WebSocket)与麦克风一直后台占用。
395+ */
396+ @Volatile
397+ var stopRequested = false
398+
399+ @Volatile
400+ private var audioRecord: AudioRecord ? = null
401+
381402 override fun run () {
382403 val audioRecord = preStarted ? : (createAudioRecord() ? : run {
383404 mainHandler.post {
@@ -386,6 +407,7 @@ class SpeechRecognitionManager(private val context: Context) {
386407 }
387408 return
388409 })
410+ this .audioRecord = audioRecord
389411
390412 if (! currentBackend.start()) {
391413 audioRecord.stop()
@@ -413,7 +435,7 @@ class SpeechRecognitionManager(private val context: Context) {
413435 val maxPreSpeechChunks = 4 // 0.4s 语音前缓冲
414436
415437 try {
416- while (! interrupted()) {
438+ while (! interrupted() && ! stopRequested ) {
417439 val nread = audioRecord.read(buffer, 0 , buffer.size)
418440 if (nread > 0 ) {
419441 var peak = 0
@@ -457,14 +479,23 @@ class SpeechRecognitionManager(private val context: Context) {
457479 }
458480 } catch (_: Exception ) {
459481 } finally {
460- audioRecord.stop()
461- audioRecord.release()
482+ // forceStopAudio 可能已从外部 stop/release,这里需容错(重复释放不抛异常中断后续流程)
483+ try { audioRecord.stop() } catch (_: Exception ) { }
484+ try { audioRecord.release() } catch (_: Exception ) { }
462485 }
463486
464487 currentBackend.stop()
465488 Log .d(TAG , " Recognition thread ended" )
466489 }
467490
491+ /* * 从外部强制停止录音:录音线程阻塞在 read() 等不可中断调用时,释放 AudioRecord 使其立即返回错误并退出。 */
492+ fun forceStopAudio () {
493+ audioRecord?.let { record ->
494+ try { record.stop() } catch (_: Exception ) { }
495+ try { record.release() } catch (_: Exception ) { }
496+ }
497+ }
498+
468499 private fun isSpeech (chunk : ByteArray ): Boolean {
469500 var peak = 0
470501 for (i in 0 until chunk.size / 2 ) {
0 commit comments