|
|
|
@ -2,9 +2,11 @@ package com.yunqiinnovation.agent_service |
|
|
|
|
|
|
|
import android.content.Context |
|
|
|
import android.media.AudioFormat |
|
|
|
import android.media.AudioManager |
|
|
|
import android.media.AudioRecord |
|
|
|
import android.media.MediaPlayer |
|
|
|
import android.media.MediaRecorder |
|
|
|
import android.media.ToneGenerator |
|
|
|
import android.os.Handler |
|
|
|
import android.os.Looper |
|
|
|
import android.util.Log |
|
|
|
@ -91,6 +93,9 @@ class StsAgent( |
|
|
|
// ── 预缓冲(VAD 触发前 ~1.5s)──────────────────────────────────────────── |
|
|
|
private val preBuffer = ArrayDeque<ByteArray>() |
|
|
|
private val preBufferLock = Any() |
|
|
|
|
|
|
|
// ── 麦克风读写锁(防止 read() 与 stop()/release() 并发导致 native SIGABRT)── |
|
|
|
private val micLock = Any() |
|
|
|
private val PRE_BUFFER_MAX_CHUNKS = 75 |
|
|
|
|
|
|
|
// ── 本地 VAD ────────────────────────────────────────────────────────────── |
|
|
|
@ -406,8 +411,15 @@ class StsAgent( |
|
|
|
recordingJob = launch { |
|
|
|
val buf = ByteArray(RECORD_CHUNK) |
|
|
|
while (isActive && (inCall || pttRecording)) { |
|
|
|
val read = audioRecord?.read(buf, 0, buf.size) ?: break |
|
|
|
// 使用非阻塞读 + micLock,确保与 stopMicHard() 中的 stop()/release() 互斥。 |
|
|
|
// READ_NON_BLOCKING 让 read() 立即返回(微秒级),锁持有时间极短, |
|
|
|
// 彻底消除 releaseBuffer 并发导致的 native SIGABRT。 |
|
|
|
val read = synchronized(micLock) { |
|
|
|
audioRecord?.read(buf, 0, buf.size, AudioRecord.READ_NON_BLOCKING) ?: -1 |
|
|
|
} |
|
|
|
if (read < 0) break |
|
|
|
if (read > 0) { micChunkCount++; onMicAudio(buf.copyOf(read)) } |
|
|
|
else delay(5) // 无数据时释放协程让出线程,避免忙等 |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
@ -417,8 +429,13 @@ class StsAgent( |
|
|
|
|
|
|
|
private fun stopMicHard() { |
|
|
|
recordingJob?.cancel(); recordingJob = null |
|
|
|
try { audioRecord?.stop(); audioRecord?.release() } catch (_: Exception) {} |
|
|
|
audioRecord = null |
|
|
|
// 拿 micLock 后再 stop()/release(),与录音循环的 read() 互斥, |
|
|
|
// 保证 stop() 永远不会在 read() 内部 releaseBuffer 期间被调用。 |
|
|
|
synchronized(micLock) { |
|
|
|
val ar = audioRecord |
|
|
|
audioRecord = null |
|
|
|
try { ar?.stop(); ar?.release() } catch (_: Exception) {} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun startMicWatchdog() { |
|
|
|
@ -504,12 +521,24 @@ class StsAgent( |
|
|
|
} |
|
|
|
|
|
|
|
forwardingAudio = true; wsConnecting = false |
|
|
|
playReadyBeep() |
|
|
|
emit("recognition_started", emptyMap()) |
|
|
|
|
|
|
|
val cached = synchronized(preBufferLock) { preBuffer.toList().also { preBuffer.clear() } } |
|
|
|
for (c in cached) feedSendBuffer(c) |
|
|
|
} |
|
|
|
|
|
|
|
/** 唤醒成功提示音:通知用户可以说话了 */ |
|
|
|
private fun playReadyBeep() { |
|
|
|
try { |
|
|
|
val toneGen = ToneGenerator(AudioManager.STREAM_MUSIC, 60) |
|
|
|
toneGen.startTone(ToneGenerator.TONE_PROP_BEEP, 180) |
|
|
|
mainHandler.postDelayed({ toneGen.release() }, 300) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.w(TAG, "playReadyBeep failed: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════ |
|
|
|
// WebSocket |
|
|
|
// ═════════════════════════════════════════════════════════════════════════ |
|
|
|
|