From f7e1f96256cf6e09b1398fd0db20f0b6bc12a5b1 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Thu, 4 Sep 2025 21:58:23 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=81=9C=E6=AD=A2opus?= =?UTF-8?q?=E5=8F=91=E7=94=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/agent_service/AgentServiceImpl.swift | 8 +++++--- .../ios/ble_service/Sources/ble_service/BleService.swift | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index c1b27f252..e8e13210c 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -354,7 +354,6 @@ class AgentServiceImpl: NSObject { if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming { os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode) - BleService.shared.closeCodec() self.stopRecognition() self.sendEvent(name: "auto_stop", data: [ "reason": "idle_timeout", @@ -394,7 +393,9 @@ class AgentServiceImpl: NSObject { stopRecognition() } - + // 接收到唤醒信号,打开编码器 (设备侧) + let openResult = BleService.shared.openEncoder() + os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败") wasMusicPlayingBeforeRecognition = MusicService.shared.isPlaying // 如果音乐正在播放,则暂停 @@ -472,7 +473,8 @@ class AgentServiceImpl: NSObject { MusicService.shared.resume() } - + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() // 重置识别模式 currentRecognitionMode = "normal" diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 3ad24d30f..caf6d9b72 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -1108,9 +1108,7 @@ private var cmdReplyType: UInt8 = 0 notifyCharacteristic != nil ? "✅" : "❌", audioCharacteristic != nil ? "✅" : "❌") - // 接收到唤醒信号,打开编码器 (设备侧) - let openResult = openEncoder() - os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败") + notifyWakeupSignalReceived() default: From 3aef355477f157dcc1b29e999d6bfca7a7762f2a Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sat, 6 Sep 2025 15:18:02 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=94=A4=E9=86=92?= =?UTF-8?q?=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/settings/views/settings_view.dart | 42 +++++++------- .../agent_service/AgentService.kt | 22 ++++++-- .../agent_service/AgentServicePlugin.kt | 13 +++-- .../yunqiinnovation/agent_service/BleAgent.kt | 1 + .../azure_speech/AzureAsrHelper.kt | 4 +- .../azure_speech/tools/SimpleAudioReceiver.kt | 46 +++++++++++---- .../yunqiinnovation/ble_service/BleService.kt | 56 +++++++++---------- 7 files changed, 108 insertions(+), 76 deletions(-) diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 4220460bd..c626f2e54 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -515,27 +515,27 @@ class SettingsView extends GetView { // ? Colors.white.withOpacity(0.1) // : Colors.grey[200]), //BLE测试 - // _buildSimpleNavigationSetting( - // title: 'opus解码测试', - // subtitle: '测试opus解码', - // icon: Icons.bluetooth_searching, - // iconBgColor: isDarkMode - // ? Colors.green[900]!.withOpacity(0.3) - // : Colors.green[100]!, - // iconColor: - // isDarkMode ? Colors.green[300]! : Colors.green[600]!, - // titleColor: isDarkMode ? Colors.white : null, - // subtitleColor: isDarkMode ? Colors.white70 : null, - // onTap: () { - // Get.toNamed(Routes.opusTest); - // }, - // isDarkMode: isDarkMode, - // ), - // Divider( - // height: 1, - // color: isDarkMode - // ? Colors.white.withOpacity(0.1) - // : Colors.grey[200]), + _buildSimpleNavigationSetting( + title: 'opus解码测试', + subtitle: '测试opus解码', + icon: Icons.bluetooth_searching, + iconBgColor: isDarkMode + ? Colors.green[900]!.withOpacity(0.3) + : Colors.green[100]!, + iconColor: + isDarkMode ? Colors.green[300]! : Colors.green[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + Get.toNamed(Routes.opusTest); + }, + isDarkMode: isDarkMode, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), // BLE测试 // _buildSimpleNavigationSetting( diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 00fd670f4..5ccfc94e9 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -130,6 +130,8 @@ object AgentService : CoroutineScope { //是否播放音乐中 private var isplaymusic = false; + //是否保留最终结果 + private var isKeepResult = true; // 打断防抖相关 private var lastInterruptTime = 0L @@ -265,6 +267,7 @@ object AgentService : CoroutineScope { */ fun dispose() { try { + BleService.closeCodec() stopRecognition() stopTts() stopAiStream() @@ -474,6 +477,7 @@ object AgentService : CoroutineScope { if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) { Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode") + BleService.closeCodec() stopRecognition() sendEvent( "auto_stop", @@ -516,13 +520,18 @@ object AgentService : CoroutineScope { * @param mode 识别模式:normal, ble_wakeup, phone_call, push_to_talk */ fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean { + if(isExternalActive){ + isKeepResult = false + BleService.openEncoder()//不发指令。只是录制opus + } if (!isInitialized) { Log.d(TAG, "服务未初始化") return false } Log.d("TAG", "开始语音识别startRecognition?") stopRecognition() - BleService.openEncoder() + + // if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){ // Log.d(TAG, "开始语音识别stopRecognition") // // BleService.closeCodec() @@ -559,6 +568,9 @@ object AgentService : CoroutineScope { AzureAsrHelper.AudioSourceType.MICROPHONE } Log.d("TAG", "azureAsrHelper?.startContinuousRecognition") + if(isExternalActive){ + isKeepResult = true + } azureAsrHelper?.startContinuousRecognition(audioSourceType) // 根据模式启动相应的空闲检测 @@ -630,7 +642,7 @@ object AgentService : CoroutineScope { } override fun onResult(result: String, detectedLanguage: String) { - if (result.isNotEmpty()) { + if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读 sendEvent( "recognition_result", mapOf( "text" to result, @@ -709,11 +721,9 @@ object AgentService : CoroutineScope { Log.d(TAG, "停止语音识别,当前模式: $currentRecognitionMode") try { + _isRecognitionActive.set(false) azureAsrHelper?.stopContinuousRecognition() - BleService.closeCodec() - _isRecognitionActive.set(false) stopIdleCheck() - // 重置识别模式 currentRecognitionMode = "normal" return @@ -721,7 +731,6 @@ object AgentService : CoroutineScope { Log.e(TAG, "停止语音识别异常: ${e.message}") _isRecognitionActive.set(false) stopIdleCheck() - // 重置识别模式 currentRecognitionMode = "normal" return @@ -1055,6 +1064,7 @@ object AgentService : CoroutineScope { ) ) if (name == "exit_interaction") { + BleService.closeCodec() stopRecognition() } } catch (e: Exception) { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 54122ce11..776a926e7 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import android.content.Intent import android.os.Build +// 添加BleService的导入语句 +import com.yunqiinnovation.ble_service.BleService class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, AgentServiceListener { @@ -124,13 +126,14 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("START_ERROR", "启动对话失败: ${e.message}", null) } } - "recognizeCallback" -> { - val isStarted = AgentService.recognizeCallback() - result.success(isStarted) - } + "recognizeCallback" -> { + val isStarted = AgentService.recognizeCallback() + result.success(isStarted) + } "stopConversation" -> { try { + BleService.closeCodec() AgentService.stopRecognition() result.success(true) } catch (e: Exception) { @@ -309,4 +312,4 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream eventChannel.setStreamHandler(null) AgentService.removeListener(this) } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index 97d4df5f2..80c9b568b 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -77,6 +77,7 @@ Log.d(TAG, "手动启动语音识别: ") Log.e(TAG, "语音代理未初始化") return } + BleService.closeCodec() AgentService.stopRecognition() return } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index fbc8bcf4d..08bf94688 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -215,7 +215,7 @@ class AzureAsrHelper(private val context: Context) { if (!audioStream.isInitialized()) { audioStream.initAudioRecord() } - + audioStream.setupStream() audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream) } catch (e: Exception) { @@ -488,7 +488,7 @@ class AzureAsrHelper(private val context: Context) { if (!audioStream.isInitialized()) { audioStream.initAudioRecord() } - + audioStream.setupStream() if (audioStream.recordfile == null) { audioStream.recordfile = RecordFile() } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt index 51a40aef4..08e5ae668 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt @@ -88,8 +88,6 @@ class SimpleAudioReceiver(private val context: Context) { * 初始化音频录制 */ fun initAudioRecord() { - val format = getOptimalAudioFormat() - pushAudioStream = AudioInputStream.createPushStream(format) isRunning.set(true) Log.d(TAG, "initAudioRecord: ${isRunning}") startWriteThread() // 再启动数据读取线程 @@ -97,6 +95,17 @@ class SimpleAudioReceiver(private val context: Context) { audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL isInitialized = true + } + /** + * 设置音频流 + */ + fun setupStream() { + // 安全关闭并清理之前的流 + pushAudioStream?.close() + pushAudioStream = null + + val format = getOptimalAudioFormat() + pushAudioStream = AudioInputStream.createPushStream(format) } fun isInitialized(): Boolean = isInitialized /** @@ -143,7 +152,7 @@ class SimpleAudioReceiver(private val context: Context) { if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data try { - Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}") + //Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}") if(audioDataCallback!=null){ audioDataCallback?.onAudio(finalData) } @@ -183,22 +192,36 @@ class SimpleAudioReceiver(private val context: Context) { writeQueue.offer(buffer.copyOf()) } - /** - * 开始音频输入 - * @param audioSourceType 音频源类型 - * @param callback 音频数据回调,可为空 - */ - fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) { + /** + * 开始音频输入 + * @param audioSourceType 音频源类型 + * @param callback 音频数据回调,可为空 + */ +fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) { + try { this.audioSourceType = audioSourceType - this.audioDataCallback = callback - writeQueue.clear() + this.audioDataCallback = callback + isWriting.set(true) + + if(writeQueue.isNotEmpty()){ + Log.d("tag", "存下来的数据: ${writeQueue?.size}") + } when (audioSourceType) { AudioSourceType.MICROPHONE -> runMicrophoneCapture() AudioSourceType.EXTERNAL -> runExternalCapture() } + } catch (e: Exception) { + Log.e(TAG, "启动音频录制失败: ${e.message}", e) + // 清理资源 + pushAudioStream?.close() + pushAudioStream = null + isWriting.set(false) + throw e // 重新抛出异常让调用者处理 } +} +// ... existing code ... /** * 配置通话音频模式以优化回声消除 */ @@ -367,6 +390,7 @@ class SimpleAudioReceiver(private val context: Context) { */ fun stopMicrophoneCapture() { try { + writeQueue.clear() isWriting.set(false) // 停止录音 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index baaed09ec..97e43996c 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -178,7 +178,7 @@ object BleService { */ fun initialize(appContext: Context): Boolean { if (isInitialized) return true - + try { this.context = appContext.applicationContext // 初始化蓝牙管理器和适配器 @@ -208,7 +208,7 @@ object BleService { recordfile1 = RecordingFile(this.context) recordfile1!!.fileName = "重新压缩" isInitialized = true - Log.d(TAG, "BLE服务初始化成功") + startOpusStreamDecoding(false, 1, 16000, 40) return true } catch (e: Exception) { Log.e(TAG, "BLE服务初始化失败: ${e.message}", e) @@ -838,22 +838,7 @@ startBytesStatistics() recordfile?.saveAudioDataToWav(data) // 将完整的帧加入队列,由专门的线程处理 audioDataQueue.offer(data) - // // 将接收到的数据添加到缓存中 - // synchronized(audioDataBuffer) { - // audioDataBuffer.addAll(data.toList()) - - // // 当缓存达到阈值时,提取完整的帧进行解码 - // while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) { - // // 提取一个完整的帧(1280字节) - // val frameData = ByteArray(AUDIO_BUFFER_SIZE) - // for (i in 0 until AUDIO_BUFFER_SIZE) { - // frameData[i] = audioDataBuffer.removeAt(0) - // } - - - // Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码") - // } - // } + } else { // Log.d(TAG, "Opus解码流未启动,忽略音频数据") } @@ -1243,7 +1228,7 @@ startBytesStatistics() // Opus 解码相关 // ====================================================================================================== - private fun startOpusStreamDecoding( + public fun startOpusStreamDecoding( hasHeader: Boolean = false, // 通常BLE传输的Opus没有文件头 channel: Int = 1, sampleRate: Int = 16000, // 确认设备端Opus编码采样率 @@ -1634,7 +1619,12 @@ startBytesStatistics() ) // 或其他默认声道 ) } - + fun closeCodec1(): Boolean { + recordfile1!!.closeFile() + + recordfile!!.closeFile() + return true + } /** * 控制编解码 - 打开解码 */ @@ -1707,16 +1697,20 @@ startBytesStatistics() */ fun openEncoder(): Boolean { Log.i(TAG, "打开编码0xB1") - - startOpusStreamDecoding(false, 1, 16000, 40) - // 目前仅发送命令通知设备开始编码。 - return sendCommand( - BleConst.CMD_CONTROL_CODEC.toByte(), - byteArrayOf( - BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), - BleConst.AUDIO_CHANNEL_RIGHT.toByte() // 或设备期望的声道 - ) - ) + recordfile1!!.closeFile() + recordfile1!!.creatingFiles() + recordfile!!.closeFile() + recordfile!!.creatingFiles() + // startOpusStreamDecoding(false, 1, 16000, 40) + // // 目前仅发送命令通知设备开始编码。 + // return sendCommand( + // BleConst.CMD_CONTROL_CODEC.toByte(), + // byteArrayOf( + // BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), + // BleConst.AUDIO_CHANNEL_RIGHT.toByte() // 或设备期望的声道 + // ) + // ) + return true } @@ -2102,7 +2096,7 @@ class RecordingFile(private val context: Context) { } /** - * 保存音频数据到 WAV 文件(异步) + * 保存音频数据到 opus 文件(异步) */ internal fun saveAudioDataToWav(buffer: ByteArray) { if (fos == null || currentAudioFile == null) return From e9e0f37d673ff510914464db475a41dbd662a8ee Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 8 Sep 2025 16:34:09 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E4=BC=98=E5=8C=96ios=E5=94=A4=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentService.kt | 2 +- .../agent_service/AgentServiceImpl.swift | 53 +++++++++++------- .../agent_service/AgentServicePlugin.swift | 4 +- .../Sources/tools/SimpleAudioReceiver.swift | 54 ++++++++++++------- .../yunqiinnovation/ble_service/BleService.kt | 8 +-- .../Sources/ble_service/BleService.swift | 20 +++---- 6 files changed, 86 insertions(+), 55 deletions(-) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 5ccfc94e9..5258483c9 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -598,7 +598,7 @@ object AgentService : CoroutineScope { val success = azureAsrHelper?.setupEventListeners(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onRecognizing(recognizing: String, detectedLanguage: String) { - if (recognizing.isNotEmpty()) { + if (recognizing.isNotEmpty()&&isKeepResult==true) { // 检测到语音,更新状态 _hasSpeechDetected.set(true) diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index e8e13210c..b294b6a05 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -69,7 +69,9 @@ class AgentServiceImpl: NSObject { // 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题 private var isStartingRecognition: Bool = false private var stopRequestedDuringStart: Bool = false - + //是否保留最终结果 + private var isKeepResult = true; + var currsessionId: String = "" var language: String = "" private var serverurl = "" @@ -354,6 +356,8 @@ class AgentServiceImpl: NSObject { if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming { os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode) + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() self.stopRecognition() self.sendEvent(name: "auto_stop", data: [ "reason": "idle_timeout", @@ -377,6 +381,14 @@ class AgentServiceImpl: NSObject { } } func startRecognition(useBle: Bool = false, mode: String = "normal") -> Bool { + // 接收到唤醒信号,打开编码器 (设备侧) + print("ai启动语音\(useBle))") + var openResult = false // 添加openResult变量定义 + if useBle{ + isKeepResult = false + // 捕获openEncoder的返回值 + openResult = BleService.shared.openEncoder() // 正确捕获返回值 + } if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false @@ -387,14 +399,11 @@ class AgentServiceImpl: NSObject { sendError("无麦克风权限", code: "PERMISSION_DENIED") return false } - } - stopTts(); - if isRecognizing { - stopRecognition() - } - - // 接收到唤醒信号,打开编码器 (设备侧) - let openResult = BleService.shared.openEncoder() + } + stopRecognition() + stopTts() + + os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败") wasMusicPlayingBeforeRecognition = MusicService.shared.isPlaying @@ -417,7 +426,7 @@ class AgentServiceImpl: NSObject { // 新增:标记为"启动中",用于允许 stop 在启动未完成时也能生效 isStartingRecognition = true - + print("ai启动语音=\(audioSourceType)=\(useBle)") guard let success = azureAsrHelper?.startContinuousRecognition( // callback: self, @@ -428,7 +437,9 @@ class AgentServiceImpl: NSObject { sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") return false } - + if(useBle){ + isKeepResult = true + } // 根据模式启动相应的空闲检测(此处 guard isRecognizing,会在 onSessionStarted 中再次启动) startIdleCheckForMode(mode: mode) @@ -450,6 +461,7 @@ class AgentServiceImpl: NSObject { /// 特性: /// 1) 即便处于"启动中"(isStartingRecognition)也会打印日志并尝试停止 /// 2) 如果底层尚未完全进入可停止状态,记录一次待停止请求,待会话启动后立即停止 + print("停止识别语音,isRecognizing=\(isRecognizing),isStartingRecognition=\(isStartingRecognition)") if !(isRecognizing || isStartingRecognition) { // 既不在识别中,也不在启动中,视为幂等处理 return true @@ -473,8 +485,6 @@ class AgentServiceImpl: NSObject { MusicService.shared.resume() } - // 关闭编码器 (设备侧) - BleService.shared.closeCodec() // 重置识别模式 currentRecognitionMode = "normal" @@ -492,7 +502,7 @@ class AgentServiceImpl: NSObject { return false } guard let audioStream = azureAsrHelper?.audioStream else { - os_log("音频流未初始化", log: logger,type: .error) + os_log("AI音频流未初始化", log: logger,type: .error) return false } audioStream.saveAudioDataTo(data: audioData) @@ -1055,6 +1065,8 @@ private func jsonToString(_ json: [String: Any]) -> String? { // 如果正在识别,需要先停止 let wasRecognizing = isRecognizing if wasRecognizing { + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() _ = stopRecognition() } @@ -1229,6 +1241,8 @@ private func jsonToString(_ json: [String: Any]) -> String? { func dispose() -> Bool { print("ai释放资源") if isRecognizing { + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() stopRecognition() } @@ -1390,6 +1404,8 @@ class ChatApiStreamCallback: StreamCallback { print("onFunctionCall=functionCall\(functionCall)") if (name == "exit_interaction") { + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() agentService.stopRecognition() } @@ -1515,9 +1531,6 @@ extension AgentServiceImpl: BleService.Callback { func onWakeupSignalReceived() { - stopTts() - - stopRecognition() startRecognition(useBle: true, mode: "ble_wakeup") } @@ -1717,7 +1730,7 @@ class AudioPlayer { extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { func onResult(_ text: String, _ detectedLanguage: String) { - if !text.isEmpty { + if !text.isEmpty && isKeepResult { var data: [String: Any] = ["text": text] if !detectedLanguage.isEmpty { data["language"] = detectedLanguage @@ -1736,7 +1749,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } func onRecognizing(_ recognizing: String, _ detectedLanguage: String) { - if !recognizing.isEmpty { + if !recognizing.isEmpty && isKeepResult { let previousHasSpeech = hasSpeechDetected hasSpeechDetected = true @@ -1772,6 +1785,8 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { // 新增:若在启动期间已经收到停止请求,则立即执行停止 if stopRequestedDuringStart { stopRequestedDuringStart = false + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() _ = stopRecognition() } } diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift index 45457b848..e785c5192 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift @@ -2,7 +2,7 @@ import Flutter import UIKit import Foundation import os.log - +import ble_service /// 代理服务Flutter插件,负责将Flutter调用桥接到原生实现 public class AgentServicePlugin: NSObject, FlutterPlugin { @@ -90,6 +90,8 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { return } print("stopConversation") + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() impl.stopRecognition() result(true) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift index 02db119fd..5aef34be7 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift @@ -144,11 +144,11 @@ public class SimpleAudioReceiver: NSObject { while self.isRunning { if !self._isWriting { - // print("startWriteThread 111") + usleep(10_000) continue } - // print("startWriteThread 222") + guard let dataToWrite = self.writeQueue.take() else { continue } do { @@ -157,14 +157,12 @@ public class SimpleAudioReceiver: NSObject { self.audioDataCallback!.onAudio(dataToWrite) } if self.pushAudioStream != nil && isContinuousRecognitionActive { - try self.pushAudioStream?.write(dataToWrite) } if self.recordfile != nil && isRecord{ - // print("写入recordfile数据长度: \(dataToWrite.count)") recordfile?.saveAudioDataToWav(dataToWrite) } - // print("写入数据长度: \(dataToWrite.count)") + } catch { print("推送音频数据失败: \(error.localizedDescription)") } @@ -183,9 +181,8 @@ public class SimpleAudioReceiver: NSObject { if audioSourceType != .external { return } - // print("外部data=\(data)") // 放入队列,由写线程写入 - writeQueue.put(data) + self.writeQueue.put(data) } /** @@ -220,18 +217,22 @@ public class SimpleAudioReceiver: NSObject { } } + /** + * 运行外部音频捕获 + * 修复OSStatus error -50问题 + */ private func runExternalCapture() { if audioSourceType == .external { - do { - try audioSession.setCategory(.playback, - mode: .videoChat, - options: [ .mixWithOthers,.allowBluetoothA2DP // 允许蓝牙耳机,不占用hfp链路 - ]) // 添加音频优先级控制 - try audioSession.overrideOutputAudioPort(.none) - try audioSession.setActive(true) - } catch { - print("设置音频会话失败: \(error.localizedDescription)") - } + // do { + // try audioSession.setCategory(.playback, + // mode: .videoChat, + // options: [ .mixWithOthers,.allowBluetoothA2DP // 允许蓝牙耳机,不占用hfp链路 + // ]) // 添加音频优先级控制 + // try audioSession.overrideOutputAudioPort(.none) + // try audioSession.setActive(true) + // } catch { + // print("设置音频会话失败: \(error.localizedDescription)") + // } //pushAudioData(data: Data()) micCapture.stopCapture() @@ -336,7 +337,7 @@ public func restoreOriginalAudioState() { public func stopMicrophoneCapture() { - + self.writeQueue.clear() _isWriting=false micCapture.stopCapture() print("stopMicrophoneCapture") @@ -517,6 +518,23 @@ private class LinkedBlockingQueue { // 通知有新元素可用 availableItems.signal() + } + + /** + * 清空队列 + */ + func clear() { + queueLock.lock() + defer { queueLock.unlock() } + + // 检查队列是否已关闭 + if isClosed { + return + } + + // 添加元素(无容量限制,与Android一致) + queue.removeAll() + } /** diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 97e43996c..8abffc462 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -202,13 +202,7 @@ object BleService { Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) // 根据需要决定是否因为Opus初始化失败而返回false } - recordfile = RecordingFile(this.context) - recordfile!!.fileName = "不拆分" - - recordfile1 = RecordingFile(this.context) - recordfile1!!.fileName = "重新压缩" isInitialized = true - startOpusStreamDecoding(false, 1, 16000, 40) return true } catch (e: Exception) { Log.e(TAG, "BLE服务初始化失败: ${e.message}", e) @@ -1701,7 +1695,7 @@ startBytesStatistics() recordfile1!!.creatingFiles() recordfile!!.closeFile() recordfile!!.creatingFiles() - // startOpusStreamDecoding(false, 1, 16000, 40) + startOpusStreamDecoding(false, 1, 16000, 40) // // 目前仅发送命令通知设备开始编码。 // return sendCommand( // BleConst.CMD_CONTROL_CODEC.toByte(), diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index caf6d9b72..2b1245106 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -586,22 +586,24 @@ private var cmdReplyType: UInt8 = 0 } /// 打开编码器 - func openEncoder() -> Bool { + /// - Returns: 操作是否成功 + public func openEncoder() -> Bool { os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info) // 确保Opus解码器处于活跃状态(如果不是则启动) // if opusProcessor?.isDecoding() != true { startOpusDecoding() //} - let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) - let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) + // let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) + // let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) - if result { - os_log("✅ 编码器命令发送成功", log: logger, type: .info) - } else { - os_log("❌ 编码器命令发送失败", log: logger, type: .error) - } + // if result { + // os_log("✅ 编码器命令发送成功", log: logger, type: .info) + // } else { + // os_log("❌ 编码器命令发送失败", log: logger, type: .error) + // } - return result + //return result + return true } /// 打开解码器 From 2cddbb8d6cd9c4eb1b3aade9baed1ec56e4b727a Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 8 Sep 2025 17:45:43 +0800 Subject: [PATCH 4/7] add --- .../kotlin/com/yunqiinnovation/ble_service/BleService.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index dcdd5502e..e25c554fb 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -190,6 +190,11 @@ object BleService { // 初始化Handler scanHandler = Handler(Looper.getMainLooper()) + recordfile = RecordingFile(this.context) + recordfile!!.fileName = "不拆分" + + recordfile1 = RecordingFile(this.context) + recordfile1!!.fileName = "重新压缩" // 启动队列处理 startAudioQueueProcessing() From ba947f1cf78eb3a8cc7a8bb4f67cea1455e54209 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 8 Sep 2025 19:12:33 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E4=B8=8D=E6=92=AD=E6=8A=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IntegratedSpeechTranslationService.swift | 141 +++++++++++------- 1 file changed, 83 insertions(+), 58 deletions(-) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift index a0023e047..4659e56c3 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift @@ -777,68 +777,93 @@ import os.log let voice: String switch language { - // 亚洲地区 - case "zh-CN": - voice = "zh-CN-XiaoxiaoNeural" - case "zh-TW": - voice = "zh-TW-HsiaoNeural" - case "zh-HK": - voice = "zh-HK-HiuGaaiNeural" - case "ja-JP": - voice = "ja-JP-NanamiNeural" - case "ko-KR": - voice = "ko-KR-SunHiNeural" - case "hi-IN": - voice = "hi-IN-KalpanaNeural" - case "ta-IN": - voice = "ta-IN-SundarNeural" - case "th-TH": - voice = "th-TH-PattaraNeural" - case "vi-VN": - voice = "vi-VN-AnhNeural" + // 中文相关 + case "zh-CN": + voice = "zh-CN-XiaoxiaoNeural" + case "zh-TW": + voice = "zh-TW-HsiaoChenNeural" + case "zh-HK": + voice = "zh-HK-HiuMaanNeural" - // 欧洲地区 - case "en-US": - voice = "en-US-AriaNeural" - case "en-GB": - voice = "en-GB-SusanNeural" - case "fr-FR": - voice = "fr-FR-DeniseNeural" - case "de-DE": - voice = "de-DE-KatjaNeural" - case "es-ES": - voice = "es-ES-ElviraNeural" - case "it-IT": - voice = "it-IT-DonatellaNeural" - case "pl-PL": - voice = "pl-PL-JozefinaNeural" - case "ru-RU": - voice = "ru-RU-DariaNeural" + // 英语相关 + case "en-US": + voice = "en-US-AriaNeural" - // 中东/非洲 - case "ar-SA": - voice = "ar-SA-MajdNeural" - case "he-IL": - voice = "he-IL-NoaNeural" - case "sw-KE": - voice = "sw-KE-SabinaNeural" + // 亚洲语言 + case "ja-JP": + voice = "ja-JP-NanamiNeural" + case "ko-KR": + voice = "ko-KR-SunHiNeural" + case "th-TH": + voice = "th-TH-PremwadeeNeural" + case "vi-VN": + voice = "vi-VN-HoaiMyNeural" + case "id-ID": + voice = "id-ID-GadisNeural" + case "hi-IN": + voice = "hi-IN-SwaraNeural" + case "ms-MY": + voice = "ms-MY-YasminNeural" + case "bn-IN": + voice = "bn-IN-TanishaaNeural" + case "ta-IN": + voice = "ta-IN-PallaviNeural" + case "te-IN": + voice = "te-IN-ShrutiNeural" + case "mr-IN": + voice = "mr-IN-AarohiNeural" + case "ur-IN": + voice = "ur-IN-GulNeural" + case "tr-TR": + voice = "tr-TR-EmelNeural" + case "fa-IR": + voice = "fa-IR-DilaraNeural" - // 美洲/大洋洲 - case "pt-BR": - voice = "pt-BR-ManuelaNeural" - case "es-MX": - voice = "es-MX-CarlaNeural" - - // 其他重要语言 - case "tr-TR": - voice = "tr-TR-SerifeNeural" - case "uk-UA": - voice = "uk-UA-OksanaNeural" - case "fa-IR": - voice = "fa-IR-SaraNeural" - case "nl-NL": - voice = "nl-NL-LotteNeural" + // 欧洲语言 + case "fr-FR": + voice = "fr-FR-DeniseNeural" + case "es-ES": + voice = "es-ES-ElviraNeural" + case "pt-BR": + voice = "pt-BR-FranciscaNeural" + case "it-IT": + voice = "it-IT-ElsaNeural" + case "de-DE": + voice = "de-DE-KatjaNeural" + case "ru-RU": + voice = "ru-RU-SvetlanaNeural" + case "pl-PL": + voice = "pl-PL-AgnieszkaNeural" + case "nl-NL": + voice = "nl-NL-ColetteNeural" + case "sv-SE": + voice = "sv-SE-SofieNeural" + case "cs-CZ": + voice = "cs-CZ-VlastaNeural" + case "el-GR": + voice = "el-GR-AthinaNeural" + case "ro-RO": + voice = "ro-RO-AlinaNeural" + case "hu-HU": + voice = "hu-HU-NoemiNeural" + case "uk-UA": + voice = "uk-UA-PolinaNeural" + case "da-DK": + voice = "da-DK-ChristelNeural" + case "fi-FI": + voice = "fi-FI-NooraNeural" + case "nb-NO": + voice = "nb-NO-IselinNeural" + case "hr-HR": + voice = "hr-HR-GabrijelaNeural" + case "ca-ES": + voice = "ca-ES-JoanaNeural" + // 阿拉伯语和非洲语言 + case "ar-EG": + voice = "ar-EG-SalmaNeural" + case "sw-KE": + voice = "sw-KE-ZuriNeural" default: voice = "en-US-AriaNeural" // 默认回退英语 } From ccee0da9a6a113b623cef044f82c425d58e165d8 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 8 Sep 2025 20:31:16 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dios=E9=80=9A=E8=AF=9D?= =?UTF-8?q?=E5=A5=94=E6=BA=83=E9=97=AE=E9=A2=98=EF=BC=8C=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E4=B8=8D=E6=92=AD=E6=8A=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/language_manager.dart | 152 +++++++++--------- .../azure_speech/AzureAsrToAsr.kt | 11 +- .../IntegratedSpeechTranslationService.swift | 33 +++- .../VolcanoTranslationServiceImpl.swift | 56 ++++++- 4 files changed, 168 insertions(+), 84 deletions(-) diff --git a/lib/data/services/language_manager.dart b/lib/data/services/language_manager.dart index c89b2b647..d20f3776e 100644 --- a/lib/data/services/language_manager.dart +++ b/lib/data/services/language_manager.dart @@ -5,22 +5,22 @@ import '../../core/utils/logger.dart'; class LanguageInfo { /// 短代码,如 'zh', 'en' final String shortCode; - + /// 中文名称 final String chineseName; - + /// 英文名称 final String englishName; - + /// Azure ASR 语言代码,如 'zh-CN', 'en-US' final String asrCode; - + /// Azure TTS 语音名称 final String ttsVoiceName; - + /// 备注信息 final String? note; - + LanguageInfo({ required this.shortCode, required this.chineseName, @@ -35,30 +35,30 @@ class LanguageInfo { class LanguageManager extends GetxService { // 单例实例 static LanguageManager? _instance; - + // 单例工厂方法 factory LanguageManager() { _instance ??= LanguageManager._internal(); return _instance!; } - + // 私有构造函数 LanguageManager._internal() { _initLanguageMap(); } - + // 语言信息映射表,以 ASR 代码为键 final Map _languageMap = {}; - + // 中文名称到 ASR 代码的映射 final Map _chineseNameToAsrCodeMap = {}; - + // 英文名称到 ASR 代码的映射 final Map _englishNameToAsrCodeMap = {}; - + // 短代码到 ASR 代码的映射 final Map _shortCodeToAsrCodeMap = {}; - + /// 初始化语言映射表 void _initLanguageMap() { // 中文相关 @@ -69,7 +69,7 @@ class LanguageManager extends GetxService { asrCode: 'zh-CN', ttsVoiceName: 'zh-CN-XiaoxiaoNeural', ); - + _addLanguage( shortCode: 'zh-Hant', chineseName: '中文(繁体)', @@ -77,7 +77,7 @@ class LanguageManager extends GetxService { asrCode: 'zh-TW', ttsVoiceName: 'zh-TW-HsiaoChenNeural', ); - + _addLanguage( shortCode: 'zh-Hant-hk', chineseName: '中文(香港繁体)', @@ -85,7 +85,7 @@ class LanguageManager extends GetxService { asrCode: 'zh-HK', ttsVoiceName: 'zh-HK-HiuMaanNeural', ); - + // 英语相关 _addLanguage( shortCode: 'en', @@ -94,7 +94,7 @@ class LanguageManager extends GetxService { asrCode: 'en-US', ttsVoiceName: 'en-US-AriaNeural', ); - + // 日语相关 _addLanguage( shortCode: 'ja', @@ -103,7 +103,7 @@ class LanguageManager extends GetxService { asrCode: 'ja-JP', ttsVoiceName: 'ja-JP-NanamiNeural', ); - + // 韩语相关 _addLanguage( shortCode: 'ko', @@ -112,7 +112,7 @@ class LanguageManager extends GetxService { asrCode: 'ko-KR', ttsVoiceName: 'ko-KR-SunHiNeural', ); - + // 欧洲语言 _addLanguage( shortCode: 'fr', @@ -121,7 +121,7 @@ class LanguageManager extends GetxService { asrCode: 'fr-FR', ttsVoiceName: 'fr-FR-DeniseNeural', ); - + _addLanguage( shortCode: 'es', chineseName: '西班牙语', @@ -129,7 +129,7 @@ class LanguageManager extends GetxService { asrCode: 'es-ES', ttsVoiceName: 'es-ES-ElviraNeural', ); - + _addLanguage( shortCode: 'pt', chineseName: '葡萄牙语', @@ -137,7 +137,7 @@ class LanguageManager extends GetxService { asrCode: 'pt-BR', ttsVoiceName: 'pt-BR-FranciscaNeural', ); - + _addLanguage( shortCode: 'it', chineseName: '意大利语', @@ -145,7 +145,7 @@ class LanguageManager extends GetxService { asrCode: 'it-IT', ttsVoiceName: 'it-IT-ElsaNeural', ); - + _addLanguage( shortCode: 'de', chineseName: '德语', @@ -153,7 +153,7 @@ class LanguageManager extends GetxService { asrCode: 'de-DE', ttsVoiceName: 'de-DE-KatjaNeural', ); - + _addLanguage( shortCode: 'ru', chineseName: '俄语', @@ -161,7 +161,7 @@ class LanguageManager extends GetxService { asrCode: 'ru-RU', ttsVoiceName: 'ru-RU-SvetlanaNeural', ); - + _addLanguage( shortCode: 'pl', chineseName: '波兰语', @@ -169,7 +169,7 @@ class LanguageManager extends GetxService { asrCode: 'pl-PL', ttsVoiceName: 'pl-PL-AgnieszkaNeural', ); - + _addLanguage( shortCode: 'nl', chineseName: '荷兰语', @@ -177,7 +177,7 @@ class LanguageManager extends GetxService { asrCode: 'nl-NL', ttsVoiceName: 'nl-NL-ColetteNeural', ); - + _addLanguage( shortCode: 'sv', chineseName: '瑞典语', @@ -185,7 +185,7 @@ class LanguageManager extends GetxService { asrCode: 'sv-SE', ttsVoiceName: 'sv-SE-SofieNeural', ); - + _addLanguage( shortCode: 'cs', chineseName: '捷克语', @@ -193,7 +193,7 @@ class LanguageManager extends GetxService { asrCode: 'cs-CZ', ttsVoiceName: 'cs-CZ-VlastaNeural', ); - + _addLanguage( shortCode: 'el', chineseName: '现代希腊语', @@ -201,7 +201,7 @@ class LanguageManager extends GetxService { asrCode: 'el-GR', ttsVoiceName: 'el-GR-AthinaNeural', ); - + _addLanguage( shortCode: 'ro', chineseName: '罗马尼亚语', @@ -209,7 +209,7 @@ class LanguageManager extends GetxService { asrCode: 'ro-RO', ttsVoiceName: 'ro-RO-AlinaNeural', ); - + _addLanguage( shortCode: 'hu', chineseName: '匈牙利语', @@ -217,7 +217,7 @@ class LanguageManager extends GetxService { asrCode: 'hu-HU', ttsVoiceName: 'hu-HU-NoemiNeural', ); - + _addLanguage( shortCode: 'uk', chineseName: '乌克兰语', @@ -225,7 +225,7 @@ class LanguageManager extends GetxService { asrCode: 'uk-UA', ttsVoiceName: 'uk-UA-PolinaNeural', ); - + _addLanguage( shortCode: 'da', chineseName: '丹麦语', @@ -233,7 +233,7 @@ class LanguageManager extends GetxService { asrCode: 'da-DK', ttsVoiceName: 'da-DK-ChristelNeural', ); - + _addLanguage( shortCode: 'fi', chineseName: '芬兰语', @@ -241,7 +241,7 @@ class LanguageManager extends GetxService { asrCode: 'fi-FI', ttsVoiceName: 'fi-FI-NooraNeural', ); - + _addLanguage( shortCode: 'no', chineseName: '挪威语', @@ -249,7 +249,7 @@ class LanguageManager extends GetxService { asrCode: 'nb-NO', ttsVoiceName: 'nb-NO-IselinNeural', ); - + _addLanguage( shortCode: 'hr', chineseName: '克罗地亚语', @@ -257,7 +257,7 @@ class LanguageManager extends GetxService { asrCode: 'hr-HR', ttsVoiceName: 'hr-HR-GabrijelaNeural', ); - + _addLanguage( shortCode: 'ca', chineseName: '加泰隆语', @@ -265,7 +265,7 @@ class LanguageManager extends GetxService { asrCode: 'ca-ES', ttsVoiceName: 'ca-ES-JoanaNeural', ); - + // 亚洲语言 _addLanguage( shortCode: 'ar', @@ -274,7 +274,7 @@ class LanguageManager extends GetxService { asrCode: 'ar-EG', ttsVoiceName: 'ar-EG-SalmaNeural', ); - + _addLanguage( shortCode: 'th', chineseName: '泰语', @@ -282,7 +282,7 @@ class LanguageManager extends GetxService { asrCode: 'th-TH', ttsVoiceName: 'th-TH-PremwadeeNeural', ); - + _addLanguage( shortCode: 'vi', chineseName: '越南语', @@ -290,7 +290,7 @@ class LanguageManager extends GetxService { asrCode: 'vi-VN', ttsVoiceName: 'vi-VN-HoaiMyNeural', ); - + _addLanguage( shortCode: 'id', chineseName: '印尼语', @@ -298,7 +298,7 @@ class LanguageManager extends GetxService { asrCode: 'id-ID', ttsVoiceName: 'id-ID-GadisNeural', ); - + _addLanguage( shortCode: 'hi', chineseName: '印地语', @@ -306,7 +306,7 @@ class LanguageManager extends GetxService { asrCode: 'hi-IN', ttsVoiceName: 'hi-IN-SwaraNeural', ); - + _addLanguage( shortCode: 'tr', chineseName: '土耳其语', @@ -314,7 +314,7 @@ class LanguageManager extends GetxService { asrCode: 'tr-TR', ttsVoiceName: 'tr-TR-EmelNeural', ); - + _addLanguage( shortCode: 'ms', chineseName: '马来语', @@ -322,7 +322,7 @@ class LanguageManager extends GetxService { asrCode: 'ms-MY', ttsVoiceName: 'ms-MY-YasminNeural', ); - + _addLanguage( shortCode: 'bn', chineseName: '孟加拉语', @@ -330,7 +330,7 @@ class LanguageManager extends GetxService { asrCode: 'bn-IN', ttsVoiceName: 'bn-IN-TanishaaNeural', ); - + _addLanguage( shortCode: 'ta', chineseName: '泰米尔语', @@ -338,7 +338,7 @@ class LanguageManager extends GetxService { asrCode: 'ta-IN', ttsVoiceName: 'ta-IN-PallaviNeural', ); - + _addLanguage( shortCode: 'te', chineseName: '泰卢固语', @@ -346,7 +346,7 @@ class LanguageManager extends GetxService { asrCode: 'te-IN', ttsVoiceName: 'te-IN-ShrutiNeural', ); - + _addLanguage( shortCode: 'mr', chineseName: '马拉提语', @@ -354,7 +354,7 @@ class LanguageManager extends GetxService { asrCode: 'mr-IN', ttsVoiceName: 'mr-IN-AarohiNeural', ); - + _addLanguage( shortCode: 'ur', chineseName: '乌尔都语', @@ -362,7 +362,7 @@ class LanguageManager extends GetxService { asrCode: 'ur-IN', ttsVoiceName: 'ur-IN-GulNeural', ); - + _addLanguage( shortCode: 'fa', chineseName: '波斯语', @@ -370,7 +370,7 @@ class LanguageManager extends GetxService { asrCode: 'fa-IR', ttsVoiceName: 'fa-IR-DilaraNeural', ); - + // 非洲语言 _addLanguage( shortCode: 'sw', @@ -379,10 +379,10 @@ class LanguageManager extends GetxService { asrCode: 'sw-KE', ttsVoiceName: 'sw-KE-ZuriNeural', ); - + Logger.info('语言管理器初始化完成,共加载 ${_languageMap.length} 种语言'); } - + /// 添加语言信息到映射表 void _addLanguage({ required String shortCode, @@ -400,133 +400,133 @@ class LanguageManager extends GetxService { ttsVoiceName: ttsVoiceName, note: note, ); - + _languageMap[asrCode] = languageInfo; _chineseNameToAsrCodeMap[chineseName] = asrCode; _englishNameToAsrCodeMap[englishName] = asrCode; _shortCodeToAsrCodeMap[shortCode] = asrCode; } - + /// 获取所有支持的语言信息 Map getAllLanguages() { return Map.from(_languageMap); } - + /// 获取所有支持的语言的中文名称 List getAllChineseNames() { return _chineseNameToAsrCodeMap.keys.toList(); } - + /// 获取所有支持的语言的英文名称 List getAllEnglishNames() { return _englishNameToAsrCodeMap.keys.toList(); } - + /// 根据 ASR 代码获取语言信息 LanguageInfo? getLanguageByAsrCode(String asrCode) { return _languageMap[asrCode]; } - + /// 根据中文名称获取语言信息 LanguageInfo? getLanguageByChineseName(String chineseName) { final asrCode = _chineseNameToAsrCodeMap[chineseName]; if (asrCode == null) return null; return _languageMap[asrCode]; } - + /// 根据英文名称获取语言信息 LanguageInfo? getLanguageByEnglishName(String englishName) { final asrCode = _englishNameToAsrCodeMap[englishName]; if (asrCode == null) return null; return _languageMap[asrCode]; } - + /// 根据短代码获取语言信息 LanguageInfo? getLanguageByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]; } - + /// 根据中文名称获取 ASR 代码 String? getAsrCodeByChineseName(String chineseName) { return _chineseNameToAsrCodeMap[chineseName]; } - + /// 根据英文名称获取 ASR 代码 String? getAsrCodeByEnglishName(String englishName) { return _englishNameToAsrCodeMap[englishName]; } - + /// 根据短代码获取 ASR 代码 String? getAsrCodeByShortCode(String shortCode) { return _shortCodeToAsrCodeMap[shortCode]; } - + /// 根据 ASR 代码获取中文名称 String? getChineseNameByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.chineseName; } - + /// 根据 ASR 代码获取英文名称 String? getEnglishNameByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.englishName; } - + /// 根据 ASR 代码获取短代码 String? getShortCodeByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.shortCode; } - + /// 根据 ASR 代码获取 TTS 语音名称 String? getTtsVoiceNameByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.ttsVoiceName; } - + /// 根据中文名称获取 TTS 语音名称 String? getTtsVoiceNameByChineseName(String chineseName) { final asrCode = _chineseNameToAsrCodeMap[chineseName]; if (asrCode == null) return null; return _languageMap[asrCode]?.ttsVoiceName; } - + /// 根据短代码获取中文名称 String? getChineseNameByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]?.chineseName; } - + /// 根据短代码获取英文名称 String? getEnglishNameByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]?.englishName; } - + /// 根据短代码获取 TTS 语音名称 String? getTtsVoiceNameByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]?.ttsVoiceName; } - + /// 获取中文名称到 ASR 代码的映射 Map getChineseNameToAsrCodeMap() { return Map.from(_chineseNameToAsrCodeMap); } - + /// 获取英文名称到 ASR 代码的映射 Map getEnglishNameToAsrCodeMap() { return Map.from(_englishNameToAsrCodeMap); } - + /// 获取短代码到 ASR 代码的映射 Map getShortCodeToAsrCodeMap() { return Map.from(_shortCodeToAsrCodeMap); } -} \ No newline at end of file +} diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt index f2b170bdf..8aebb37a8 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt @@ -1315,7 +1315,16 @@ class VolcanoTranslationServiceImpl : "ms-MY" to "ms", // 马来语(马来西亚) "id-ID" to "id", // 印尼语(印尼) "fil-PH" to "tl", // 菲律宾语(菲律宾) - + "sv-SE" to "sv", // 瑞典语(瑞典) + "ro-RO" to "ro", // 罗马尼亚语(罗马尼亚) + "hu-HU" to "hu", // 匈牙利语(匈牙利) + "da-DK" to "da", // 丹麦语(丹麦) + "fi-FI" to "fi", // 芬兰语(芬兰) + "nb-NO" to "nb", // 挪威语(挪威) + "hr-HR" to "hr", // 克罗地亚语(克罗地亚) + "ca-ES" to "ca", // 加泰隆语 + "mr-IN" to "mr", // 马拉地语(印度) + "ur-IN" to "ur", // 乌尔都语(印度) // 欧洲 "en-US" to "en", // 英语(美国) "en-GB" to "en", // 英语(英国) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift index 4659e56c3..97db886f4 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift @@ -605,13 +605,36 @@ import os.log audioProcessor?.stopMicrophoneCapture() recordFile?.closeFile(isSave: true) - // 等待当前处理完成 - while serviceState.isTranslating || serviceState.isSynthesizing { - Thread.sleep(forTimeInterval: 0.1) + // 使用后台队列等待当前处理完成,避免阻塞主线程 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + + // 设置超时时间,避免无限等待 + let timeout = DispatchTime.now() + .seconds(5) + + while (self.serviceState.isTranslating || self.serviceState.isSynthesizing) && DispatchTime.now() < timeout { + Thread.sleep(forTimeInterval: 0.1) + } + + // 强制停止合成器(如果仍在运行) + if self.serviceState.isSynthesizing { + do { + try self.synthesizer?.stopSpeaking() + } catch { + os_log("停止语音合成失败: %@", log: self.log, type: .error, error.localizedDescription) + } + self.serviceState.isSynthesizing = false + + DispatchQueue.main.async { + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) + } + } + + DispatchQueue.main.async { + os_log("停止连续语音翻译", log: self.log, type: .info) + } } - os_log("停止连续语音翻译", log: log, type: .info) - } catch { os_log("停止翻译失败: %@", log: log, type: .error, error.localizedDescription) eventCallback?.onError(component: "Service", error: "停止失败: \(error.localizedDescription)") diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift index 7ef63cdeb..5ad252aac 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift @@ -24,6 +24,8 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T private var region = "cn-north-1" private var maxRetryAttempts = 3 private var timeout: TimeInterval = 10.0 + private var isDisposed = false // 添加释放状态标志 + private let disposeLock = NSLock() // 添加线程安全锁 // HTTP客户端 private lazy var urlSession: URLSession = { @@ -52,7 +54,16 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T "ms-MY": "ms", // 马来语(马来西亚) "id-ID": "id", // 印尼语(印尼) "fil-PH": "tl", // 菲律宾语(菲律宾) - + "sv-SE": "sv", // 瑞典语(瑞典) + "ro-RO": "ro", // 罗马尼亚语(罗马尼亚) + "hu-HU": "hu", // 匈牙利语(匈牙利) + "da-DK": "da", // 丹麦语(丹麦) + "fi-FI": "fi", // 芬兰语(芬兰) + "nb-NO": "nb", // 挪威语(挪威) + "hr-HR": "hr", // 克罗地亚语(克罗地亚) + "ca-ES": "ca", // 加泰隆语 + "mr-IN": "mr", // 马拉地语(印度) + "ur-IN": "ur", // 乌尔都语(印度) // 欧洲 "en-US": "en", // 英语(美国) "en-GB": "en", // 英语(英国) @@ -150,11 +161,25 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T /** * 释放资源 + * 添加线程安全保护,防止重复释放 */ public func dispose() { + disposeLock.lock() + defer { disposeLock.unlock() } + + if isDisposed { + os_log("翻译服务已经释放,跳过重复释放", log: log, type: .info) + return + } + + os_log("开始释放翻译服务资源", log: log, type: .info) + isDisposed = true isInitialized = false + + // 取消并失效URLSession urlSession.invalidateAndCancel() - os_log("火山翻译服务已释放", log: log, type: .info) + + os_log("翻译服务资源释放完成", log: log, type: .info) } // MARK: - Private Methods @@ -203,6 +228,7 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T /** * 执行翻译 + * 添加服务状态检查,防止在已释放的会话中创建任务 */ private func performTranslation( text: String, @@ -210,6 +236,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T targetLanguage: String ) async -> IntegratedSpeechTranslationService.TranslationResult { + // 检查服务是否已被释放 + disposeLock.lock() + let disposed = isDisposed + disposeLock.unlock() + + if disposed { + os_log("翻译服务已释放,取消翻译请求", log: log, type: .error) + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译服务已释放" + ) + } + do { // 转换语言代码 let sourceCode = getLanguageCode(languageCode: sourceLanguage) @@ -263,6 +302,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T os_log("发送翻译请求到: %@", log: log, type: .debug, url.absoluteString) + // 再次检查服务状态,防止在请求构建过程中被释放 + disposeLock.lock() + let stillDisposed = isDisposed + disposeLock.unlock() + + if stillDisposed { + os_log("翻译服务在请求过程中被释放,取消网络请求", log: log, type: .error) + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译服务已释放" + ) + } + // 发送请求 let (data, response) = try await urlSession.data(for: request) From ce3096a727cf9c1384c9d47f4807acf5ff64e8de Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Tue, 9 Sep 2025 11:25:08 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=89=E5=8D=93?= =?UTF-8?q?=E9=80=9A=E8=AF=9D=E7=BF=BB=E8=AF=91=E9=A2=91=E7=B9=81=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E5=A5=94=E6=BA=83=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../azure_speech/AzureAsrToAsr.kt | 130 +++++++++++------- 1 file changed, 80 insertions(+), 50 deletions(-) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt index 8aebb37a8..bc154fe05 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt @@ -475,26 +475,57 @@ class IntegratedSpeechTranslationService( } /** - * 停止连续语音翻译 + * 停止连续语音翻译(非阻塞) + * 在 IO 线程内顺序停止识别器、录音线程与文件,并对翻译/合成收尾增加统一超时保护,避免阻塞主线程与潜在 ANR。 */ fun stopContinuousTranslation() { try { - recognizer?.stopContinuousRecognitionAsync() - audioProcessor?.stopRecording() - recordfile1!!.closeFile(true) + Log.d(TAG, "停止连续语音翻译-开始") + // 先标记识别关闭,避免后续流程继续入队处理 serviceState.isRecognizing.set(false) - // 等待当前处理完成 - while (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) { - Thread.sleep(100) - } - Log.d(TAG, "停止连续语音翻译") + // 在 IO 线程中做真实的停止动作,避免阻塞主线程 + launch(Dispatchers.IO) { + // 1) 停止识别器:等待短超时,避免卡住 + try { + recognizer?.stopContinuousRecognitionAsync() + ?.get(1500, java.util.concurrent.TimeUnit.MILLISECONDS) + } catch (t: Throwable) { + Log.w(TAG, "停止识别器超时或失败", t) + } + + // 2) 停止录音线程(打断并短等待退出) + runCatching { audioProcessor?.stopRecording() } + .onFailure { Log.w(TAG, "停止录音失败", it) } + + // 3) 关闭录音文件(若开启过录音) + runCatching { recordfile1?.closeFile(true) } + .onFailure { Log.w(TAG, "关闭录音文件失败", it) } + + // 4) 等待翻译/合成收尾,整体不超过 2s + val start = System.currentTimeMillis() + while ((serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) + && System.currentTimeMillis() - start < 2000 + ) { + delay(100) + } + if (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) { + Log.w(TAG, "等待翻译/合成完成超时,强制标记结束") + } + + // 5) 统一落幕:确保状态关闭 + serviceState.isTranslating.set(false) + serviceState.isSynthesizing.set(false) + + Log.d(TAG, "停止连续语音翻译-完成") + } } catch (e: Exception) { Log.e(TAG, "停止翻译失败", e) eventCallback?.onError("Service", "停止失败: ${e.message}") } } + /** * 处理翻译和合成流程 */ @@ -1184,58 +1215,57 @@ fun getSupportedLanguages(): List { } /** - * 改进的资源释放方法 + * 安全释放音频处理器资源 + * 添加更强的异常处理和超时保护 */ fun dispose() { try { Log.d(TAG, "开始释放音频处理器资源...") - // 1. 停止运行标志 - isRunning.set(false) + // 1. 设置停止标志 + if (!isRunning.compareAndSet(true, false)) { + Log.d(TAG, "音频处理器已停止,跳过释放") + return + } - // 2. 中断处理线程 - processingThread?.let { thread -> - if (thread.isAlive) { + // 1) 安全停止处理线程 + processingThread?.let { thread -> + if (thread.isAlive) { + try { thread.interrupt() - try { - // 等待线程结束,最多等待2秒 - thread.join(2000) - if (thread.isAlive) { - Log.w(TAG, "音频处理线程未能正常结束") - } else { - Log.d(TAG, "音频处理线程已结束") - } - } catch (e: InterruptedException) { - Log.w(TAG, "等待音频处理线程结束时被中断") - Thread.currentThread().interrupt() + thread.join(500) + if (thread.isAlive) { + Log.w(TAG, "音频处理线程未在500ms内结束") + // 不强制杀死,交由系统回收,但继续清理资源 + } else { + Log.d(TAG, "音频处理线程已正常结束") } + } catch (e: InterruptedException) { + Log.w(TAG, "等待线程结束时被中断", e) + Thread.currentThread().interrupt() + } catch (e: Exception) { + Log.e(TAG, "停止处理线程时发生异常", e) } } - processingThread = null - - // 3. 关闭音频流 - try { - pushAudioStream?.close() - pushAudioStream = null - Log.d(TAG, "音频流已关闭") - } catch (e: Exception) { - Log.e(TAG, "关闭音频流失败", e) - } - - // 4. 清理音频队列 - try { - val remainingData = audioQueue.size - audioQueue.clear() - Log.d(TAG, "音频队列已清理,丢弃 $remainingData 个数据包") - } catch (e: Exception) { - Log.e(TAG, "清理音频队列失败", e) - } - - Log.d(TAG, "音频处理器资源释放完成") - - } catch (e: Exception) { - Log.e(TAG, "释放音频处理器资源时发生异常", e) } + processingThread = null + + // 2) 关闭输入流 + try { + pushAudioStream?.close() + } catch (t: Throwable) { + Log.w(TAG, "关闭 PushAudioInputStream 时异常", t) + } finally { + pushAudioStream = null + } + + // 3) 清空队列 + audioQueue.clear() + + Log.d(TAG, "音频处理器资源释放完成") + } catch (e: Exception) { + Log.e(TAG, "释放音频处理器资源失败", e) + } } } }