diff --git a/lib/data/models/appconfig.dart b/lib/data/models/appconfig.dart index 22a09d84a..1d1b844ab 100644 --- a/lib/data/models/appconfig.dart +++ b/lib/data/models/appconfig.dart @@ -42,6 +42,11 @@ class AppConfig { return names; } + static List allproductUuids() { + List uuids = config.products.map((user) => user.scanuuid).toList(); + return uuids; + } + static DBProduct? getproduct(String name) { final foundProduct = config.products.firstWhere((product) => product.devicename == name); diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index a835a5e39..5d1af6ca3 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -833,9 +833,9 @@ class BleManager extends GetxService { } /// 验证设备是否可以配对 - Future startScan(String devicename) async { + Future startScan(List uuids) async { try { - await _bleService.startScan(devicename); + await _bleService.startScan(uuids); } catch (e) { Logger.error('开始扫描设备: ${e.toString()}'); } @@ -860,9 +860,9 @@ class BleManager extends GetxService { } } - Future>?> getDeviceList(String deviceName) async { + Future>?> getDeviceList(String devicename) async { try { - final result = _bleService.getDeviceList(deviceName); + final result = _bleService.getDeviceList(devicename); return result; } catch (e) { Logger.error('获取设备信息: ${e.toString()}'); diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index 630e7b98c..369e0ca31 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -37,7 +37,7 @@ class AzureTtsService extends GetxService implements TtsService { // 可观察状态 final isEnabled = true.obs; final _isSpeaking = false.obs; - + String _currsessionid = ''; // 流式文本缓冲区 String _streamBuffer = ''; @@ -183,13 +183,32 @@ class AzureTtsService extends GetxService implements TtsService { } @override - Future speakOnce(String text) async { + Future startspeak(String sessionid) async { + if (!_isInitialized) await initialize(); + if (!isEnabled.value) return false; + + try { + // 开始合成,传递参数 + final result = await _channel.invokeMethod('startspeak', { + 'sessionid': sessionid, + }); + + return result == true; + } catch (e) { + Logger.error('语音合成失败: ${e.toString()}'); + return false; + } + } + + @override + Future speakOnce(String sessionid, String text) async { if (!_isInitialized) await initialize(); if (!isEnabled.value || text.isEmpty) return false; try { // 开始合成,传递参数 final result = await _channel.invokeMethod('speakText', { + 'sessionid': sessionid, 'text': text, }); @@ -201,7 +220,7 @@ class AzureTtsService extends GetxService implements TtsService { } @override - Future speakStream(String text) async { + Future speakStream(String sessionid, String text) async { if (!isEnabled.value || text.isEmpty) return false; try { @@ -256,12 +275,12 @@ class AzureTtsService extends GetxService implements TtsService { } @override - Future flushStream() async { + Future flushStream(String sessionid) async { if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; try { // 处理缓冲区中可能的完整句子 - await speakStream(''); + await speakStream(sessionid, ''); // 如果缓冲区仍有剩余文本,将其添加到播放队列 if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { @@ -345,7 +364,7 @@ class AzureTtsService extends GetxService implements TtsService { final text = _textQueue.first; // 开始合成 - final success = await speakOnce(text); + final success = await speakOnce(_currsessionid, text); // 仅当合成操作失败时才从队列移除并继续处理下一项 // 成功的话会等待合成完成事件后自动处理下一项 diff --git a/lib/data/services/speech_impl/voice_clone_tts_service.dart b/lib/data/services/speech_impl/voice_clone_tts_service.dart index 4fa6514ed..a779270c4 100644 --- a/lib/data/services/speech_impl/voice_clone_tts_service.dart +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -27,6 +27,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { late ConcatenatingAudioSource _playlist; // 当前语音 + String _currsessionid = ''; final RxString _currentVoice = ''.obs; @override @@ -215,7 +216,21 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } @override - Future speakOnce(String text) async { + Future startspeak(String sessionid) async { + if (!_isInitialized) await initialize(); + if (!isEnabled.value) return false; + + try { + _currsessionid = sessionid; + return true; + } catch (e) { + Logger.error('语音合成失败: ${e.toString()}'); + return false; + } + } + + @override + Future speakOnce(String sessionid, String text) async { if (!isEnabled.value || text.isEmpty) return false; try { @@ -236,7 +251,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } @override - Future speakStream(String text) async { + Future speakStream(String sessionid, String text) async { if (!isEnabled.value || text.isEmpty) return false; try { @@ -295,12 +310,14 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } @override - Future flushStream() async { + Future flushStream( + String sessionid, + ) async { if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; try { // 处理缓冲区中可能的完整句子 - await speakStream(''); + await speakStream(sessionid, ''); // 如果缓冲区仍有剩余文本,将其添加到播放队列 if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { @@ -344,7 +361,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { final item = _textQueue.removeAt(0); // 使用speakOnce播放当前项 - await speakOnce(item); + await speakOnce(_currsessionid, item); } } catch (e) { Logger.error('处理语音队列出错: ${e.toString()}'); diff --git a/lib/data/services/speech_impl/volcano_tts_service.dart b/lib/data/services/speech_impl/volcano_tts_service.dart index f84ed6b0a..b2bdcc2a1 100644 --- a/lib/data/services/speech_impl/volcano_tts_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_service.dart @@ -206,7 +206,26 @@ class VolcanoTtsService extends GetxService implements TtsService { } @override - Future speakOnce(String text) async { + Future startspeak(String sessionid) async { + if (!await _ensureInitialized()) return false; + + try { + // 单次播放 + final result = await _channel.invokeMethod('startspeak', { + 'sessionid': sessionid, + }) ?? + false; + + return result; + } catch (e) { + debugPrint('单次播放失败:$e'); + _eventStreamController.add(TtsEvent.error('单次播放失败:$e')); + return false; + } + } + + @override + Future speakOnce(String sessionid, String text) async { if (!await _ensureInitialized()) return false; try { @@ -225,7 +244,7 @@ class VolcanoTtsService extends GetxService implements TtsService { } @override - Future speakStream(String text) async { + Future speakStream(String sessionid, String text) async { if (!await _ensureInitialized()) return false; try { @@ -244,7 +263,9 @@ class VolcanoTtsService extends GetxService implements TtsService { } @override - Future flushStream() async { + Future flushStream( + String sessionid, + ) async { if (!await _ensureInitialized()) return false; try { diff --git a/lib/data/services/tts_service.dart b/lib/data/services/tts_service.dart index 996437def..f36827699 100644 --- a/lib/data/services/tts_service.dart +++ b/lib/data/services/tts_service.dart @@ -19,14 +19,17 @@ abstract class TtsService { /// 设置语音 Future setVoice(String voiceName); + ///开始新的播放会话 + Future startspeak(String sessionid); + /// 单次播放文本,完成后返回 - Future speakOnce(String text); + Future speakOnce(String sessionid, String text); /// 将文本加入播放队列,支持连续播放多段文本 - Future speakStream(String text); + Future speakStream(String sessionid, String text); /// 播放队列中剩余文本 - Future flushStream(); + Future flushStream(String sessionid); /// 停止播放 Future stop(); diff --git a/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart b/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart index 8b72d435e..9e55b7d18 100644 --- a/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart +++ b/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart @@ -42,7 +42,7 @@ class FTFTranslationController extends GetxController final sourceLanguageCode = 'zh-CN'.obs; final targetLanguageCode = 'en-US'.obs; final isTtsEnabled = true.obs; - + var _currsessionid = ''; @override void onInit() { super.onInit(); @@ -168,7 +168,7 @@ class FTFTranslationController extends GetxController 'en-US-AriaNeural'; await _ttsService.setVoice(voiceName); - await _ttsService.speakOnce(text); + await _ttsService.speakOnce(_currsessionid, text); } catch (e) { Logger.error('播放翻译失败: ${e.toString()}'); } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index fd7dc00e4..631f1c78d 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -318,8 +318,8 @@ class AgentController extends GetxController { // 更新分页状态 currentPage.value = 1; hasMoreMessages.value = result.totalPages > 1; - print( - 'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}'); + // print( + // 'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}'); Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); _logMessageDetails(historyMessages); @@ -800,12 +800,14 @@ class AgentController extends GetxController { ["card_navigation"]["start"] as String; final end = metaResult["maps_route_navigation"] ["card_navigation"]["end"] as String; - // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); - Future.delayed(Duration(seconds: 2), () { - // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); - // 2秒后执行的代码 - openNavigation(start, end, ""); - }); + final items = metaResult['items']; + if (items != null) { + Future.delayed(Duration(seconds: 2), () { + // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); + // 2秒后执行的代码 + openNavigation(start, end, ""); + }); + } } } } @@ -949,10 +951,6 @@ class AgentController extends GetxController { final text = textController.text.trim(); if (text.isEmpty) return; - final message = Message(isUser: true, text: text); - messages.add(message); - textController.clear(); - // 使用reverse:true时通常不需要手动滚动 // 3. 调用AI服务处理 try { @@ -963,6 +961,9 @@ class AgentController extends GetxController { // 根据TTS状态决定是否朗读 await AgentService.processTextInput(text, speakResponse: isTtsEnabled.value); + final message = Message(isUser: true, text: text); + messages.add(message); + textController.clear(); } catch (e) { Logger.e(TAG, '发送文本消息失败: $e'); isProcessing.value = false; @@ -1597,7 +1598,7 @@ class AgentController extends GetxController { isPushToTalkActive.value = true; //await AgentService.recognizeCallback(); // 开始语音识别 - await AgentService.startConversation(mode: 'push_to_talk'); + AgentService.startConversation(mode: 'push_to_talk'); // await startVoiceInput(); } @@ -1611,7 +1612,7 @@ class AgentController extends GetxController { // 清除按住说话状态(但保持按住说话模式) isPushToTalkActive.value = false; - await AgentService.stopConversation(); + AgentService.stopConversation(); // 停止语音识别 // await stopVoiceInput(); } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 7ed4c0ddd..054a37ca0 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; +import 'package:haptic_feedback/haptic_feedback.dart'; import '../../../core/widgets/intro_overlay_builder.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; @@ -616,6 +617,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { _holdTimer = Timer( const Duration(milliseconds: 100), () { + Haptics.vibrate(HapticsType.success); controller.startPushToTalk(); // if (activeSpeaker == 0) { // if (speakerId == 1) { diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index c26503de6..d4120a931 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -296,20 +296,22 @@ class MessageBubble extends GetView { final start = card_navigation['start'] as String? ?? ''; final end = card_navigation['end'] as String? ?? ''; final destination = card_navigation['destination'] as String? ?? ''; - final rawList = card_navigation['items'] as List? ?? []; - final list = - rawList.map((e) => Map.from(e)).toList(); - final rawNavigation = card_navigation['navigation']; - final Map? navigation = - rawNavigation is Map ? rawNavigation : null; - print("导航实时数据 ${rawNavigation}"); - // 导航 - return navigationCard(context, - start: start, - end: end, - destination: destination, - modes: list, - navigation: navigation); + final rawList = card_navigation['items'] as List? ?? null; + if (rawList != null) { + final list = + rawList.map((e) => Map.from(e)).toList(); + final rawNavigation = card_navigation['navigation']; + final Map? navigation = + rawNavigation is Map ? rawNavigation : null; + // print("导航实时数据 ${rawNavigation}"); + // 导航 + return navigationCard(context, + start: start, + end: end, + destination: destination, + modes: list, + navigation: navigation); + } } } diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index c3fa87398..d3a95129e 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,4 +1,3 @@ - import 'package:get/get.dart'; import 'dart:async'; import '../../../data/services/novice_guide_service.dart'; @@ -69,7 +68,7 @@ class HomeController extends GetxController { super.onInit(); _otaCheckService = Get.find(); _initWithCachedInfo(); - _startBatteryRefreshTimer(); // 启动电池信息定时刷新 + // _startBatteryRefreshTimer(); // 启动电池信息定时刷新 } @override @@ -158,12 +157,12 @@ class HomeController extends GetxController { }); } // 根据连接状态控制定时器 - if (isNowConnected) { - _startBatteryRefreshTimer(); // 连接时启动定时器 - } else { - _stopBatteryRefreshTimer(); // 断开时停止定时器 - // 注意:不再重置OTA检查状态,因为现在由全局服务管理 - } + // if (isNowConnected) { + // _startBatteryRefreshTimer(); // 连接时启动定时器 + // } else { + // _stopBatteryRefreshTimer(); // 断开时停止定时器 + // // 注意:不再重置OTA检查状态,因为现在由全局服务管理 + // } } update(); @@ -351,7 +350,8 @@ class HomeController extends GetxController { Logger.d(_tag, 'OTA检查状态: $status'); // 使用全局OTA检查服务执行检查 - final success = await _otaCheckService.performOtaCheck(_isFullyConnected.value); + final success = + await _otaCheckService.performOtaCheck(_isFullyConnected.value); if (success) { Logger.d(_tag, 'OTA检查已成功执行'); @@ -365,6 +365,4 @@ class HomeController extends GetxController { Map getOtaCheckStatus() { return _otaCheckService.getCheckStatus(); } - - } diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index f2783f4bb..feddfb18e 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -735,6 +735,7 @@ class PairingController extends GetxController with WidgetsBindingObserver { } final currentBleState = await _bleManager.getConnectionState(); final names = AppConfig.allproductNames(); + final uuids = AppConfig.allproductUuids(); // 检查是否已有绑定的设备 print("device.address${device.address}"); if (User.instance.devices.isNotEmpty) { @@ -791,13 +792,13 @@ class PairingController extends GetxController with WidgetsBindingObserver { } } print( - '获取到 deviceName: $deviceName address: ${device.address} names:$names'); + '获取到 deviceName: $deviceName address: ${device.address} uuids:$uuids'); if (names.contains(deviceName)) { if (currentBleState == 2) { //已连接直接返回 return 9; } - await _bleManager.startScan(deviceName); + await _bleManager.startScan(uuids); await Future.delayed(const Duration(seconds: 1)); // 尝试最多读取 3 次设备信息,每次间隔 1 秒 List>? deviceinfos; diff --git a/lib/modules/speech_test/controllers/speech_test_controller.dart b/lib/modules/speech_test/controllers/speech_test_controller.dart index 57d54bcad..a2f74d3bf 100644 --- a/lib/modules/speech_test/controllers/speech_test_controller.dart +++ b/lib/modules/speech_test/controllers/speech_test_controller.dart @@ -53,6 +53,8 @@ class SpeechTestController extends GetxController { '这是第三句测试语音,将在连续模式下播放完毕。' ]; + var _currsessionid = ''; + @override void onInit() { super.onInit(); @@ -246,7 +248,7 @@ class SpeechTestController extends GetxController { isProcessing.value = true; ttsStatus.value = '准备播放...'; - final result = await _ttsService.speakOnce(text); + final result = await _ttsService.speakOnce(_currsessionid, text); if (!result) { ttsStatus.value = '播放失败'; @@ -269,7 +271,7 @@ class SpeechTestController extends GetxController { // 为避免阻塞,使用一个句子接一个句子的方式添加到队列 for (final sentence in testSentences) { - if (!await _ttsService.speakStream(sentence)) { + if (!await _ttsService.speakStream(_currsessionid, sentence)) { ttsStatus.value = '添加句子到队列失败'; break; } @@ -278,7 +280,7 @@ class SpeechTestController extends GetxController { } // 刷新队列,确保播放完成 - await _ttsService.flushStream(); + await _ttsService.flushStream(_currsessionid); } catch (e) { ttsStatus.value = '连续播放异常: $e'; Logger.error('连续播放异常: ${e.toString()}'); diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 540d20409..326001c0a 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -88,6 +88,7 @@ class TranslationController extends GetxController { final targetLanguageCode = 'en-US'.obs; var detectedLanguageCode = ''; + var currsessionid = ''; final currentModeTitle = 'simultaneousTranslation'.obs; final currentMode = 'simultaneous'.obs; // 翻译历史和当前项 @@ -937,7 +938,7 @@ class TranslationController extends GetxController { final startTime = DateTime.now(); await _ttsService.setVoice(voiceName); - await _ttsService.speakOnce(text); + await _ttsService.speakOnce(currsessionid, text); final endTime = DateTime.now(); // 记录TTS API调用统计 - 优化版(基于文本字符数计费) @@ -993,7 +994,7 @@ class TranslationController extends GetxController { _languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ?? 'en-US-AriaNeural'; await _ttsService.setVoice(voiceName); - await _ttsService.speakOnce(item.translatedText); + await _ttsService.speakOnce(currsessionid, item.translatedText); } catch (e) { Logger.error('播放翻译项失败: ${e.toString()}'); } @@ -1012,7 +1013,7 @@ class TranslationController extends GetxController { 'en-US-AriaNeural'; await _ttsService.setVoice(voiceName); - await _ttsService.speakOnce(text); + await _ttsService.speakOnce(currsessionid, text); } catch (e) { Logger.error('播放文本失败: ${e.toString()}'); } 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 5a050b840..347838118 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 @@ -378,7 +378,7 @@ class AgentServiceImpl: NSObject { return false } } - + stopTts(); if isRecognizing { stopRecognition() } @@ -396,7 +396,7 @@ class AgentServiceImpl: NSObject { } print("ai启动语音=\(audioSourceType)=\(useBle)") guard let success = azureAsrHelper?.startContinuousRecognition( - // callback: self, + // callback: self, audioSourceType: audioSourceType ), success else { sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") @@ -456,7 +456,7 @@ audioStream.saveAudioDataTo(data: audioData) return true } - func speakText(_ text: String) -> Bool { + func speakText(sessionid sessionid:String,_ text: String) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false @@ -466,7 +466,7 @@ audioStream.saveAudioDataTo(data: audioData) return false } - return azureTtsHelper?.speakOnce(text) ?? false + return azureTtsHelper?.speakOnce(sessionid:sessionid,text) ?? false } func stopTts() -> Bool { @@ -591,6 +591,7 @@ audioStream.saveAudioDataTo(data: audioData) imagePath: imagePath ) currsessionId = UUID().uuidString + azureTtsHelper?.startspeak(sessionid: currsessionId) os_log("设置回调并调用sendMessageStream", log: logger, type: .info) chatApiService.setStreamCallback(currsessionId,callback) chatApiService.sendMessageStream(currsessionId,messages: messages) @@ -1253,7 +1254,7 @@ class ChatApiStreamCallback: StreamCallback { responseBuilder += token if speakResponse && reply && broadcast{ - try agentService.azureTtsHelper?.speakStream(token) + try agentService.azureTtsHelper?.speakStream(sessionid:sessionId,token) // 在开始流式TTS时立即停止气泡音 } if (reply && token != "") { @@ -1275,7 +1276,7 @@ class ChatApiStreamCallback: StreamCallback { if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{ - agentService.azureTtsHelper?.flushStream() + agentService.azureTtsHelper?.flushStream(sessionid:sessionId) } let response = responseBuilder @@ -1522,7 +1523,7 @@ class AudioPlayer { stopAwaitSound() // 播放等待音效 - playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer) + playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer,volume:0.2) // 启动超时定时器 startAwaitTimeout() @@ -1563,7 +1564,7 @@ class AudioPlayer { os_log("未找到音频文件: %{public}@.%{public}@ in bundle %{public}@", log: logger, type: .error, named, fileType, resourcesBundle.bundlePath) } - private func playLoopSound(named: String, fileType: String = "mp3", player: inout AVAudioPlayer?) { + private func playLoopSound(named: String, fileType: String = "mp3", player: inout AVAudioPlayer?, volume: Float = 1.0) { // 停止当前播放的循环音频 player?.stop() player = nil @@ -1571,6 +1572,10 @@ class AudioPlayer { if let path = resourcesBundle.path(forResource: named, ofType: fileType) { let url = URL(fileURLWithPath: path) safePlayLoopAudio(url: url, player: &player) + + // 在创建播放器之后设置音量 + player?.volume = volume + return } @@ -1707,7 +1712,10 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { hasSpeechDetected = false // 使用模式感知的空闲检测启动方法 startIdleCheckForMode(mode: currentRecognitionMode) - audioPlayer?.playStartSound() + if (currentRecognitionMode == "ble_wakeup") { + audioPlayer?.playStartSound() + } + } func onSessionStopped() { @@ -1715,7 +1723,9 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { isRecognizing = false hasSpeechDetected = false stopIdleCheck() - audioPlayer?.playStopSound() + if (currentRecognitionMode == "ble_wakeup") { + audioPlayer?.playStopSound() + } } func onCanceled(_ reason: String, _ errorDetails: String) { 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 4664ba735..f113e5a3b 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 @@ -86,12 +86,13 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { case "speakText": guard let arguments = call.arguments as? [String: Any], + let sessionid = arguments["sessionid"] as? String, let text = arguments["text"] as? String else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) return } - let success = impl.speakText(text) + let success = impl.speakText(sessionid:sessionid,text) result(success) case "stopTts": diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index d8795a60c..62783be72 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -711,9 +711,6 @@ public class AzureAsrHelper: NSObject { * 重命名指定路径的音频文件 */ public func renameFile(filePath: String, newName: String)-> Bool { - - - audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call return true } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index f068f476f..1359d3426 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -337,6 +337,7 @@ private func sendAstEvent(_ event: [String: Any]) { os_log("音频流未初始化", type: .error) return } + print("liwei----------外部音频推送:\(audioBytes.data.count)") audioStream.saveAudioDataTo(data: audioBytes.data) @@ -480,26 +481,33 @@ private func sendAstEvent(_ event: [String: Any]) { case "speakText": guard let args = call.arguments as? [String: Any], + let sessionid = args["sessionid"] as? String, let text = args["text"] as? String else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) return } - let success = azureTtsHelper.speakOnce(text) + let success = azureTtsHelper.speakOnce(sessionid:sessionid,text) result(success) case "speakStream": guard let args = call.arguments as? [String: Any], + let sessionid = args["sessionid"] as? String, let text = args["text"] as? String else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) return } - let success = azureTtsHelper.speakStream(text) + let success = azureTtsHelper.speakStream(sessionid:sessionid,text) result(success) case "flushStream": - let success = azureTtsHelper.flushStream() + guard let args = call.arguments as? [String: Any], + let sessionid = args["sessionid"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) + return + } + let success = azureTtsHelper.flushStream(sessionid: sessionid) result(success) case "stopSpeaking": @@ -892,6 +900,7 @@ extension AzureSpeechPlugin: BleService.Callback { os_log("音频流未初始化", type: .error) return } + print("liwei--------------接收到音频数据回调") audioStream.saveAudioDataTo(data: data) } @@ -903,6 +912,7 @@ extension AzureSpeechPlugin: BleService.Callback { os_log("音频流未初始化", type: .error) return } + print("liwei--------------接收到音频数据回调1") audioStream.saveAudioDataTo(data: data) } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift index 1e147a899..bd9ccfc32 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift @@ -16,6 +16,7 @@ public class AzureTtsHelper: NSObject, ITtsService { private var synthesizer: SPXSpeechSynthesizer? private var isInitialized = false private var speaking = false + private var sessionid = "" //语音播报任务idsetAudioOutputRoute // 当前配置 private var currentVoice = DEFAULT_VOICE @@ -109,10 +110,14 @@ public class AzureTtsHelper: NSObject, ITtsService { } } + public func startspeak(sessionid sessionid: String)-> Bool { + self.sessionid = sessionid + return true + } /** * 单次合成并播放 */ - public func speakOnce(_ text: String) -> Bool { + public func speakOnce(sessionid sessionid: String,_ text: String) -> Bool { if !isInitialized { os_log("语音合成未初始化", log: log, type: .error) notifyEvent(eventType: .error, params: [ @@ -122,6 +127,10 @@ public class AzureTtsHelper: NSObject, ITtsService { return false } + if (self.sessionid != sessionid){ + return false + } + // 清理文本 let cleanedText = cleanTextForTTS(text) if cleanedText.isEmpty { @@ -130,7 +139,7 @@ public class AzureTtsHelper: NSObject, ITtsService { // 异步处理 enqueueSynthesisTask { - self.performSynthesis(text: cleanedText) + self.performSynthesis(sessionid: sessionid, text: cleanedText) } return true @@ -139,7 +148,7 @@ public class AzureTtsHelper: NSObject, ITtsService { /** * 流式合成文本 */ - public func speakStream(_ text: String) -> Bool { + public func speakStream(sessionid sessionid: String,_ text: String) -> Bool { if !isInitialized { notifyEvent(eventType: .error, params: [ "errorCode": "NOT_INITIALIZED", @@ -147,6 +156,10 @@ public class AzureTtsHelper: NSObject, ITtsService { ]) return false } + if (self.sessionid != sessionid){ + return false + } + self.sessionid = sessionid if text.isEmpty { return true @@ -184,7 +197,7 @@ public class AzureTtsHelper: NSObject, ITtsService { streamBuffer = String(currentText[startIndex...]) if !textToSpeak.isEmpty { - return speakOnce(textToSpeak) + return speakOnce(sessionid: sessionid, textToSpeak) } } @@ -194,7 +207,7 @@ public class AzureTtsHelper: NSObject, ITtsService { /** * 刷新并播放流式文本 */ - public func flushStream() -> Bool { + public func flushStream(sessionid sessionid: String) -> Bool { if !isInitialized { notifyEvent(eventType: .error, params: [ "errorCode": "NOT_INITIALIZED", @@ -210,7 +223,7 @@ public class AzureTtsHelper: NSObject, ITtsService { return true } - return speakOnce(remainingText) + return speakOnce(sessionid:sessionid,remainingText) } /** @@ -218,22 +231,31 @@ public class AzureTtsHelper: NSObject, ITtsService { */ public func stop() -> Bool { speaking = false + sessionid = "" streamBuffer = "" - + lastSpokenText = "" taskLock.lock() pendingTasks.removeAll() taskLock.unlock() + do { + print("liwei-------------停止语音合成播放") + try self.synthesizer?.stopSpeaking() + self.notifyEvent(eventType: .synthesisCanceled) + } catch { + os_log("停止语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription) + self.notifyEvent(eventType: .error, params: [ + "errorCode": "STOP_FAILED", + "errorMessage": error.localizedDescription + ]) + } + synthesisQueue.async { do { - try self.synthesizer?.stopSpeaking() - self.notifyEvent(eventType: .synthesisCanceled) - } catch { - os_log("停止语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription) - self.notifyEvent(eventType: .error, params: [ - "errorCode": "STOP_FAILED", - "errorMessage": error.localizedDescription - ]) + self.synthesizer = nil + try self.recreateSynthesizer() + }catch { + os_log("重置语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription) } } @@ -314,7 +336,7 @@ public class AzureTtsHelper: NSObject, ITtsService { // 如果之前正在播放,恢复播放 if wasSpeaking, let lastText = lastSpokenText { - speakOnce(lastText) + speakOnce(sessionid: "",lastText) } return true @@ -386,13 +408,19 @@ public class AzureTtsHelper: NSObject, ITtsService { /** * 执行语音合成 */ - private func performSynthesis(text: String) { + private func performSynthesis(sessionid:String,text: String) { + if (self.sessionid != sessionid) { + return + } speaking = true - lastSpokenText = text +// lastSpokenText = text // 生成SSML let ssml = generateOptimizedSsml(text) do { + if (self.sessionid != sessionid) { + return + } os_log("开始合成: %{public}@", log: log, type: .debug, ssml) let result = try synthesizer?.startSpeakingSsml(ssml) 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 a922f92f0..db83bd553 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 @@ -882,6 +882,7 @@ import os.log * 推送外部音频数据 */ public func pushAudioData(audioData: Data) { + audioProcessor?.saveAudioDataTo(data:audioData) } @@ -968,4 +969,4 @@ func withTimeout(_ timeout: TimeInterval, operation: @escaping () async throw group.cancelAll() return result } -} \ No newline at end of file +} 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 f7e6bced6..f5cc617a0 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 @@ -70,8 +70,11 @@ public class SimpleAudioReceiver: NSObject { print("音频录制组件已初始化,跳过重复初始化") return } + if audioSession.recordPermission == .granted { //判断权限成功 预热麦克风 + preConfigureAudioSession() + } - print("初始化了") + // 预配置音频会话 audioFormat = getOptimalAudioFormat() pushAudioStream = SPXPushAudioInputStream() @@ -84,6 +87,22 @@ public class SimpleAudioReceiver: NSObject { startWriteThread() } + // 【新增】预配置音频会话方法 + /** + * 预配置音频会话,在应用启动时调用以减少首次录音延迟 + * 【优化重点】将耗时的音频会话配置提前到初始化阶段 + */ + private func preConfigureAudioSession() { + do { + // 设置音频会话类别和模式 + runMicrophoneCapture() + stopMicrophoneCapture() + print("音频会话预配置完成") + } catch { + print("音频会话预配置失败: \(error)") + } + } + /// 获取最佳音频格式 (iOS 通常支持标准采样率) public func getOptimalAudioFormat() -> AVAudioFormat? { let sampleRate: Double = 16000 // iOS 通常支持 16kHz @@ -206,11 +225,19 @@ public class SimpleAudioReceiver: NSObject { */ private func runMicrophoneCapture() { do { + + print("liwei------------启动麦克风权限") + // 【新增】首先配置音频会话 try audioSession.setCategory( .playAndRecord, - mode: .videoChat, - options: [.defaultToSpeaker,.allowBluetooth,.mixWithOthers] + mode: .default, + options: [ + .defaultToSpeaker, + .allowBluetooth, + .mixWithOthers, + .duckOthers + ] ) // 【新增】请求麦克风权限(如果尚未授权) @@ -448,7 +475,7 @@ public func restoreOriginalAudioState() { _isWriting=false audioEngine?.pause() - print("stopMicrophoneCapture") + print("stopMicrophoneCapture") } public func releaseAudioResources() { @@ -470,6 +497,7 @@ public func restoreOriginalAudioState() { case receiver case bluetooth } + public func setAudioOutputRoute(_ route: AudioOutputRoute) { do { 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 bae9549ef..cb8d6d962 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 @@ -285,7 +285,7 @@ object BleService { * @param timeout 扫描超时时间(毫秒) * @return 是否成功开始扫描 */ - fun startScan(deviceName: String, timeout: Long = 15000): Boolean { + fun startScan(uuids: List, timeout: Long = 15000): Boolean { // Log.i(TAG, "liwei---------startScan 1 $deviceName") if (!isInitialized) { Log.e(TAG, "BLE服务未初始化") @@ -318,12 +318,16 @@ object BleService { // .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) // 使用ALL_MATCHES获取所有结果 // .setReportDelay(0) // 立即报告结果 // .build() - val filter = ScanFilter.Builder() - // 建议用 Service UUID 或 Device Name 替代 Manufacturer Data - // .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID)) -// .setDeviceName(deviceName) // 更通用、更安全 - .build() - +// val filter = ScanFilter.Builder() +// // 建议用 Service UUID 或 Device Name 替代 Manufacturer Data +// // .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID)) +//// .setDeviceName(deviceName) // 更通用、更安全 +// .build() + val filters = uuids.map { uuidStr -> + ScanFilter.Builder() + .setServiceUuid(ParcelUuid(UUID.fromString(uuidStr))) + .build() + } val settings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 推荐:不会触发权限问题 @@ -331,7 +335,7 @@ object BleService { .build() // Log.i(TAG, "liwei---------startScan 7") try { - scanner.startScan(listOf(filter), settings, scanCallback) + scanner.startScan(filters, settings, scanCallback) isScanning = true // Log.i(TAG, "liwei---------startScan 8") // 设置扫描超时 diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index 7e5a21ffd..1331ab839 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -293,10 +293,10 @@ class BleService { } /// 断开连接 - Future startScan(String devicename) async { + Future startScan(List uuids) async { try { final result = await _methodChannel - .invokeMethod('startScan', {'devicename': devicename}); + .invokeMethod('startScan', {'uuids': uuids}); return result ?? false; } catch (e) { print('开启扫描ble设备失败: $e'); diff --git a/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift b/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift index 72ada9274..b3735eb68 100644 --- a/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift +++ b/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift @@ -92,7 +92,11 @@ import Foundation * @param text 要合成的文本 * @return 是否成功开始合成 */ - func speakOnce(_ text: String) -> Bool + func speakOnce(sessionid sessionid: String,_ text: String) -> Bool + + + //开始播报 + func startspeak(sessionid sessionid: String)-> Bool /** * 流式合成 @@ -101,14 +105,14 @@ import Foundation * @param text 要合成的文本片段 * @return 是否成功处理 */ - func speakStream(_ text: String) -> Bool + func speakStream(sessionid sessionid: String,_ text: String) -> Bool /** * 刷新并播放流式文本缓冲区中的剩余内容 * * @return 是否成功处理 */ - func flushStream() -> Bool + func flushStream(sessionid sessionid: String) -> Bool /** * 停止当前合成和播放