Browse Source

合并代码,优化按住说话,去处带耳机选择了hfp链路导致耳机出问题

weicu
fdp 1 year ago
parent
commit
62d32ea392
  1. 4
      android/app/src/main/AndroidManifest.xml
  2. 5
      lib/data/models/appconfig.dart
  3. 10
      lib/data/services/ble_manager.dart
  4. 31
      lib/data/services/speech_impl/azure_tts_service.dart
  5. 27
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  6. 27
      lib/data/services/speech_impl/volcano_tts_service.dart
  7. 9
      lib/data/services/tts_service.dart
  8. 4
      lib/data/services/version_update_service.dart
  9. 4
      lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart
  10. 29
      lib/modules/agent/controllers/agent_controller.dart
  11. 2
      lib/modules/agent/views/agent_view.dart
  12. 30
      lib/modules/agent/views/message_bubble.dart
  13. 20
      lib/modules/home/controllers/home_controller.dart
  14. 5
      lib/modules/pairing/controllers/pairing_controller.dart
  15. 8
      lib/modules/speech_test/controllers/speech_test_controller.dart
  16. 17
      lib/modules/translation/controllers/translation_controller.dart
  17. 12
      lib/modules/translation/views/translation_view.dart
  18. 30
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  19. 3
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  20. 27
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  21. 3
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  22. 16
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  23. 64
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  24. 3
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift
  25. 6
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift
  26. 1
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  27. 20
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  28. 4
      local_plugins/ble_service/lib/ble_service.dart
  29. 10
      local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift
  30. 2
      pubspec.yaml

4
android/app/src/main/AndroidManifest.xml

@ -29,9 +29,9 @@
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" <!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> --> tools:ignore="ScopedStorage" /> -->
<!-- Android 13+ 媒体权限 --> <!-- Android 13+ 媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 --> <!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

5
lib/data/models/appconfig.dart

