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] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=94=A4=E9=86=92=E9=80=9F?= =?UTF-8?q?=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