Skip to content

Commit 3a62dae

Browse files
authored
Merge pull request #770 from kingzcheung/main
fix(speech): 修复录音线程无法正常停止的问题
2 parents eeee792 + f0774c8 commit 3a62dae

2 files changed

Lines changed: 41 additions & 9 deletions

File tree

app/src/main/java/com/kingzcheung/xime/speech/SpeechRecognitionManager.kt

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {

app/src/main/java/com/kingzcheung/xime/ui/keyboard/VoiceKeyboardLayout.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ fun VoiceKeyboardLayout(
7474
modifier = Modifier
7575
.fillMaxWidth()
7676
.weight(1f)
77-
.padding(top = 16.dp),
77+
.padding(top = 8.dp),
7878
horizontalAlignment = Alignment.CenterHorizontally,
79-
verticalArrangement = Arrangement.spacedBy(12.dp)
79+
// 垂直居中:矮屏/大字缩放导致空间不足时内容对称分布,避免贴顶溢出被下方按钮行盖住("文字只显示上半部分")
80+
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically)
8081
) {
8182
if (pluginName.isNotEmpty()) {
8283
Text(
@@ -124,7 +125,7 @@ fun VoiceKeyboardLayout(
124125
Row(
125126
modifier = Modifier
126127
.fillMaxWidth()
127-
.padding(horizontal = 16.dp, vertical = 20.dp),
128+
.padding(horizontal = 16.dp, vertical = 12.dp),
128129
horizontalArrangement = Arrangement.spacedBy(12.dp),
129130
verticalAlignment = Alignment.CenterVertically
130131
) {
@@ -203,7 +204,7 @@ fun VoiceKeyboardLayout(
203204
Box(
204205
modifier = Modifier
205206
.fillMaxWidth()
206-
.height(80.dp),
207+
.height(64.dp),
207208
contentAlignment = Alignment.Center
208209
) {
209210
Canvas(

0 commit comments

Comments
 (0)