@ -42,6 +42,11 @@ class AppConfig {
return names; return names;
} }
static List<String> allproductUuids() {
List<String> uuids = config.products.map((user) => user.scanuuid).toList();
return uuids;
}
static DBProduct? getproduct(String name) { static DBProduct? getproduct(String name) {
final foundProduct = final foundProduct =
config.products.firstWhere((product) => product.devicename == name); config.products.firstWhere((product) => product.devicename == name);

10
lib/data/services/ble_manager.dart

@ -532,7 +532,7 @@ class BleManager extends GetxService {
try { try {
// 从.env文件直接读取环境变量 // 从.env文件直接读取环境变量
var serverurl = dotenv.env['SERVER_URL'] ?? ''; var serverurl = dotenv.env['SERVER_URL'] ?? '';
if (Get.locale?.countryCode != 'CN') { if (Get.deviceLocale?.countryCode != 'CN') {
serverurl = dotenv.env['HW_SERVER_URL'] ?? ''; serverurl = dotenv.env['HW_SERVER_URL'] ?? '';
} }
@ -833,9 +833,9 @@ class BleManager extends GetxService {
} }
/// 验证设备是否可以配对 /// 验证设备是否可以配对
Future<void> startScan(String devicename) async { Future<void> startScan(List<String> uuids) async {
try { try {
await _bleService.startScan(devicename); await _bleService.startScan(uuids);
} catch (e) { } catch (e) {
Logger.error('开始扫描设备: ${e.toString()}'); Logger.error('开始扫描设备: ${e.toString()}');
} }
@ -860,9 +860,9 @@ class BleManager extends GetxService {
} }
} }
Future<List<Map<String, String>>?> getDeviceList(String deviceName) async { Future<List<Map<String, String>>?> getDeviceList(String devicename) async {
try { try {
final result = _bleService.getDeviceList(deviceName); final result = _bleService.getDeviceList(devicename);
return result; return result;
} catch (e) { } catch (e) {
Logger.error('获取设备信息: ${e.toString()}'); Logger.error('获取设备信息: ${e.toString()}');

31
lib/data/services/speech_impl/azure_tts_service.dart

@ -37,7 +37,7 @@ class AzureTtsService extends GetxService implements TtsService {
// 可观察状态 // 可观察状态
final isEnabled = true.obs; final isEnabled = true.obs;
final _isSpeaking = false.obs; final _isSpeaking = false.obs;
String _currsessionid = '';
// 流式文本缓冲区 // 流式文本缓冲区
String _streamBuffer = ''; String _streamBuffer = '';
@ -183,13 +183,32 @@ class AzureTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> speakOnce(String text) async { Future<bool> 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<bool> speakOnce(String sessionid, String text) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
// 开始合成,传递参数 // 开始合成,传递参数
final result = await _channel.invokeMethod('speakText', { final result = await _channel.invokeMethod('speakText', {
'sessionid': sessionid,
'text': text, 'text': text,
}); });
@ -201,7 +220,7 @@ class AzureTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> speakStream(String text) async { Future<bool> speakStream(String sessionid, String text) async {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
@ -256,12 +275,12 @@ class AzureTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> flushStream() async { Future<bool> flushStream(String sessionid) async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try { try {
// 处理缓冲区中可能的完整句子 // 处理缓冲区中可能的完整句子
await speakStream(''); await speakStream(sessionid, '');
// 如果缓冲区仍有剩余文本,将其添加到播放队列 // 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
@ -345,7 +364,7 @@ class AzureTtsService extends GetxService implements TtsService {
final text = _textQueue.first; final text = _textQueue.first;
// 开始合成 // 开始合成
final success = await speakOnce(text); final success = await speakOnce(_currsessionid, text);
// 仅当合成操作失败时才从队列移除并继续处理下一项 // 仅当合成操作失败时才从队列移除并继续处理下一项
// 成功的话会等待合成完成事件后自动处理下一项 // 成功的话会等待合成完成事件后自动处理下一项

27
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -27,6 +27,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
late ConcatenatingAudioSource _playlist; late ConcatenatingAudioSource _playlist;
// 当前语音 // 当前语音
String _currsessionid = '';
final RxString _currentVoice = ''.obs; final RxString _currentVoice = ''.obs;
@override @override
@ -215,7 +216,21 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> speakOnce(String text) async { Future<bool> 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<bool> speakOnce(String sessionid, String text) async {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
@ -236,7 +251,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> speakStream(String text) async { Future<bool> speakStream(String sessionid, String text) async {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
@ -295,12 +310,14 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> flushStream() async { Future<bool> flushStream(
String sessionid,
) async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try { try {
// 处理缓冲区中可能的完整句子 // 处理缓冲区中可能的完整句子
await speakStream(''); await speakStream(sessionid, '');
// 如果缓冲区仍有剩余文本,将其添加到播放队列 // 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
@ -344,7 +361,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
final item = _textQueue.removeAt(0); final item = _textQueue.removeAt(0);
// 使用speakOnce播放当前项 // 使用speakOnce播放当前项
await speakOnce(item); await speakOnce(_currsessionid, item);
} }
} catch (e) { } catch (e) {
Logger.error('处理语音队列出错: ${e.toString()}'); Logger.error('处理语音队列出错: ${e.toString()}');

27
lib/data/services/speech_impl/volcano_tts_service.dart

@ -206,7 +206,26 @@ class VolcanoTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> speakOnce(String text) async { Future<bool> startspeak(String sessionid) async {
if (!await _ensureInitialized()) return false;
try {
// 单次播放
final result = await _channel.invokeMethod<bool>('startspeak', {
'sessionid': sessionid,
}) ??
false;
return result;
} catch (e) {
debugPrint('单次播放失败:$e');
_eventStreamController.add(TtsEvent.error('单次播放失败:$e'));
return false;
}
}
@override
Future<bool> speakOnce(String sessionid, String text) async {
if (!await _ensureInitialized()) return false; if (!await _ensureInitialized()) return false;
try { try {
@ -225,7 +244,7 @@ class VolcanoTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> speakStream(String text) async { Future<bool> speakStream(String sessionid, String text) async {
if (!await _ensureInitialized()) return false; if (!await _ensureInitialized()) return false;
try { try {
@ -244,7 +263,9 @@ class VolcanoTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> flushStream() async { Future<bool> flushStream(
String sessionid,
) async {
if (!await _ensureInitialized()) return false; if (!await _ensureInitialized()) return false;
try { try {

9
lib/data/services/tts_service.dart

@ -19,14 +19,17 @@ abstract class TtsService {
/// 设置语音 /// 设置语音
Future<bool> setVoice(String voiceName); Future<bool> setVoice(String voiceName);
///开始新的播放会话
Future<bool> startspeak(String sessionid);
/// 单次播放文本,完成后返回 /// 单次播放文本,完成后返回
Future<bool> speakOnce(String text); Future<bool> speakOnce(String sessionid, String text);
/// 将文本加入播放队列,支持连续播放多段文本 /// 将文本加入播放队列,支持连续播放多段文本
Future<bool> speakStream(String text); Future<bool> speakStream(String sessionid, String text);
/// 播放队列中剩余文本 /// 播放队列中剩余文本
Future<bool> flushStream(); Future<bool> flushStream(String sessionid);
/// 停止播放 /// 停止播放
Future<bool> stop(); Future<bool> stop();

4
lib/data/services/version_update_service.dart

@ -30,7 +30,9 @@ class VersionUpdateService extends GetxService {
static final String? _countryCode = Get.deviceLocale?.countryCode; //国家地区编码 static final String? _countryCode = Get.deviceLocale?.countryCode; //国家地区编码
static final String? _languageCode = Get.deviceLocale?.languageCode; //语言编码 static final String? _languageCode = Get.deviceLocale?.languageCode; //语言编码
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
static final String? _SERVER_URL = dotenv.env['SERVER_URL']; //服务器地址 static final String? _SERVER_URL = (_countryCode == 'CN')
? dotenv.env['SERVER_URL']
: dotenv.env['HW_SERVER_URL']; //服务器地址
// 添加 getter 方法来访问缓存的静态数据 // 添加 getter 方法来访问缓存的静态数据
/// 获取缓存的应用版本 /// 获取缓存的应用版本

4
lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart

@ -42,7 +42,7 @@ class FTFTranslationController extends GetxController
final sourceLanguageCode = 'zh-CN'.obs; final sourceLanguageCode = 'zh-CN'.obs;
final targetLanguageCode = 'en-US'.obs; final targetLanguageCode = 'en-US'.obs;
final isTtsEnabled = true.obs; final isTtsEnabled = true.obs;
var _currsessionid = '';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -168,7 +168,7 @@ class FTFTranslationController extends GetxController
'en-US-AriaNeural'; 'en-US-AriaNeural';
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(_currsessionid, text);
} catch (e) { } catch (e) {
Logger.error('播放翻译失败: ${e.toString()}'); Logger.error('播放翻译失败: ${e.toString()}');
} }

29
lib/modules/agent/controllers/agent_controller.dart

@ -318,8 +318,8 @@ class AgentController extends GetxController {
// 更新分页状态 // 更新分页状态
currentPage.value = 1; currentPage.value = 1;
hasMoreMessages.value = result.totalPages > 1; hasMoreMessages.value = result.totalPages > 1;
print( // print(
'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}'); // 'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}');
Logger.i(TAG, Logger.i(TAG,
'成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}');
_logMessageDetails(historyMessages); _logMessageDetails(historyMessages);
@ -800,12 +800,14 @@ class AgentController extends GetxController {
["card_navigation"]["start"] as String; ["card_navigation"]["start"] as String;
final end = metaResult["maps_route_navigation"] final end = metaResult["maps_route_navigation"]
["card_navigation"]["end"] as String; ["card_navigation"]["end"] as String;
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); final items = metaResult['items'];
Future.delayed(Duration(seconds: 2), () { if (items != null) {
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); Future.delayed(Duration(seconds: 2), () {
// 2秒后执行的代码 // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
openNavigation(start, end, ""); // 2秒后执行的代码
}); openNavigation(start, end, "");
});
}
} }
} }
} }
@ -949,10 +951,6 @@ class AgentController extends GetxController {
final text = textController.text.trim(); final text = textController.text.trim();
if (text.isEmpty) return; if (text.isEmpty) return;
final message = Message(isUser: true, text: text);
messages.add(message);
textController.clear();
// 使用reverse:true时通常不需要手动滚动 // 使用reverse:true时通常不需要手动滚动
// 3. 调用AI服务处理 // 3. 调用AI服务处理
try { try {
@ -963,6 +961,9 @@ class AgentController extends GetxController {
// 根据TTS状态决定是否朗读 // 根据TTS状态决定是否朗读
await AgentService.processTextInput(text, await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value); speakResponse: isTtsEnabled.value);
final message = Message(isUser: true, text: text);
messages.add(message);
textController.clear();
} catch (e) { } catch (e) {
Logger.e(TAG, '发送文本消息失败: $e'); Logger.e(TAG, '发送文本消息失败: $e');
isProcessing.value = false; isProcessing.value = false;
@ -1597,7 +1598,7 @@ class AgentController extends GetxController {
isPushToTalkActive.value = true; isPushToTalkActive.value = true;
//await AgentService.recognizeCallback(); //await AgentService.recognizeCallback();
// 开始语音识别 // 开始语音识别
await AgentService.startConversation(mode: 'push_to_talk'); AgentService.startConversation(mode: 'push_to_talk');
// await startVoiceInput(); // await startVoiceInput();
} }
@ -1611,7 +1612,7 @@ class AgentController extends GetxController {
// 清除按住说话状态(但保持按住说话模式) // 清除按住说话状态(但保持按住说话模式)
isPushToTalkActive.value = false; isPushToTalkActive.value = false;
await AgentService.stopConversation(); AgentService.stopConversation();
// 停止语音识别 // 停止语音识别
// await stopVoiceInput(); // await stopVoiceInput();
} }

2
lib/modules/agent/views/agent_view.dart

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../core/widgets/intro_overlay_builder.dart'; import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import '../controllers/message_action_controller.dart'; import '../controllers/message_action_controller.dart';
@ -616,6 +617,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
_holdTimer = Timer( _holdTimer = Timer(
const Duration(milliseconds: 100), const Duration(milliseconds: 100),
() { () {
Haptics.vibrate(HapticsType.success);
controller.startPushToTalk(); controller.startPushToTalk();
// if (activeSpeaker == 0) { // if (activeSpeaker == 0) {
// if (speakerId == 1) { // if (speakerId == 1) {

30
lib/modules/agent/views/message_bubble.dart

@ -296,20 +296,22 @@ class MessageBubble extends GetView<AgentController> {
final start = card_navigation['start'] as String? ?? ''; final start = card_navigation['start'] as String? ?? '';
final end = card_navigation['end'] as String? ?? ''; final end = card_navigation['end'] as String? ?? '';
final destination = card_navigation['destination'] as String? ?? ''; final destination = card_navigation['destination'] as String? ?? '';
final rawList = card_navigation['items'] as List? ?? []; final rawList = card_navigation['items'] as List? ?? null;
final list = if (rawList != null) {
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); final list =
final rawNavigation = card_navigation['navigation']; rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final Map<String, dynamic>? navigation = final rawNavigation = card_navigation['navigation'];
rawNavigation is Map<String, dynamic> ? rawNavigation : null; final Map<String, dynamic>? navigation =
print("导航实时数据 ${rawNavigation}"); rawNavigation is Map<String, dynamic> ? rawNavigation : null;
// 导航 // print("导航实时数据 ${rawNavigation}");
return navigationCard(context, // 导航
start: start, return navigationCard(context,
end: end, start: start,
destination: destination, end: end,
modes: list, destination: destination,
navigation: navigation); modes: list,
navigation: navigation);
}
} }
} }

20
lib/modules/home/controllers/home_controller.dart

@ -1,4 +1,3 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import '../../../data/services/novice_guide_service.dart'; import '../../../data/services/novice_guide_service.dart';
@ -69,7 +68,7 @@ class HomeController extends GetxController {
super.onInit(); super.onInit();
_otaCheckService = Get.find<OtaCheckService>(); _otaCheckService = Get.find<OtaCheckService>();
_initWithCachedInfo(); _initWithCachedInfo();
_startBatteryRefreshTimer(); // 启动电池信息定时刷新 // _startBatteryRefreshTimer(); // 启动电池信息定时刷新
} }
@override @override
@ -158,12 +157,12 @@ class HomeController extends GetxController {
}); });
} }
// 根据连接状态控制定时器 // 根据连接状态控制定时器
if (isNowConnected) { // if (isNowConnected) {
_startBatteryRefreshTimer(); // 连接时启动定时器 // _startBatteryRefreshTimer(); // 连接时启动定时器
} else { // } else {
_stopBatteryRefreshTimer(); // 断开时停止定时器 // _stopBatteryRefreshTimer(); // 断开时停止定时器
// 注意:不再重置OTA检查状态,因为现在由全局服务管理 // // 注意:不再重置OTA检查状态,因为现在由全局服务管理
} // }
} }
update(); update();
@ -351,7 +350,8 @@ class HomeController extends GetxController {
Logger.d(_tag, 'OTA检查状态: $status'); Logger.d(_tag, 'OTA检查状态: $status');
// 使用全局OTA检查服务执行检查 // 使用全局OTA检查服务执行检查
final success = await _otaCheckService.performOtaCheck(_isFullyConnected.value); final success =
await _otaCheckService.performOtaCheck(_isFullyConnected.value);
if (success) { if (success) {
Logger.d(_tag, 'OTA检查已成功执行'); Logger.d(_tag, 'OTA检查已成功执行');
@ -365,6 +365,4 @@ class HomeController extends GetxController {
Map<String, dynamic> getOtaCheckStatus() { Map<String, dynamic> getOtaCheckStatus() {
return _otaCheckService.getCheckStatus(); return _otaCheckService.getCheckStatus();
} }
} }

5
lib/modules/pairing/controllers/pairing_controller.dart

@ -735,6 +735,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
} }
final currentBleState = await _bleManager.getConnectionState(); final currentBleState = await _bleManager.getConnectionState();
final names = AppConfig.allproductNames(); final names = AppConfig.allproductNames();
final uuids = AppConfig.allproductUuids();
// 检查是否已有绑定的设备 // 检查是否已有绑定的设备
print("device.address${device.address}"); print("device.address${device.address}");
if (User.instance.devices.isNotEmpty) { if (User.instance.devices.isNotEmpty) {
@ -791,13 +792,13 @@ class PairingController extends GetxController with WidgetsBindingObserver {
} }
} }
print( print(
'获取到 deviceName: $deviceName address: ${device.address} names:$names'); '获取到 deviceName: $deviceName address: ${device.address} uuids:$uuids');
if (names.contains(deviceName)) { if (names.contains(deviceName)) {
if (currentBleState == 2) { if (currentBleState == 2) {
//已连接直接返回 //已连接直接返回
return 9; return 9;
} }
await _bleManager.startScan(deviceName); await _bleManager.startScan(uuids);
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
// 尝试最多读取 3 次设备信息,每次间隔 1 秒 // 尝试最多读取 3 次设备信息,每次间隔 1 秒
List<Map<String, String>>? deviceinfos; List<Map<String, String>>? deviceinfos;

8
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -53,6 +53,8 @@ class SpeechTestController extends GetxController {
'这是第三句测试语音,将在连续模式下播放完毕。' '这是第三句测试语音,将在连续模式下播放完毕。'
]; ];
var _currsessionid = '';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -246,7 +248,7 @@ class SpeechTestController extends GetxController {
isProcessing.value = true; isProcessing.value = true;
ttsStatus.value = '准备播放...'; ttsStatus.value = '准备播放...';
final result = await _ttsService.speakOnce(text); final result = await _ttsService.speakOnce(_currsessionid, text);
if (!result) { if (!result) {
ttsStatus.value = '播放失败'; ttsStatus.value = '播放失败';
@ -269,7 +271,7 @@ class SpeechTestController extends GetxController {
// 为避免阻塞,使用一个句子接一个句子的方式添加到队列 // 为避免阻塞,使用一个句子接一个句子的方式添加到队列
for (final sentence in testSentences) { for (final sentence in testSentences) {
if (!await _ttsService.speakStream(sentence)) { if (!await _ttsService.speakStream(_currsessionid, sentence)) {
ttsStatus.value = '添加句子到队列失败'; ttsStatus.value = '添加句子到队列失败';
break; break;
} }
@ -278,7 +280,7 @@ class SpeechTestController extends GetxController {
} }
// 刷新队列,确保播放完成 // 刷新队列,确保播放完成
await _ttsService.flushStream(); await _ttsService.flushStream(_currsessionid);
} catch (e) { } catch (e) {
ttsStatus.value = '连续播放异常: $e'; ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: ${e.toString()}'); Logger.error('连续播放异常: ${e.toString()}');

17
lib/modules/translation/controllers/translation_controller.dart

@ -88,6 +88,7 @@ class TranslationController extends GetxController {
final targetLanguageCode = 'en-US'.obs; final targetLanguageCode = 'en-US'.obs;
var detectedLanguageCode = ''; var detectedLanguageCode = '';
var currsessionid = '';
final currentModeTitle = 'simultaneousTranslation'.obs; final currentModeTitle = 'simultaneousTranslation'.obs;
final currentMode = 'simultaneous'.obs; final currentMode = 'simultaneous'.obs;
// 翻译历史和当前项 // 翻译历史和当前项
@ -130,6 +131,8 @@ class TranslationController extends GetxController {
if (mode == 'call') { if (mode == 'call') {
await _initializeCallModeTranslationService(); await _initializeCallModeTranslationService();
} else if (mode == 'faceToFace') {
await _ttsService.setAudioOutputDevice(1);
} }
} }
@ -200,8 +203,8 @@ class TranslationController extends GetxController {
Future<void> _handleTranslationPlayback() async { Future<void> _handleTranslationPlayback() async {
if (fTFTranslationResult != '' && !isPlayback) { if (fTFTranslationResult != '' && !isPlayback) {
isPlayback = true; isPlayback = true;
await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); // await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
await Future.delayed(const Duration(milliseconds: 1300)); // await Future.delayed(const Duration(milliseconds: 1300));
await playTranslatedText(fTFTranslationResult!); await playTranslatedText(fTFTranslationResult!);
fTFTranslationResult = ''; fTFTranslationResult = '';
} else if ((translationHistory.isEmpty || } else if ((translationHistory.isEmpty ||
@ -913,8 +916,8 @@ class TranslationController extends GetxController {
if (activeSpeaker.value == 0 && !isPlayback) { if (activeSpeaker.value == 0 && !isPlayback) {
isPlayback = true; isPlayback = true;
await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); // await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
await Future.delayed(const Duration(milliseconds: 1300)); // await Future.delayed(const Duration(milliseconds: 1300));
await playTranslatedText(fTFTranslationResult!); await playTranslatedText(fTFTranslationResult!);
fTFTranslationResult = ''; fTFTranslationResult = '';
} }
@ -935,7 +938,7 @@ class TranslationController extends GetxController {
final startTime = DateTime.now(); final startTime = DateTime.now();
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(currsessionid, text);
final endTime = DateTime.now(); final endTime = DateTime.now();
// 记录TTS API调用统计 - 优化版(基于文本字符数计费) // 记录TTS API调用统计 - 优化版(基于文本字符数计费)
@ -991,7 +994,7 @@ class TranslationController extends GetxController {
_languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ?? _languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ??
'en-US-AriaNeural'; 'en-US-AriaNeural';
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(item.translatedText); await _ttsService.speakOnce(currsessionid, item.translatedText);
} catch (e) { } catch (e) {
Logger.error('播放翻译项失败: ${e.toString()}'); Logger.error('播放翻译项失败: ${e.toString()}');
} }
@ -1010,7 +1013,7 @@ class TranslationController extends GetxController {
'en-US-AriaNeural'; 'en-US-AriaNeural';
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(currsessionid, text);
} catch (e) { } catch (e) {
Logger.error('播放文本失败: ${e.toString()}'); Logger.error('播放文本失败: ${e.toString()}');
} }

12
lib/modules/translation/views/translation_view.dart

@ -1107,13 +1107,11 @@ class TranslationView extends GetView<TranslationController> {
'faceToFaceTranslation'.tr, 'faceToFaceTranslation'.tr,
Icons.people, Icons.people,
Colors.green, Colors.green,
onTap: controller.bluetoothManager.currentDevice != null onTap: () async {
? () async { Get.back();
Get.back(); await controller.changeTranslationMode('faceToFace');
await controller.changeTranslationMode('faceToFace'); await controller.startRecognition();
await controller.startRecognition(); },
}
: null,
), ),
// _buildModeItem( // _buildModeItem(
// context, // context,

30
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -378,7 +378,7 @@ class AgentServiceImpl: NSObject {
return false return false
} }
} }
stopTts();
if isRecognizing { if isRecognizing {
stopRecognition() stopRecognition()
} }
@ -396,7 +396,7 @@ class AgentServiceImpl: NSObject {
} }
print("ai启动语音=\(audioSourceType)=\(useBle)") print("ai启动语音=\(audioSourceType)=\(useBle)")
guard let success = azureAsrHelper?.startContinuousRecognition( guard let success = azureAsrHelper?.startContinuousRecognition(
// callback: self, // callback: self,
audioSourceType: audioSourceType audioSourceType: audioSourceType
), success else { ), success else {
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
@ -456,7 +456,7 @@ audioStream.saveAudioDataTo(data: audioData)
return true return true
} }
func speakText(_ text: String) -> Bool { func speakText(sessionid sessionid:String,_ text: String) -> Bool {
if !isInitialized { if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED") sendError("服务未初始化", code: "NOT_INITIALIZED")
return false return false
@ -466,7 +466,7 @@ audioStream.saveAudioDataTo(data: audioData)
return false return false
} }
return azureTtsHelper?.speakOnce(text) ?? false return azureTtsHelper?.speakOnce(sessionid:sessionid,text) ?? false
} }
func stopTts() -> Bool { func stopTts() -> Bool {
@ -591,6 +591,7 @@ audioStream.saveAudioDataTo(data: audioData)
imagePath: imagePath imagePath: imagePath
) )
currsessionId = UUID().uuidString currsessionId = UUID().uuidString
azureTtsHelper?.startspeak(sessionid: currsessionId)
os_log("设置回调并调用sendMessageStream", log: logger, type: .info) os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
chatApiService.setStreamCallback(currsessionId,callback) chatApiService.setStreamCallback(currsessionId,callback)
chatApiService.sendMessageStream(currsessionId,messages: messages) chatApiService.sendMessageStream(currsessionId,messages: messages)
@ -1253,7 +1254,7 @@ class ChatApiStreamCallback: StreamCallback {
responseBuilder += token responseBuilder += token
if speakResponse && reply && broadcast{ if speakResponse && reply && broadcast{
try agentService.azureTtsHelper?.speakStream(token) try agentService.azureTtsHelper?.speakStream(sessionid:sessionId,token)
// 在开始流式TTS时立即停止气泡音 // 在开始流式TTS时立即停止气泡音
} }
if (reply && token != "") { if (reply && token != "") {
@ -1275,7 +1276,7 @@ class ChatApiStreamCallback: StreamCallback {
if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{ if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{
agentService.azureTtsHelper?.flushStream() agentService.azureTtsHelper?.flushStream(sessionid:sessionId)
} }
let response = responseBuilder let response = responseBuilder
@ -1522,7 +1523,7 @@ class AudioPlayer {
stopAwaitSound() stopAwaitSound()
// 播放等待音效 // 播放等待音效
playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer) playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer,volume:0.2)
// 启动超时定时器 // 启动超时定时器
startAwaitTimeout() startAwaitTimeout()
@ -1563,7 +1564,7 @@ class AudioPlayer {
os_log("未找到音频文件: %{public}@.%{public}@ in bundle %{public}@", log: logger, type: .error, named, fileType, resourcesBundle.bundlePath) 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?.stop()
player = nil player = nil
@ -1571,6 +1572,10 @@ class AudioPlayer {
if let path = resourcesBundle.path(forResource: named, ofType: fileType) { if let path = resourcesBundle.path(forResource: named, ofType: fileType) {
let url = URL(fileURLWithPath: path) let url = URL(fileURLWithPath: path)
safePlayLoopAudio(url: url, player: &player) safePlayLoopAudio(url: url, player: &player)
// 在创建播放器之后设置音量
player?.volume = volume
return return
} }
@ -1707,7 +1712,10 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
hasSpeechDetected = false hasSpeechDetected = false
// 使用模式感知的空闲检测启动方法 // 使用模式感知的空闲检测启动方法
startIdleCheckForMode(mode: currentRecognitionMode) startIdleCheckForMode(mode: currentRecognitionMode)
audioPlayer?.playStartSound() if (currentRecognitionMode == "ble_wakeup") {
audioPlayer?.playStartSound()
}
} }
func onSessionStopped() { func onSessionStopped() {
@ -1715,7 +1723,9 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
isRecognizing = false isRecognizing = false
hasSpeechDetected = false hasSpeechDetected = false
stopIdleCheck() stopIdleCheck()
audioPlayer?.playStopSound() if (currentRecognitionMode == "ble_wakeup") {
audioPlayer?.playStopSound()
}
} }
func onCanceled(_ reason: String, _ errorDetails: String) { func onCanceled(_ reason: String, _ errorDetails: String) {

3
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -86,12 +86,13 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
case "speakText": case "speakText":
guard let arguments = call.arguments as? [String: Any], guard let arguments = call.arguments as? [String: Any],
let sessionid = arguments["sessionid"] as? String,
let text = arguments["text"] as? String else { let text = arguments["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return return
} }
let success = impl.speakText(text) let success = impl.speakText(sessionid:sessionid,text)
result(success) result(success)
case "stopTts": case "stopTts":

27
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt

@ -16,6 +16,7 @@ import android.media.AudioManager
import android.media.AudioFocusRequest import android.media.AudioFocusRequest
import android.media.AudioAttributes import android.media.AudioAttributes
import com.yunqiinnovation.azure_speech.tools.RecordFile import com.yunqiinnovation.azure_speech.tools.RecordFile
import android.os.Build // 新增:用于获取设备信息
/** /**
* 简单音频接收器类,用于处理音频录制和流传输 * 简单音频接收器类,用于处理音频录制和流传输
@ -232,13 +233,35 @@ class SimpleAudioReceiver(private val context: Context) {
} }
} }
/**
* 判断是否为荣耀手机
* @return true 如果是荣耀手机,false 否则
*/
private fun isHonorDevice(): Boolean {
val brand = Build.BRAND?.lowercase() ?: ""
val manufacturer = Build.MANUFACTURER?.lowercase() ?: ""
val model = Build.MODEL?.lowercase() ?: ""
// 检查品牌、制造商或型号中是否包含荣耀相关标识
return brand.contains("honor") ||
manufacturer.contains("honor") ||
model.contains("honor") ||
brand.contains("荣耀") ||
manufacturer.contains("荣耀")
}
/** /**
* 运行麦克风捕获 * 运行麦克风捕获
*/ */
private fun runMicrophoneCapture() { private fun runMicrophoneCapture() {
try { try {
// 首先设置通话音频模式 // 根据手机品牌决定是否设置通话音频模式
//setupCommunicationAudioMode() if (isHonorDevice()) {
Log.d(TAG, "检测到荣耀设备,启用通话音频模式")
setupCommunicationAudioMode()
} else {
Log.d(TAG, "非荣耀设备,跳过通话音频模式设置")
}
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
val sampleRate = supportedSampleRates.firstOrNull { rate -> val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat) val bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat)

3
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 { public func renameFile(filePath: String, newName: String)-> Bool {
audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call
return true return true
} }

16
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) os_log("音频流未初始化", type: .error)
return return
} }
print("liwei----------外部音频推送:\(audioBytes.data.count)")
audioStream.saveAudioDataTo(data: audioBytes.data) audioStream.saveAudioDataTo(data: audioBytes.data)
@ -481,26 +482,33 @@ private func sendAstEvent(_ event: [String: Any]) {
case "speakText": case "speakText":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let sessionid = args["sessionid"] as? String,
let text = args["text"] as? String else { let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return return
} }
let success = azureTtsHelper.speakOnce(text) let success = azureTtsHelper.speakOnce(sessionid:sessionid,text)
result(success) result(success)
case "speakStream": case "speakStream":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let sessionid = args["sessionid"] as? String,
let text = args["text"] as? String else { let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return return
} }
let success = azureTtsHelper.speakStream(text) let success = azureTtsHelper.speakStream(sessionid:sessionid,text)
result(success) result(success)
case "flushStream": 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) result(success)
case "stopSpeaking": case "stopSpeaking":
@ -893,6 +901,7 @@ extension AzureSpeechPlugin: BleService.Callback {
os_log("音频流未初始化", type: .error) os_log("音频流未初始化", type: .error)
return return
} }
print("liwei--------------接收到音频数据回调")
audioStream.saveAudioDataTo(data: data) audioStream.saveAudioDataTo(data: data)
} }
@ -904,6 +913,7 @@ extension AzureSpeechPlugin: BleService.Callback {
os_log("音频流未初始化", type: .error) os_log("音频流未初始化", type: .error)
return return
} }
print("liwei--------------接收到音频数据回调1")
audioStream.saveAudioDataTo(data: data) audioStream.saveAudioDataTo(data: data)
} }

64
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 synthesizer: SPXSpeechSynthesizer?
private var isInitialized = false private var isInitialized = false
private var speaking = false private var speaking = false
private var sessionid = "" //语音播报任务idsetAudioOutputRoute
// 当前配置 // 当前配置
private var currentVoice = DEFAULT_VOICE 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 { if !isInitialized {
os_log("语音合成未初始化", log: log, type: .error) os_log("语音合成未初始化", log: log, type: .error)
notifyEvent(eventType: .error, params: [ notifyEvent(eventType: .error, params: [
@ -122,6 +127,10 @@ public class AzureTtsHelper: NSObject, ITtsService {
return false return false
} }
if (self.sessionid != sessionid){
return false
}
// 清理文本 // 清理文本
let cleanedText = cleanTextForTTS(text) let cleanedText = cleanTextForTTS(text)
if cleanedText.isEmpty { if cleanedText.isEmpty {
@ -130,7 +139,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
// 异步处理 // 异步处理
enqueueSynthesisTask { enqueueSynthesisTask {
self.performSynthesis(text: cleanedText) self.performSynthesis(sessionid: sessionid, text: cleanedText)
} }
return true 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 { if !isInitialized {
notifyEvent(eventType: .error, params: [ notifyEvent(eventType: .error, params: [
"errorCode": "NOT_INITIALIZED", "errorCode": "NOT_INITIALIZED",
@ -147,6 +156,10 @@ public class AzureTtsHelper: NSObject, ITtsService {
]) ])
return false return false
} }
if (self.sessionid != sessionid){
return false
}
self.sessionid = sessionid
if text.isEmpty { if text.isEmpty {
return true return true
@ -184,7 +197,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
streamBuffer = String(currentText[startIndex...]) streamBuffer = String(currentText[startIndex...])
if !textToSpeak.isEmpty { 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 { if !isInitialized {
notifyEvent(eventType: .error, params: [ notifyEvent(eventType: .error, params: [
"errorCode": "NOT_INITIALIZED", "errorCode": "NOT_INITIALIZED",
@ -210,7 +223,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
return true return true
} }
return speakOnce(remainingText) return speakOnce(sessionid:sessionid,remainingText)
} }
/** /**
@ -218,22 +231,31 @@ public class AzureTtsHelper: NSObject, ITtsService {
*/ */
public func stop() -> Bool { public func stop() -> Bool {
speaking = false speaking = false
sessionid = ""
streamBuffer = "" streamBuffer = ""
lastSpokenText = ""
taskLock.lock() taskLock.lock()
pendingTasks.removeAll() pendingTasks.removeAll()
taskLock.unlock() 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 { synthesisQueue.async {
do { do {
try self.synthesizer?.stopSpeaking() self.synthesizer = nil
self.notifyEvent(eventType: .synthesisCanceled) try self.recreateSynthesizer()
} catch { }catch {
os_log("停止语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription) os_log("重置语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
self.notifyEvent(eventType: .error, params: [
"errorCode": "STOP_FAILED",
"errorMessage": error.localizedDescription
])
} }
} }
@ -314,7 +336,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
// 如果之前正在播放,恢复播放 // 如果之前正在播放,恢复播放
if wasSpeaking, let lastText = lastSpokenText { if wasSpeaking, let lastText = lastSpokenText {
speakOnce(lastText) speakOnce(sessionid: "",lastText)
} }
return true 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 speaking = true
lastSpokenText = text // lastSpokenText = text
// 生成SSML // 生成SSML
let ssml = generateOptimizedSsml(text) let ssml = generateOptimizedSsml(text)
do { do {
if (self.sessionid != sessionid) {
return
}
os_log("开始合成: %{public}@", log: log, type: .debug, ssml) os_log("开始合成: %{public}@", log: log, type: .debug, ssml)
let result = try synthesizer?.startSpeakingSsml(ssml) let result = try synthesizer?.startSpeakingSsml(ssml)

3
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift

@ -882,6 +882,7 @@ import os.log
* 推送外部音频数据 * 推送外部音频数据
*/ */
public func pushAudioData(audioData: Data) { public func pushAudioData(audioData: Data) {
audioProcessor?.saveAudioDataTo(data:audioData) audioProcessor?.saveAudioDataTo(data:audioData)
} }
@ -968,4 +969,4 @@ func withTimeout<T>(_ timeout: TimeInterval, operation: @escaping () async throw
group.cancelAll() group.cancelAll()
return result return result
} }
} }

6
local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift

@ -51,10 +51,10 @@ class MicrophoneCapture: NSObject {
// 设置音频会话参数 // 设置音频会话参数
try audioSession.setCategory(.playAndRecord, try audioSession.setCategory(.playAndRecord,
mode: .videoChat, mode: .videoChat,
options: [.allowBluetooth, options: [//.allowBluetooth,
// .allowAirPlay, // .allowAirPlay,
// .allowBluetoothA2DP, .allowBluetoothA2DP,
//.mixWithOthers, .mixWithOthers,
.defaultToSpeaker .defaultToSpeaker
]) ])

1
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -332,6 +332,7 @@ public func restoreOriginalAudioState() {
case receiver case receiver
case bluetooth case bluetooth
} }
public func setAudioOutputRoute(_ route: AudioOutputRoute) { public func setAudioOutputRoute(_ route: AudioOutputRoute) {
// do { // do {

20
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -285,7 +285,7 @@ object BleService {
* @param timeout 扫描超时时间(毫秒) * @param timeout 扫描超时时间(毫秒)
* @return 是否成功开始扫描 * @return 是否成功开始扫描
*/ */
fun startScan(deviceName: String, timeout: Long = 15000): Boolean { fun startScan(uuids: List<String>, timeout: Long = 15000): Boolean {
// Log.i(TAG, "liwei---------startScan 1 $deviceName") // Log.i(TAG, "liwei---------startScan 1 $deviceName")
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "BLE服务未初始化") Log.e(TAG, "BLE服务未初始化")
@ -318,12 +318,16 @@ object BleService {
// .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) // 使用ALL_MATCHES获取所有结果 // .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) // 使用ALL_MATCHES获取所有结果
// .setReportDelay(0) // 立即报告结果 // .setReportDelay(0) // 立即报告结果
// .build() // .build()
val filter = ScanFilter.Builder() // val filter = ScanFilter.Builder()
// 建议用 Service UUID 或 Device Name 替代 Manufacturer Data // // 建议用 Service UUID 或 Device Name 替代 Manufacturer Data
// .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID)) // // .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID))
// .setDeviceName(deviceName) // 更通用、更安全 //// .setDeviceName(deviceName) // 更通用、更安全
.build() // .build()
val filters = uuids.map { uuidStr ->
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(UUID.fromString(uuidStr)))
.build()
}
val settings = ScanSettings.Builder() val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 推荐:不会触发权限问题 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 推荐:不会触发权限问题
@ -331,7 +335,7 @@ object BleService {
.build() .build()
// Log.i(TAG, "liwei---------startScan 7") // Log.i(TAG, "liwei---------startScan 7")
try { try {
scanner.startScan(listOf(filter), settings, scanCallback) scanner.startScan(filters, settings, scanCallback)
isScanning = true isScanning = true
// Log.i(TAG, "liwei---------startScan 8") // Log.i(TAG, "liwei---------startScan 8")
// 设置扫描超时 // 设置扫描超时

4
local_plugins/ble_service/lib/ble_service.dart

@ -293,10 +293,10 @@ class BleService {
} }
/// 断开连接 /// 断开连接
Future<bool> startScan(String devicename) async { Future<bool> startScan(List<String> uuids) async {
try { try {
final result = await _methodChannel final result = await _methodChannel
.invokeMethod<bool>('startScan', {'devicename': devicename}); .invokeMethod<bool>('startScan', {'uuids': uuids});
return result ?? false; return result ?? false;
} catch (e) { } catch (e) {
print('开启扫描ble设备失败: $e'); print('开启扫描ble设备失败: $e');

10
local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift

@ -92,7 +92,11 @@ import Foundation
* @param text 要合成的文本 * @param text 要合成的文本
* @return 是否成功开始合成 * @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 要合成的文本片段 * @param text 要合成的文本片段
* @return 是否成功处理 * @return 是否成功处理
*/ */
func speakStream(_ text: String) -> Bool func speakStream(sessionid sessionid: String,_ text: String) -> Bool
/** /**
* 刷新并播放流式文本缓冲区中的剩余内容 * 刷新并播放流式文本缓冲区中的剩余内容
* *
* @return 是否成功处理 * @return 是否成功处理
*/ */
func flushStream() -> Bool func flushStream(sessionid sessionid: String) -> Bool
/** /**
* 停止当前合成和播放 * 停止当前合成和播放

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans name: voitrans
description: "Voitrans - AI Voice Assistant." description: "Voitrans - AI Voice Assistant."
publish_to: "none" publish_to: "none"
version: 1.0.10+20 version: 1.0.10+25
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save