From bc5c7bdeeb227fe27b9c01657447a5f1612d2788 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sat, 28 Jun 2025 13:17:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BF=BB=E8=AF=91=E9=A2=91?= =?UTF-8?q?=E7=B9=81=E5=BC=80=E5=90=AF=E5=85=B3=E9=97=AD=E5=8D=A1=E6=AD=BB?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E6=96=B0=E5=8A=A0=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E7=AB=AF=E6=9D=A5=E5=9B=9E=E5=88=87=E6=8D=A2?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E9=9D=A2=E5=AF=B9=E9=9D=A2=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=BD=95=E9=9F=B3=E7=95=8C?= =?UTF-8?q?=E9=9D=A2bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/asr_service.dart | 7 + .../speech_impl/azure_asr_service.dart | 50 ++ .../speech_impl/volcano_asr_api_service.dart | 24 + .../speech_impl/volcano_asr_service.dart | 24 + .../speech_impl/xunfei_asr_service.dart | 24 + lib/modules/home/views/home_view.dart | 105 ++- .../meeting_record_controller.dart | 24 +- lib/modules/settings/views/settings_view.dart | 44 +- .../controllers/translation_controller.dart | 109 ++- .../translation/views/translation_view.dart | 231 ++++++- .../azure_speech/AzureAsrHelper.kt | 618 ++++++++++++------ .../azure_speech/AzureSpeechPlugin.kt | 46 +- .../azure_speech/AzureTtsHelper.kt | 305 +++++---- 13 files changed, 1104 insertions(+), 507 deletions(-) diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index a9a9e6b52..70f88353b 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -26,6 +26,13 @@ abstract class AsrService { /// 停止连续语音识别 Future stopContinuousRecognition(); + Future setPause(); + + Future setcontinue(); + Future restoreOriginalAudioState(); + + Future disableBluetoothAudio(); + /// 检查连续识别是否活跃 bool isContinuousRecognitionActive(); diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 39ac44aa9..cc5ae9746 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -414,4 +414,54 @@ class AzureAsrService extends GetxService implements AsrService { rethrow; } } + + @override + Future setPause() async { + // TODO: implement setPause + try { + final bool result = await _channel.invokeMethod('setPause'); + + return; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future setcontinue() async { + // TODO: implement setcontinue + try { + final bool result = await _channel.invokeMethod('setcontinue'); + return; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future disableBluetoothAudio() async { + // TODO: implement disableBluetoothAudio + try { + final bool result = await _channel.invokeMethod('disableBluetoothAudio'); + return; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future restoreOriginalAudioState() async { + // TODO: implement restoreOriginalAudioState + try { + final bool result = + await _channel.invokeMethod('restoreOriginalAudioState'); + return; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } } diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index a15a4ae0b..3fbaa53ef 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -841,4 +841,28 @@ class VolcanoAsrApiService implements AsrService { // TODO: implement renameFile throw UnimplementedError(); } + + @override + Future setPause() { + // TODO: implement setPause + throw UnimplementedError(); + } + + @override + Future setcontinue() { + // TODO: implement setcontinue + throw UnimplementedError(); + } + + @override + Future disableBluetoothAudio() { + // TODO: implement disableBluetoothAudio + throw UnimplementedError(); + } + + @override + Future restoreOriginalAudioState() { + // TODO: implement restoreOriginalAudioState + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index 8571f9c4c..a7b3f541d 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -376,4 +376,28 @@ class VolcanoAsrService extends GetxService implements AsrService { // TODO: implement renameFile throw UnimplementedError(); } + + @override + Future setPause() { + // TODO: implement setPause + throw UnimplementedError(); + } + + @override + Future setcontinue() { + // TODO: implement setcontinue + throw UnimplementedError(); + } + + @override + Future disableBluetoothAudio() { + // TODO: implement disableBluetoothAudio + throw UnimplementedError(); + } + + @override + Future restoreOriginalAudioState() { + // TODO: implement restoreOriginalAudioState + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index f4d77ae50..6b5450761 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -300,4 +300,28 @@ class XunfeiAsrService extends GetxService implements AsrService { // TODO: implement renameFile throw UnimplementedError(); } + + @override + Future setPause() { + // TODO: implement setPause + throw UnimplementedError(); + } + + @override + Future setcontinue() { + // TODO: implement setcontinue + throw UnimplementedError(); + } + + @override + Future disableBluetoothAudio() { + // TODO: implement disableBluetoothAudio + throw UnimplementedError(); + } + + @override + Future restoreOriginalAudioState() { + // TODO: implement restoreOriginalAudioState + throw UnimplementedError(); + } } diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index d100d1a6c..f447a9839 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -209,44 +209,44 @@ class HomeView extends GetView { return Container( key: ValueKey('device_status_$isConnected'), decoration: BoxDecoration( - color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(isDarkMode ? 0.05 : 0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - padding: EdgeInsets.all(16.w), - child: Column( - children: [ - // 状态标题行 - _buildDeviceStatusHeader(isDarkMode), - - // 设备信息 - SizedBox(height: 12.h), - Obx(() { - // 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断 - // 使用响应式访问确保UI更新 - return controller.isFullyConnectedRx.value - ? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息 - : _buildDisconnectedInfo(isDarkMode); //未连接提示 - }), - - // 连接按钮(仅在未连接时显示) - SizedBox(height: 12.h), - Obx(() { - // 使用响应式访问确保UI更新 - if (!controller.isFullyConnectedRx.value) { - return _buildConnectButton(); - } else { - return const SizedBox(); - } - }), - ], - ), + color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(isDarkMode ? 0.05 : 0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + // 状态标题行 + _buildDeviceStatusHeader(isDarkMode), + + // 设备信息 + SizedBox(height: 12.h), + Obx(() { + // 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断 + // 使用响应式访问确保UI更新 + return controller.isFullyConnectedRx.value + ? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息 + : _buildDisconnectedInfo(isDarkMode); //未连接提示 + }), + + // 连接按钮(仅在未连接时显示) + SizedBox(height: 12.h), + Obx(() { + // 使用响应式访问确保UI更新 + if (!controller.isFullyConnectedRx.value) { + return _buildConnectButton(); + } else { + return const SizedBox(); + } + }), + ], + ), ); }); } @@ -328,7 +328,6 @@ class HomeView extends GetView { // 连接错误部分 - // 已连接设备信息(新增设备名称 + 信号强度 + 电池信息) Widget _buildConnectedDeviceInfo(bool isDarkMode) { return Column( @@ -417,20 +416,20 @@ class HomeView extends GetView { isDarkMode: isDarkMode, ), - // 分隔线 - Container( - height: 30.h, - width: 1, - color: isDarkMode ? Colors.grey[800] : Colors.grey[300], - ), - - // 充电盒 - _buildCompactBatteryItem( - label: '充电盒', - batteryLevel: caseBattery.value, - isCharging: caseCharging.value, - isDarkMode: isDarkMode, - ), + // // 分隔线 + // Container( + // height: 30.h, + // width: 1, + // color: isDarkMode ? Colors.grey[800] : Colors.grey[300], + // ), + + // // 充电盒 + // _buildCompactBatteryItem( + // label: '充电盒', + // batteryLevel: caseBattery.value, + // isCharging: caseCharging.value, + // isDarkMode: isDarkMode, + // ), ], ), ); diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index f9f66fc7e..1dfe1df3e 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -36,7 +36,8 @@ class MeetingRecordController extends GetxController final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call final RxBool isMarked = false.obs; final RxBool showTextContent = false.obs; - final RxBool isVoiceRecognitionActive = false.obs; + final RxBool isRecording = false.obs; + //final RxBool isVoiceRecognitionActive = false.obs; final RxList waveData = [].obs; // Transcription content @@ -133,9 +134,12 @@ class MeetingRecordController extends GetxController _recognitionSubscription?.cancel(); titleEditingController.dispose(); titleFocusNode.dispose(); - _bleManager.closeCodec(); - _asrService.stopContinuousRecognition(); + if (audioType.value != 0) { + _bleManager.closeCodec(); + } _asrService.stopRecord(true); + _asrService.stopContinuousRecognition(); + isRecording.value = false; } /// Handles focus changes in title text field @@ -222,7 +226,7 @@ class MeetingRecordController extends GetxController // Start audio recording await _asrService.enableRecord("${dir.path}/$fullFileName.wav"); - + isRecording.value = true; // Update state fileName.value = fullFileName; newName.value = fullFileName; @@ -327,7 +331,10 @@ class MeetingRecordController extends GetxController /// Saves recording and cleans up resources Future saveRecording() async { resetTimer(); - _bleManager.closeCodec(); + if (audioType.value != 0) { + _bleManager.closeCodec(); + } + if (!isRecording.value) return; await _asrService.stopRecord(true); // Rename file if title was edited @@ -339,12 +346,12 @@ class MeetingRecordController extends GetxController final Newdir = Directory("${appDir.path}/DestAudio/"); if (!await Newdir.exists()) await Newdir.create(recursive: true); await _asrService.moveFile( - "${dir.path}/${fileName.value}.wav", - "${Newdir.path}/${fileName.value}.wav", + "${dir.path}${fileName.value}.wav", + "${Newdir.path}${fileName.value}.wav", ); } else { await _asrService.renameFile( - "${dir.path}/${fileName.value}.wav", + "${dir.path}${fileName.value}.wav", newName.value, ); } @@ -352,6 +359,7 @@ class MeetingRecordController extends GetxController await _asrService.stopContinuousRecognition(); _resetToDefaultState(); + isRecording.value = false; } /// Resets controller to initial state diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 6471a8223..ef7c04602 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -547,28 +547,28 @@ class SettingsView extends GetView { // color: isDarkMode // ? Colors.white.withOpacity(0.1) // : Colors.grey[200]), - // // 面对面翻译测试 - // _buildSimpleNavigationSetting( - // title: '面对面翻译测试', - // subtitle: '测试面对面翻译', - // 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.ftftranslation); - // }, - // isDarkMode: isDarkMode, - // ), - // Divider( - // height: 1, - // color: isDarkMode - // ? Colors.white.withOpacity(0.1) - // : Colors.grey[200]), + // 面对面翻译测试 + _buildSimpleNavigationSetting( + title: '面对面翻译测试', + subtitle: '测试面对面翻译', + 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.ftftranslation); + }, + isDarkMode: isDarkMode, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), // BLE测试 _buildSimpleNavigationSetting( title: 'opus解码测试', diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 77d13af1d..fd9658ac6 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -1,3 +1,4 @@ +import 'dart:ffi'; import 'dart:io'; import 'package:deep_voice/core/utils/permission_util.dart'; @@ -37,7 +38,7 @@ class TranslationController extends GetxController { // 音频输入源 bool _audioSourceType = false; // 倒计时器 - + bool istts = false; final RxString recordDurationText = '00:00'.obs; Timer? _recordTimer; int _seconds = 0; @@ -47,6 +48,8 @@ class TranslationController extends GetxController { static const String _targetLanguageKey = 'translation_target_language'; static const int _maxHistoryItems = 10; + RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 + RxInt lastActiveSpeaker = 0.obs; // 上一次状态 // 状态变量 final isRecognizing = false.obs; final isTranslating = false.obs; @@ -62,12 +65,15 @@ class TranslationController extends GetxController { final currentModeTitle = 'simultaneousTranslation'.obs; final currentMode = 'simultaneous'.obs; // 翻译历史和当前项 + + String? fTFTranslationResult = ''; final translationHistory = [].obs; String? currentSessionId; final appDocDir = getApplicationDocumentsDirectory(); var appDir; var dir; + //int num = 0; Future changeTranslationMode(String mode) async { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); @@ -89,6 +95,29 @@ class TranslationController extends GetxController { } } + Future setActiveSpeaker(int id) async { + activeSpeaker.value = id; + // 监听识别结果 + if (activeSpeaker.value != 0) { + istts = false; + _asrService.setcontinue(); + lastActiveSpeaker.value = id; + } else { + _asrService.setPause(); + if (fTFTranslationResult != '' && !istts) { + istts = true; + await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); + await playTranslatedText(fTFTranslationResult!); + fTFTranslationResult = ''; + } + } + } + + // 切换TTS功能 + void disableBluetoothAudio() => _asrService.disableBluetoothAudio(); + // 切换TTS功能 + void restoreOriginalAudioState() => _asrService.restoreOriginalAudioState(); + // 兼容旧版视图的计算属性 Rx get currentItem { if (translationHistory.isNotEmpty && @@ -210,19 +239,6 @@ class TranslationController extends GetxController { return false; } } - // /// 设置事件监听器 - // void _setupEventListeners() { - // // 监听扫描结果 - // _scanSubscription = jieli.scanResults.listen((devices) { - // scannedDevices.assignAll(devices); - // }); - - // // 监听唤醒信号 - // _wakeupSubscription = jieli.wakeupStream.listen((wakeup) { - // wakeupReceived.value = true; - // Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号'); - // }); - // } // 初始化服务 Future _initServices() async { @@ -340,21 +356,6 @@ class TranslationController extends GetxController { } currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); - - // // 初始化新的翻译项 - // final newItem = TranslationItem( - // sourceText: '', - // translatedText: '', - // sourceLanguageCode: sourceLanguageCode.value, - // targetLanguageCode: targetLanguageCode.value, - // timestamp: DateTime.now(), - // sessionId: currentSessionId!, - // isFirstInSession: true, - // isIntermediate: true, - // ); - - // // 添加到历史记录 - // translationHistory.add(newItem); // 开始连续语音识别 if (currentMode.value == "audioVideo") { bleManager.openDecoder(); @@ -375,6 +376,7 @@ class TranslationController extends GetxController { } final recognitionStream = await _asrService.startContinuousRecognition(_audioSourceType); + _asrService.setcontinue(); isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { startRecording(); @@ -461,8 +463,6 @@ class TranslationController extends GetxController { translationHistory.last.sessionId != currentSessionId, isIntermediate: true, ); - - translationHistory.add(newItem); // 添加新项后滚动到底部 _scrollToBottom(); } else { @@ -562,32 +562,29 @@ class TranslationController extends GetxController { detectedSourceLanguageCode; translationHistory[index].targetLanguageCode = detectedTargetLanguageCode; - print("判断是否是面对面模式且语言一致: ${detectedSourceLanguageCode}"); - // 判断是否是面对面模式且语言一致 - final bool isFaceToFaceSameLang = - detectedSourceLanguageCode == sourceLanguageCode.value && - currentMode.value == "faceToFace"; - - // 设置音频输出设备 - var isSuccess = await _ttsService.setAudioOutputDevice( - isFaceToFaceSameLang - ? AudioOutputDevice.speaker.index - : AudioOutputDevice.headphones.index, - ); - if (isSuccess) { - print('设置音频输出设备成功'); - translationHistory.refresh(); - saveTranslationHistory(); - // 更新后滚动到底部 - _scrollToBottom(); - - // 播放TTS - 只在最终结果时播放 - if (isTtsEnabled.value && isFinal) { - // Logger.info('播放翻译文本: ${translationResult}'); - await playTranslatedText(translationResult); + translationHistory.refresh(); + saveTranslationHistory(); + // 更新后滚动到底部 + _scrollToBottom(); + if (isFinal && currentMode.value == 'faceToFace') { + print("音频输出文本2$translationResult"); + fTFTranslationResult = + (fTFTranslationResult ?? '') + translationResult; + + if (activeSpeaker.value == 0 && !istts) { + istts = true; + await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); + await playTranslatedText(fTFTranslationResult!); + fTFTranslationResult = ''; + ; } - } else { - print('设置音频输出设备失败'); + } + // 播放TTS - 只在最终结果时播放 + if (isTtsEnabled.value && + isFinal && + currentMode.value != 'faceToFace') { + _ttsService.setAudioOutputDevice(activeSpeaker.value); + await playTranslatedText(translationResult); } } else { Logger.info( diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 221bab5a7..e2bf87d54 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -399,38 +399,209 @@ class TranslationView extends GetView { ), ], ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 主要控制按钮 - 开始/暂停语音识别 - Obx(() { - final isRecognizing = controller.isRecognizing.value; - return GestureDetector( - onTap: () { - if (isRecognizing) { - controller.stopAll(); - } else { - controller.startRecognition(); - } - }, - child: Container( - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - color: isRecognizing - ? (isDarkMode ? Colors.red[700] : Colors.red) - : (isDarkMode ? AppColors.primary : Colors.blue), - shape: BoxShape.circle, - ), - child: Icon( - isRecognizing ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 30.sp, + child: Obx(() { + // 同时模式显示双按钮 + if (controller.currentMode.value == 'faceToFace') { + controller.startRecognition(); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 耳机图标按钮 (说话者1) + _buildHoldToTalkButton( + isDarkMode: isDarkMode, + speakerId: 1, + activeSpeaker: controller.activeSpeaker.value, + icon: Icons.headset, // 耳机图标 + label: "耳机用户", + ), + SizedBox(width: 40.w), + // 手机图标按钮 (说话者2) + _buildHoldToTalkButton( + isDarkMode: isDarkMode, + speakerId: 2, + activeSpeaker: controller.activeSpeaker.value, + icon: Icons.phone_android, // 手机图标 + label: "手机用户", + ), + ], + ); + } + // 普通模式显示单按钮 + else { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 主要控制按钮 - 开始/暂停语音识别 + Obx(() { + final isRecognizing = controller.isRecognizing.value; + return GestureDetector( + onTap: () { + if (isRecognizing) { + controller.stopAll(); + } else { + controller.startRecognition(); + } + }, + child: Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: isRecognizing + ? (isDarkMode ? Colors.red[700] : Colors.red) + : (isDarkMode ? AppColors.primary : Colors.blue), + shape: BoxShape.circle, + ), + child: Icon( + isRecognizing ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 30.sp, + ), + ), + ); + }), + ], + ); + } + }), + ); + } + +// // 按住说话按钮组件 +// Widget _buildHoldToTalkButton({ +// required bool isDarkMode, +// required int speakerId, +// required int activeSpeaker, +// required IconData icon, +// required String label, +// }) { +// bool isActive = activeSpeaker == speakerId; + +// return Listener( +// onPointerDown: (_) { +// if (activeSpeaker == 0) { +// // 没有激活的按钮时才能激活 + +// if (speakerId == 1) { +// //耳机收音 +// controller.restoreOriginalAudioState(); + +// //controller.setActiveSpeaker(speakerId); +// } else { +// controller.disableBluetoothAudio(); +// } +// controller.setActiveSpeaker(speakerId); +// //controller.startRecognition(); +// } +// }, +// onPointerUp: (_) { +// if (isActive) { +// //controller.stopRecognition(); +// controller.setActiveSpeaker(0); +// } +// }, +// child: Container( +// width: 60.w, +// height: 60.w, +// decoration: BoxDecoration( +// color: isActive +// ? (isDarkMode ? Colors.green[700] : Colors.green) // 激活状态颜色 +// : (isDarkMode ? AppColors.primary : Colors.blue), +// shape: BoxShape.circle, +// ), +// child: Icon( +// Icons.mic, // 麦克风图标 +// color: isActive ? Colors.white : Colors.white.withOpacity(0.7), +// size: 30.sp, +// ), +// ), +// ); +// } + +// 按住说话按钮组件 + Widget _buildHoldToTalkButton({ + required bool isDarkMode, + required int speakerId, + required int activeSpeaker, + required IconData icon, + required String label, + }) { + bool isActive = activeSpeaker == speakerId; + bool isDisabled = activeSpeaker != 0 && activeSpeaker != speakerId; + + return Listener( + onPointerDown: (_) { + if (activeSpeaker == 0) { + // 没有激活的按钮时才能激活 + if (speakerId == 1) { + // 耳机收音 - 恢复原始音频状态 + controller.restoreOriginalAudioState(); + } else { + // 手机收音 - 禁用蓝牙音频 + controller.disableBluetoothAudio(); + } + controller.setActiveSpeaker(speakerId); + } + }, + onPointerUp: (_) { + if (isActive) { + controller.setActiveSpeaker(0); + } + }, + onPointerCancel: (_) { + if (isActive) { + controller.setActiveSpeaker(0); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 80.w, + height: 80.w, + decoration: BoxDecoration( + color: isActive + ? (isDarkMode ? Colors.green[700] : Colors.green) + : (isDisabled + ? (isDarkMode ? Colors.grey[800] : Colors.grey[300]) + : (isDarkMode ? AppColors.primary : Colors.blue)), + shape: BoxShape.circle, + boxShadow: isActive + ? [ + BoxShadow( + color: Colors.green.withOpacity(0.5), + blurRadius: 10, + spreadRadius: 3, + ) + ] + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, + children: [ + // 设备类型图标(叠加在麦克风上) + Icon( + icon, + color: isActive + ? Colors.white + : Colors.white.withOpacity(isDisabled ? 0.3 : 0.9), + size: 24.sp, ), + ], + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: isActive + ? Colors.white + : Colors.white.withOpacity(isDisabled ? 0.3 : 0.9), ), - ); - }), - ], + ), + ], + ), ), ); } 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 f38f5e31e..919247837 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 @@ -1,23 +1,17 @@ package com.yunqiinnovation.azure_speech import android.content.Context -import android.content.pm.PackageManager -import android.media.AudioAttributes + import android.media.AudioFormat import android.media.AudioRecord import android.media.MediaRecorder import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.NoiseSuppressor import android.media.audiofx.AutomaticGainControl -import android.os.Process import android.util.Log import com.microsoft.cognitiveservices.speech.* -import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.util.EventHandler -import kotlinx.coroutines.* -import java.util.concurrent.ExecutionException -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.CoroutineContext + import android.media.AudioManager import com.microsoft.cognitiveservices.speech.audio.PullAudioInputStreamCallback import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat @@ -25,6 +19,9 @@ import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit import com.yunqiinnovation.azure_speech.tools.RecordFile +import com.microsoft.cognitiveservices.speech.audio.AudioConfig +import com.microsoft.cognitiveservices.speech.audio.AudioInputStream +import com.microsoft.cognitiveservices.speech.audio.PushAudioInputStream /** @@ -37,6 +34,9 @@ class AzureAsrHelper(private val context: Context) { private val tag = "AzureAsrHelper" // 核心组件 + private var echoCanceler: AcousticEchoCanceler? = null + private var noiseSuppressor: NoiseSuppressor? = null + private var automaticGainControl: AutomaticGainControl? = null private var speechConfig: SpeechConfig? = null private var recognizer: SpeechRecognizer? = null private var audioConfig: AudioConfig? = null @@ -55,7 +55,7 @@ class AzureAsrHelper(private val context: Context) { var audioSourceType = AudioSourceType.MICROPHONE // 音频处理 - private var microphoneStream: MicrophoneStream? = null + // private var microphoneStream: MicrophoneStream? = null private var externalAudioStream: ExternalAudioPullStream? = null // 音频模式管理 @@ -65,6 +65,10 @@ class AzureAsrHelper(private val context: Context) { // 录音文件处理 var recordfile: RecordFile? = null + private var isPaused = false + private var audioRecord: AudioRecord? = null + private var captureThread: Thread? = null + private var audioStream: PushAudioInputStream? = null /** * 音频来源类型 @@ -101,11 +105,16 @@ class AzureAsrHelper(private val context: Context) { // 释放之前的资源 dispose() - + // 1. 创建可控制的音频流 + val format = AudioStreamFormat.getWaveFormatPCM(16000, 16, 1) + audioStream = AudioInputStream.createPushStream(format) // 初始化音频管理器 audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL + // 5. 初始化麦克风数据 + initAudioRecord() + // 保存配置 this.subscriptionKey = subscriptionKey this.region = region @@ -206,14 +215,17 @@ class AzureAsrHelper(private val context: Context) { */ private fun setupMicrophoneStream() { try { + // 创建麦克风流 - microphoneStream = MicrophoneStream() + // microphoneStream = MicrophoneStream() // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 - audioConfig = AudioConfig.fromStreamInput(microphoneStream) + // audioConfig = AudioConfig.fromStreamInput(microphoneStream) + audioConfig = AudioConfig.fromStreamInput(audioStream) + } catch (e: Exception) { Log.e(tag, "设置麦克风流失败: ${e.message}") - microphoneStream = null + //microphoneStream = null } } @@ -349,10 +361,13 @@ class AzureAsrHelper(private val context: Context) { setupEventListeners(callback) // 启动音频处理 - startAudioProcessing() + //startAudioProcessing() // 开始连续识别 recognizer?.startContinuousRecognitionAsync() + // 开始录音 + audioRecord?.startRecording() + startCaptureThread() // 再启动数据读取线程 isContinuousRecognitionActive = true return true @@ -458,7 +473,7 @@ class AzureAsrHelper(private val context: Context) { // 停止音频处理 stopAudioProcessing() // 停止录音 - + stopMicrophoneCapture() // 会话结束事件会设置isContinuousRecognitionActive = false return true } catch (e: Exception) { @@ -497,6 +512,7 @@ class AzureAsrHelper(private val context: Context) { recognizer?.stopContinuousRecognitionAsync() isContinuousRecognitionActive = false } + releaseAudioResources() // 停止音频处理 stopAudioProcessing() @@ -510,13 +526,17 @@ class AzureAsrHelper(private val context: Context) { speechConfig?.close() speechConfig = null + // 关闭音频流(根据实际实现可能需要) + audioStream?.close() + audioStream = null + // 释放音频配置 audioConfig?.close() audioConfig = null // 确保状态被重置 isContinuousRecognitionActive = false - microphoneStream = null + //microphoneStream = null externalAudioStream = null audioConfig = null recognizer = null @@ -525,7 +545,7 @@ class AzureAsrHelper(private val context: Context) { // 确保状态被重置 isContinuousRecognitionActive = false - microphoneStream = null + //microphoneStream = null externalAudioStream = null audioConfig = null recognizer = null @@ -555,15 +575,15 @@ class AzureAsrHelper(private val context: Context) { * 停止音频处理 */ private fun stopAudioProcessing() { - microphoneStream?.let { - try { - it.close() - microphoneStream = null - } catch (e: Exception) { - Log.e(tag, "关闭麦克风流失败: ${e.message}") - e.printStackTrace() - } - } + // microphoneStream?.let { + // try { + // it.close() + // microphoneStream = null + // } catch (e: Exception) { + // Log.e(tag, "关闭麦克风流失败: ${e.message}") + // e.printStackTrace() + // } + // } externalAudioStream?.let { try { @@ -631,200 +651,418 @@ class AzureAsrHelper(private val context: Context) { recordfile!!.closeFile(isSave) } + fun stopAudioPause() { + Log.d(tag, "关闭麦克风stopAudioPause: ") + // 清理旧的识别器 + isPaused = true + + + } + + fun setAudiocontinue() { + Log.d(tag, "继续麦克风stopAudioPause: ") + + // 开始连续识别 + isPaused = false + } + /** - * 麦克风流 - 拉流模式 - * 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据 + * 禁用蓝牙音频功能,切换回正常音频模式 */ - private inner class MicrophoneStream : PullAudioInputStreamCallback() { - private var audioRecord: AudioRecord? = null - private var echoCanceler: AcousticEchoCanceler? = null - private var noiseSuppressor: NoiseSuppressor? = null - private var automaticGainControl: AutomaticGainControl? = null - - // 音频配置 - private val sampleRate = 16000 - private val channelConfig = AudioFormat.CHANNEL_IN_MONO - private val audioFormat = AudioFormat.ENCODING_PCM_16BIT - - // 优化:减少缓冲区大小以降低延迟 - private val bufferSize = AudioRecord.getMinBufferSize( - sampleRate, channelConfig, audioFormat - ).let { if (it < 0) 2048 else it } - - init { - initMicrophone() + fun disableBluetoothAudio() { + try { + // 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由 + audioManager?.isBluetoothScoOn = false + // 停止蓝牙SCO连接 + audioManager?.stopBluetoothSco() + // 切换回正常音频模式(非通话模式) + audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + + } catch (e: Exception) { + Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}") } + } - /** - * 初始化麦克风 - */ - private fun initMicrophone() { - try { - // 创建录音对象 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - val format = AudioFormat.Builder() - .setSampleRate(sampleRate) - .setEncoding(audioFormat) - .setChannelMask(channelConfig) - .build() - - audioRecord = AudioRecord.Builder() - .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) - .setAudioFormat(format) - .setBufferSizeInBytes(bufferSize) - .build() - } else { - audioRecord = AudioRecord( - MediaRecorder.AudioSource.VOICE_COMMUNICATION, - sampleRate, - channelConfig, - audioFormat, - bufferSize - ) - } + /** + * 恢复原始音频设备状态(通常是重新启用蓝牙) + */ + fun restoreOriginalAudioState() { + try { + // 恢复应用启动时的原始音频模式 + audioManager?.mode = AudioManager.MODE_NORMAL - // 检查初始化状态 - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { - throw IllegalStateException("AudioRecord初始化失败") - } + // 重新启用蓝牙SCO + audioManager?.isBluetoothScoOn = true + // 启动蓝牙SCO连接(通常在需要蓝牙通话时使用) + audioManager?.startBluetoothSco() - // 应用音频效果 - applyAudioEffects() + } catch (e: Exception) { + Log.e("AudioConfig", "Failed to restore audio state: ${e.message}") + } + } - // 开始录音 - audioRecord?.startRecording() + /** + * 开始麦克风捕获 + */ + private fun initAudioRecord() { + // 设置音频参数 + val sampleRate = 16000 // 16kHz采样率(语音识别常用) + val channelConfig = AudioFormat.CHANNEL_IN_MONO // 单声道输入 + val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式 + + // 计算最小缓冲区大小(避免出现音频欠载) + val minBufferSize = AudioRecord.getMinBufferSize( + sampleRate, + channelConfig, + audioFormat + ) - // 检查录音状态 - if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) { - throw IllegalStateException("录音启动失败") - } - } catch (e: Exception) { - e.printStackTrace() - } - } + // 创建 AudioRecord 对象 + audioRecord = AudioRecord( + MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除) + sampleRate, + channelConfig, + audioFormat, + minBufferSize * 2 // 使用两倍缓冲区防止溢出 + ) - /** - * 应用音频效果 - 总是启用所有音频增强功能 - */ - private fun applyAudioEffects() { - val sessionId = audioRecord?.audioSessionId ?: return - try { - // 设置音频模式为通信模式 - this@AzureAsrHelper.audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION - - // 启用声学回音消除 - if (AcousticEchoCanceler.isAvailable()) { - echoCanceler = AcousticEchoCanceler.create(sessionId) - echoCanceler?.enabled = true - Log.i(tag, "声学回音消除已启用") - } else { - Log.w(tag, "设备不支持声学回音消除") - } + // 应用音频效果 + applyAudioEffects() + } - // 启用噪声抑制 - if (NoiseSuppressor.isAvailable()) { - noiseSuppressor = NoiseSuppressor.create(sessionId) - noiseSuppressor?.enabled = true - Log.i(tag, "噪声抑制已启用") - } else { - Log.w(tag, "设备不支持噪声抑制") + // 3. 单独封装线程启动逻辑 + private fun startCaptureThread() { + captureThread = Thread { + // 循环条件确保录音已开始 + + val buffer = ByteArray(1024) // 4KB数据缓冲区 + while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + // 从麦克风读取数据 + val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 + + // 仅在未暂停时处理数据 + if (!isPaused && bytesRead > 0) { + // 写入音频流(可能是网络传输或本地处理) + audioStream?.write(buffer) + // 保存到WAV文件(如果启用了录制功能) + recordfile?.saveAudioDataToWav(buffer) } - // 启用自动增益控制 - if (AutomaticGainControl.isAvailable()) { - automaticGainControl = AutomaticGainControl.create(sessionId) - automaticGainControl?.enabled = true - Log.i(tag, "自动增益控制已启用") - } else { - Log.w(tag, "设备不支持自动增益控制") + // 暂停时短暂休眠以减少CPU占用 + if (isPaused) { + try { + Thread.sleep(50) + } catch (e: InterruptedException) { + break // 线程被中断时退出循环 + } } - - Log.i(tag, "回音消除和音频增强功能已全部启用") - } catch (e: Exception) { - Log.e(tag, "设置音频效果失败: ${e.message}") } + + }.apply { start() } + } + + /** + * 停止麦克风捕获并释放所有相关资源 + */ + private fun stopMicrophoneCapture() { + try { + // 中断并等待捕获线程结束 + captureThread?.interrupt() + captureThread?.join(300) // 最多等待300ms + + // 停止录音并释放AudioRecord资源 + audioRecord?.stop() // 必须先停止再释放 + + + } catch (e: Exception) { + Log.e("AudioConfig", "Error releasing resources: ${e.message}") + } finally { + // 确保恢复原始音频状态 + restoreOriginalAudioState() } + } - /** - * 获取音频格式 - */ - fun getFormat(): AudioStreamFormat = - AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort()) + /** + * 应用音频效果 - 总是启用所有音频增强功能 + */ + private fun applyAudioEffects() { + val sessionId = audioRecord?.audioSessionId ?: return - /** - * Azure SDK 调用此方法获取音频数据 - * 这是拉流模式的核心方法,由SDK调用以获取音频数据 - */ - /** - * 优化:简化音频数据读取(移除文件保存) - */ - override fun read(buffer: ByteArray): Int { - try { - val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1 - if (result < 0) { - Log.e(tag, "读取音频数据失败: $result") - return 0 - } - recordfile?.saveAudioDataToWav(buffer) - return result - } catch (e: Exception) { - Log.e(tag, "读取音频异常: ${e.message}") - return 0 + try { + // 设置音频模式为通信模式 + audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + + // 启用声学回音消除 + if (AcousticEchoCanceler.isAvailable()) { + echoCanceler = AcousticEchoCanceler.create(sessionId) + echoCanceler?.enabled = true + Log.i(tag, "声学回音消除已启用") + } else { + Log.w(tag, "设备不支持声学回音消除") } - } - /** - * 优化:简化资源关闭 - */ - override fun close() { - releaseAudioResources() - } + // 启用噪声抑制 + if (NoiseSuppressor.isAvailable()) { + noiseSuppressor = NoiseSuppressor.create(sessionId) + noiseSuppressor?.enabled = true + Log.i(tag, "噪声抑制已启用") + } else { + Log.w(tag, "设备不支持噪声抑制") + } - /** - * 释放音频资源 - */ - private fun releaseAudioResources() { - try { - // 停止录音 - if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { - audioRecord?.stop() - } + // 启用自动增益控制 + if (AutomaticGainControl.isAvailable()) { + automaticGainControl = AutomaticGainControl.create(sessionId) + automaticGainControl?.enabled = true + Log.i(tag, "自动增益控制已启用") + } else { + Log.w(tag, "设备不支持自动增益控制") + } - // 释放回音消除器 - echoCanceler?.let { - it.enabled = false - it.release() - echoCanceler = null - } + Log.i(tag, "回音消除和音频增强功能已全部启用") + } catch (e: Exception) { + Log.e(tag, "设置音频效果失败: ${e.message}") + } + } - // 释放噪声抑制器 - noiseSuppressor?.let { - it.enabled = false - it.release() - noiseSuppressor = null - } + /** + * 释放音频资源 + */ + private fun releaseAudioResources() { + try { + // 停止录音 + if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + audioRecord?.stop() + } - // 释放自动增益控制 - automaticGainControl?.let { - it.enabled = false - it.release() - automaticGainControl = null - } + // 释放回音消除器 + echoCanceler?.let { + it.enabled = false + it.release() + echoCanceler = null + } - // 恢复音频模式 - this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode + // 释放噪声抑制器 + noiseSuppressor?.let { + it.enabled = false + it.release() + noiseSuppressor = null + } - // 释放录音实例 - audioRecord?.release() - audioRecord = null - } catch (e: Exception) { - Log.e(tag, "释放音频资源失败: ${e.message}") - e.printStackTrace() + // 释放自动增益控制 + automaticGainControl?.let { + it.enabled = false + it.release() + automaticGainControl = null } + + // 恢复音频模式 + this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode + + // 释放录音实例 + audioRecord?.release() + audioRecord = null + } catch (e: Exception) { + Log.e(tag, "释放音频资源失败: ${e.message}") + e.printStackTrace() } } + // /** + // * 麦克风流 - 拉流模式 + // * 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据 + // */ + // private inner class MicrophoneStream : PullAudioInputStreamCallback() { + // private var audioRecord: AudioRecord? = null + // private var echoCanceler: AcousticEchoCanceler? = null + // private var noiseSuppressor: NoiseSuppressor? = null + // private var automaticGainControl: AutomaticGainControl? = null + + // // 音频配置 + // private val sampleRate = 16000 + // private val channelConfig = AudioFormat.CHANNEL_IN_MONO + // private val audioFormat = AudioFormat.ENCODING_PCM_16BIT + + // // 优化:减少缓冲区大小以降低延迟 + // private val bufferSize = AudioRecord.getMinBufferSize( + // sampleRate, channelConfig, audioFormat + // ).let { if (it < 0) 2048 else it } + + // init { + // initMicrophone() + // } + + // /** + // * 初始化麦克风 + // */ + // private fun initMicrophone() { + // try { + // // 创建录音对象 + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // val format = AudioFormat.Builder() + // .setSampleRate(sampleRate) + // .setEncoding(audioFormat) + // .setChannelMask(channelConfig) + // .build() + + // audioRecord = AudioRecord.Builder() + // .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) + // .setAudioFormat(format) + // .setBufferSizeInBytes(bufferSize) + // .build() + // } else { + // audioRecord = AudioRecord( + // MediaRecorder.AudioSource.VOICE_COMMUNICATION, + // sampleRate, + // channelConfig, + // audioFormat, + // bufferSize + // ) + // } + + // // 检查初始化状态 + // if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + // throw IllegalStateException("AudioRecord初始化失败") + // } + + // // 应用音频效果 + // applyAudioEffects() + + // // 开始录音 + // audioRecord?.startRecording() + + // // 检查录音状态 + // if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) { + // throw IllegalStateException("录音启动失败") + // } + // } catch (e: Exception) { + // e.printStackTrace() + // } + // } + + // /** + // * 应用音频效果 - 总是启用所有音频增强功能 + // */ + // private fun applyAudioEffects() { + // val sessionId = audioRecord?.audioSessionId ?: return + + // try { + // // 设置音频模式为通信模式 + // this@AzureAsrHelper.audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + + // // 启用声学回音消除 + // if (AcousticEchoCanceler.isAvailable()) { + // echoCanceler = AcousticEchoCanceler.create(sessionId) + // echoCanceler?.enabled = true + // Log.i(tag, "声学回音消除已启用") + // } else { + // Log.w(tag, "设备不支持声学回音消除") + // } + + // // 启用噪声抑制 + // if (NoiseSuppressor.isAvailable()) { + // noiseSuppressor = NoiseSuppressor.create(sessionId) + // noiseSuppressor?.enabled = true + // Log.i(tag, "噪声抑制已启用") + // } else { + // Log.w(tag, "设备不支持噪声抑制") + // } + + // // 启用自动增益控制 + // if (AutomaticGainControl.isAvailable()) { + // automaticGainControl = AutomaticGainControl.create(sessionId) + // automaticGainControl?.enabled = true + // Log.i(tag, "自动增益控制已启用") + // } else { + // Log.w(tag, "设备不支持自动增益控制") + // } + + // Log.i(tag, "回音消除和音频增强功能已全部启用") + // } catch (e: Exception) { + // Log.e(tag, "设置音频效果失败: ${e.message}") + // } + // } + + // /** + // * 获取音频格式 + // */ + // fun getFormat(): AudioStreamFormat = + // AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort()) + + // /** + // * Azure SDK 调用此方法获取音频数据 + // * 这是拉流模式的核心方法,由SDK调用以获取音频数据 + // */ + // /** + // * 优化:简化音频数据读取(移除文件保存) + // */ + // override fun read(buffer: ByteArray): Int { + // try { + // val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1 + // if (result < 0) { + // Log.e(tag, "读取音频数据失败: $result") + // return 0 + // } + // recordfile?.saveAudioDataToWav(buffer) + // return result + // } catch (e: Exception) { + // Log.e(tag, "读取音频异常: ${e.message}") + // return 0 + // } + // } + + // /** + // * 优化:简化资源关闭 + // */ + // override fun close() { + // releaseAudioResources() + // } + + // /** + // * 释放音频资源 + // */ + // private fun releaseAudioResources() { + // try { + // // 停止录音 + // if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + // audioRecord?.stop() + // } + + // // 释放回音消除器 + // echoCanceler?.let { + // it.enabled = false + // it.release() + // echoCanceler = null + // } + + // // 释放噪声抑制器 + // noiseSuppressor?.let { + // it.enabled = false + // it.release() + // noiseSuppressor = null + // } + + // // 释放自动增益控制 + // automaticGainControl?.let { + // it.enabled = false + // it.release() + // automaticGainControl = null + // } + + // // 恢复音频模式 + // this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode + + // // 释放录音实例 + // audioRecord?.release() + // audioRecord = null + // } catch (e: Exception) { + // Log.e(tag, "释放音频资源失败: ${e.message}") + // e.printStackTrace() + // } + // } + // } + /** * 外部音频拉流 * 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式 diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 741605be7..06b1d07e9 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -16,6 +16,7 @@ import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType import com.yunqiinnovation.ble_service.BleService import com.deep_voice.speech.tts.AudioOutputDevice + /** AzureSpeechPlugin */ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { private val tag = "AzureSpeechPlugin" @@ -254,6 +255,24 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } + "setPause" -> { + azureAsrHelper.stopAudioPause(); + } + + "setcontinue" -> { + // azureAsrHelper.stopAudioPause(); + azureAsrHelper.setAudiocontinue(); + } + + "disableBluetoothAudio" -> { + azureAsrHelper.disableBluetoothAudio(); + } + + "restoreOriginalAudioState" -> { + // azureAsrHelper.stopAudioPause(); + azureAsrHelper.restoreOriginalAudioState(); + } + "startContinuousRecognition" -> { val useExternalAudio = call.argument("audioSourceType") ?: false // 确保事件通道已准备好 @@ -501,20 +520,21 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { azureTtsHelper.release() result.success(true) } + "setAudioOutputDevice" -> { - val type = call.argument("type") - if (type == 0) { - // 默认(如果有耳机选耳机,否则使用系统扬声器) - azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) - } else if (type == 1) { - // 强制使用耳机 - azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) - } else if (type == 2) { - // 强制使用扬声器 - azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) - } - - + val type = call.argument("type") + if (type == 0) { + // 默认(如果有耳机选耳机,否则使用系统扬声器) + azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) + } else if (type == 1) { + // 强制使用声器 + azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) + } else if (type == 2) { + // 强制使用耳机 + azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) + } + + result.success(true) } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index f0fb6f08a..2e9559c77 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -17,7 +17,7 @@ import java.io.InputStream /** * Azure TTS Helper - * + * * 基于微软Azure语音服务的TTS实现 * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis */ @@ -27,33 +27,33 @@ class AzureTtsHelper(private val context: Context) : ITtsService { private const val DEFAULT_LANGUAGE = "zh-CN" private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" } - + // Azure语音服务配置 private var speechConfig: SpeechConfig? = null private var synthesizer: SpeechSynthesizer? = null private var isInitialized = false private var isSpeaking = false - + // 当前配置 private var currentVoice = DEFAULT_VOICE private var currentRate = "0%" private var currentPitch = "0%" private var currentVolume = "100%" - + // 事件监听器列表 private val eventListeners = mutableListOf() private val audioDataListeners = mutableListOf() - + // 自定义音频输出流 private var customAudioOutputStream: PushAudioOutputStream? = null - + // 流式文本处理的缓冲区 private val streamBuffer = StringBuilder() private var lastSpeakTime = 0L /** * 初始化TTS引擎 - * + * * @param ttsAppId 未使用,保留为空即可 * @param ttsAppToken Azure语音服务订阅密钥 * @param ttsResource Azure语音服务区域 @@ -61,33 +61,33 @@ class AzureTtsHelper(private val context: Context) : ITtsService { * @return 是否初始化成功 */ override fun initialize( - ttsAppId: String, - ttsAppToken: String, - ttsResource: String, + ttsAppId: String, + ttsAppToken: String, + ttsResource: String, language: String ): Boolean { try { // 释放之前的资源 dispose() - + // 创建语音配置 speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource) - + // 使用标准的音频格式(16KHz, 16bit, 单声道) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm) - + // 确保使用正确的语音输出 speechConfig?.setProperty("SPEECH-AudioOutputFormat", "riff-16khz-16bit-mono-pcm") - + // 优化:设置低延迟连接属性 speechConfig?.setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "300") speechConfig?.setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") - + speechConfig?.setSpeechSynthesisVoiceName(currentVoice) - + // 添加日志记录语音格式 FileLogger.d(TAG, "语音设置: voice=$currentVoice, format=Riff16Khz16BitMonoPcm") - + // 创建音频配置 val audioConfig = if (customAudioOutputStream != null) { // 使用自定义音频输出流 @@ -96,29 +96,29 @@ class AzureTtsHelper(private val context: Context) : ITtsService { // 使用默认扬声器 AudioConfig.fromDefaultSpeakerOutput() } - + // 创建合成器 synthesizer = SpeechSynthesizer(speechConfig, audioConfig) - + // 设置事件监听 setupEventListeners() - + isInitialized = true FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource") - + // 优化:初始化完成后立即预热 warmupSynthesizer() - + return true } catch (e: Exception) { FileLogger.e(TAG, "TTS引擎初始化失败: ${e.message}") return false } } - + /** * 设置自定义音频输出流 - * + * * @param outputStream 自定义音频输出流,如果为null则使用默认音频输出 * @return 是否设置成功 */ @@ -126,19 +126,19 @@ class AzureTtsHelper(private val context: Context) : ITtsService { try { // 保存引用 customAudioOutputStream = outputStream - + // 如果已初始化,需要重新创建合成器以应用新的音频输出流 if (isInitialized) { recreateSynthesizer() } - + return true } catch (e: Exception) { FileLogger.e(TAG, "设置自定义音频输出流失败: ${e.message}") return false } } - + /** * 添加TTS事件监听器 */ @@ -147,14 +147,14 @@ class AzureTtsHelper(private val context: Context) : ITtsService { eventListeners.add(listener) } } - + /** * 移除TTS事件监听器 */ override fun removeListener(listener: TtsEventListener) { eventListeners.remove(listener) } - + /** * 添加音频数据监听器 */ @@ -163,17 +163,17 @@ class AzureTtsHelper(private val context: Context) : ITtsService { audioDataListeners.add(listener) } } - + /** * 移除音频数据监听器 */ override fun removeAudioDataListener(listener: AudioDataListener) { audioDataListeners.remove(listener) } - + /** * 设置是否使用内部播放器 - * + * * @param useInternalPlayer true: 使用内部播放器自动播放音频 * false: 仅通过音频数据监听器输出数据,不播放 */ @@ -183,7 +183,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { if (!useInternalPlayer && customAudioOutputStream == null) { // 创建自定义音频输出流以便捕获音频数据 customAudioOutputStream = SimpleAudioPlayer(context).getAudioOutputStream() - + // 如果已经初始化,需要重新创建 synthesizer if (isInitialized && speechConfig != null) { synthesizer?.close() @@ -194,7 +194,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } else if (useInternalPlayer && customAudioOutputStream != null) { // 切换回使用内部播放器 customAudioOutputStream = null - + // 如果已经初始化,需要重新创建 synthesizer if (isInitialized && speechConfig != null) { synthesizer?.close() @@ -204,16 +204,16 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } } } - + /** * 设置音频输出设备 - * + * * @param device 音频输出设备类型 */ override fun setAudioOutputDevice(device: AudioOutputDevice) { // Azure TTS使用系统默认的音频路由 // 音频输出设备的控制需要通过Android的AudioManager实现 - FileLogger.e(TAG, "音频输出设备类型device=: ${device}") + FileLogger.e(TAG, "音频输出设备类型device=: ${device}") val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager audioManager?.let { manager -> when (device) { @@ -221,7 +221,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService { // 默认模式:系统自动选择 manager.mode = AudioManager.MODE_NORMAL manager.isSpeakerphoneOn = false + manager.startBluetoothSco() } + AudioOutputDevice.SPEAKER -> { // 强制使用扬声器 /* MODE_NORMAL 默认媒体模式 ❌ 不能强制扬声器(会被耳机覆盖) @@ -230,17 +232,20 @@ class AzureTtsHelper(private val context: Context) : ITtsService { */ manager.mode = AudioManager.MODE_IN_COMMUNICATION manager.isSpeakerphoneOn = true + manager.stopBluetoothSco() } + AudioOutputDevice.HEADPHONES -> { // 强制使用耳机(如果已连接) manager.mode = AudioManager.MODE_NORMAL manager.isSpeakerphoneOn = false + manager.startBluetoothSco() // 注意:Android不能强制路由到耳机,只能在耳机已连接时使用 } } } } - + /** * 触发事件通知 */ @@ -256,25 +261,25 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } } } - + /** * 设置事件监听器 */ private fun setupEventListeners() { synthesizer?.apply { // 合成开始事件 - SynthesisStarted?.addEventListener { _, eventArgs -> + SynthesisStarted?.addEventListener { _, eventArgs -> FileLogger.d(TAG, "语音合成开始: resultId=${eventArgs.result.resultId}") notifyEvent(TtsEventType.SYNTHESIS_STARTED) // Azure TTS 在使用默认音频输出时会立即开始播放 notifyEvent(TtsEventType.PLAYBACK_STARTED) } - + // 合成中事件(接收音频数据) Synthesizing?.addEventListener { _, eventArgs -> val audioData = eventArgs.result.audioData FileLogger.d(TAG, "接收到音频数据: ${audioData.size} 字节") - + // 通知音频数据监听器 if (audioData.isNotEmpty()) { val listeners = ArrayList(audioDataListeners) @@ -287,38 +292,45 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } } } - + // 合成完成事件 - SynthesisCompleted?.addEventListener { _, eventArgs -> - FileLogger.d(TAG, "语音合成完成: resultId=${eventArgs.result.resultId}, 音频长度=${eventArgs.result.audioLength} 字节") + SynthesisCompleted?.addEventListener { _, eventArgs -> + FileLogger.d( + TAG, + "语音合成完成: resultId=${eventArgs.result.resultId}, 音频长度=${eventArgs.result.audioLength} 字节" + ) isSpeaking = false notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) // Azure TTS 合成完成即播放完成 notifyEvent(TtsEventType.PLAYBACK_COMPLETED) } - + // 合成取消事件 - SynthesisCanceled?.addEventListener { _, eventArgs -> + SynthesisCanceled?.addEventListener { _, eventArgs -> val reason = eventArgs.result.reason FileLogger.e(TAG, "语音合成取消: reason=$reason") - + if (reason == ResultReason.Canceled) { - val cancellation = SpeechSynthesisCancellationDetails.fromResult(eventArgs.result) - FileLogger.e(TAG, "取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}") + val cancellation = + SpeechSynthesisCancellationDetails.fromResult(eventArgs.result) + FileLogger.e( + TAG, + "取消详情: reason=${cancellation.reason}, errorCode=${cancellation.errorCode}, errorDetails=${cancellation.errorDetails}" + ) } - + isSpeaking = false notifyEvent(TtsEventType.SYNTHESIS_CANCELED) } } } - + /** * 设置语音角色 */ override fun setVoice(voiceName: String): Boolean { if (!isInitialized) return false - + try { currentVoice = voiceName speechConfig?.setSpeechSynthesisVoiceName(voiceName) @@ -326,21 +338,23 @@ class AzureTtsHelper(private val context: Context) : ITtsService { return true } catch (e: Exception) { FileLogger.e(TAG, "设置语音失败: ${e.message}") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "VOICE_SET_FAILED", - "errorMessage" to "设置语音失败: ${e.message}" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "VOICE_SET_FAILED", + "errorMessage" to "设置语音失败: ${e.message}" + ) + ) return false } } - + /** * 重新创建合成器 */ private fun recreateSynthesizer() { try { synthesizer?.close() - + // 创建音频配置 val audioConfig = if (customAudioOutputStream != null) { // 使用自定义音频输出流 @@ -349,19 +363,21 @@ class AzureTtsHelper(private val context: Context) : ITtsService { // 使用默认扬声器 AudioConfig.fromDefaultSpeakerOutput() } - + // 使用新的音频配置创建合成器 synthesizer = SpeechSynthesizer(speechConfig, audioConfig) setupEventListeners() } catch (e: Exception) { FileLogger.e(TAG, "重新创建合成器失败: ${e.message}") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "RECREATE_FAILED", - "errorMessage" to "重新创建合成器失败: ${e.message}" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "RECREATE_FAILED", + "errorMessage" to "重新创建合成器失败: ${e.message}" + ) + ) } } - + /** * 预热TTS引擎(减少首次合成延迟) */ @@ -377,7 +393,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { """.trimIndent() - + // 静音预热(音量设为0) synthesizer?.SpeakSsmlAsync(warmupSsml) } catch (e: Exception) { @@ -385,6 +401,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { FileLogger.d(TAG, "TTS预热失败: ${e.message}") } } + //清洗播报语音内容 fun cleanTextForTTS(text: String): String { return text @@ -394,12 +411,13 @@ class AzureTtsHelper(private val context: Context) : ITtsService { .replace(Regex("\\s+"), " ") // 多个空格合并 .trim() } + /** * 设置语音参数 */ private fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean { if (!isInitialized) return false - + try { currentRate = formatPercentage(rate) currentPitch = formatPercentage(pitch) @@ -407,28 +425,30 @@ class AzureTtsHelper(private val context: Context) : ITtsService { return true } catch (e: Exception) { FileLogger.e(TAG, "设置语音参数失败: ${e.message}") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "PARAMS_SET_FAILED", - "errorMessage" to "设置语音参数失败: ${e.message}" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "PARAMS_SET_FAILED", + "errorMessage" to "设置语音参数失败: ${e.message}" + ) + ) return false } } - + /** * 格式化百分比值 */ private fun formatPercentage(value: Int): String { return if (value >= 0) "+$value%" else "$value%" } - + /** * 生成SSML */ private fun generateSsml(rawText: String): String { // 1. 定义要静音的符号和表情符号列表 val symbolsToMute = listOf( - "#", "*", + "#", "*", "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" ) @@ -461,7 +481,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { """.trimIndent() } - + /** * 生成优化的SSML(减少复杂度提升速度) */ @@ -487,74 +507,81 @@ class AzureTtsHelper(private val context: Context) : ITtsService { """.trimIndent() } - + /** * 单次播放文本(非流式) */ override fun speakOnce(text: String): Boolean { if (!isInitialized) { FileLogger.e(TAG, "TTS引擎未初始化") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "NOT_INITIALIZED", - "errorMessage" to "TTS引擎未初始化" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + ) + ) return false } - + try { // 优化:使用简化的SSML生成 val ssml = generateOptimizedSsml(text) - + isSpeaking = true - + FileLogger.d(TAG, "开始语音合成,文本长度: ${text.length}") - + // 直接调用SpeakSsmlAsync,SDK内部已经是异步的 synthesizer?.SpeakSsmlAsync(ssml) - + // 返回true表示已开始合成请求 return true } catch (e: Exception) { isSpeaking = false FileLogger.e(TAG, "语音合成异常: ${e.message}") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "SYNTHESIS_ERROR", - "errorMessage" to "语音合成异常: ${e.message}" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "SYNTHESIS_ERROR", + "errorMessage" to "语音合成异常: ${e.message}" + ) + ) return false } } - + /** * 流式合成文本 */ override fun speakStream(text: String): Boolean { if (!isInitialized || text.isBlank()) { if (!isInitialized) { - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "NOT_INITIALIZED", - "errorMessage" to "TTS引擎未初始化" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + ) + ) } return false } - + try { // 添加新文本到缓冲区 streamBuffer.append(text) - + // 优化:大幅减少防抖时间从600ms到150ms val currentTime = System.currentTimeMillis() if (currentTime - lastSpeakTime < 150) { return true } lastSpeakTime = currentTime - + val currentText = streamBuffer.toString() //cleanTextForTTS(streamBuffer.toString()) - + // 优化:使用字符集合替代列表,提升查找效率 - val punctuationSet = setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n') - + val punctuationSet = + setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n') + // 优化:从后往前查找最后一个标点符号 var lastPunctuationIndex = -1 for (i in currentText.length - 1 downTo 0) { @@ -563,73 +590,79 @@ class AzureTtsHelper(private val context: Context) : ITtsService { break } } - + // 如果找到标点符号,则播放到该标点符号 if (lastPunctuationIndex >= 0) { // 提取要播放的文本(包含标点符号) val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1) - + // 剩余的文本保存在缓冲区中 streamBuffer.delete(0, lastPunctuationIndex + 1) - + // 播放提取的文本 return speakOnce(textToSpeak) } - + // 如果没有找到标点符号,则等待更多文本 return true } catch (e: Exception) { FileLogger.e(TAG, "流式语音合成失败: ${e.message}") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "STREAM_FAILED", - "errorMessage" to "流式语音合成失败: ${e.message}" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "STREAM_FAILED", + "errorMessage" to "流式语音合成失败: ${e.message}" + ) + ) return false } } - + /** * 播放剩余的流式文本 */ override fun flushStream(): Boolean { if (!isInitialized) { - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "NOT_INITIALIZED", - "errorMessage" to "TTS引擎未初始化" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + ) + ) return false } - + try { // 获取缓冲区中剩余的文本 val remainingText = streamBuffer.toString() - + // 清空缓冲区 streamBuffer.clear() - + // 如果缓冲区为空,直接返回成功 if (remainingText.isBlank()) { return true } - + // 播放剩余文本 return speakOnce(remainingText) } catch (e: Exception) { FileLogger.e(TAG, "刷新流式文本失败: ${e.message}") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "FLUSH_FAILED", - "errorMessage" to "刷新流式文本失败: ${e.message}" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "FLUSH_FAILED", + "errorMessage" to "刷新流式文本失败: ${e.message}" + ) + ) return false } } - + /** * 停止当前语音合成 */ override fun stop(): Boolean { if (!isInitialized) return false - + try { synthesizer?.StopSpeakingAsync() streamBuffer.clear() @@ -638,21 +671,23 @@ class AzureTtsHelper(private val context: Context) : ITtsService { return true } catch (e: Exception) { FileLogger.e(TAG, "停止语音合成失败: ${e.message}") - notifyEvent(TtsEventType.ERROR, mapOf( - "errorCode" to "STOP_FAILED", - "errorMessage" to "停止语音合成失败: ${e.message}" - )) + notifyEvent( + TtsEventType.ERROR, mapOf( + "errorCode" to "STOP_FAILED", + "errorMessage" to "停止语音合成失败: ${e.message}" + ) + ) return false } } - + /** * 释放资源 */ override fun release() { dispose() } - + /** * 释放资源(内部方法) */ @@ -660,15 +695,15 @@ class AzureTtsHelper(private val context: Context) : ITtsService { try { // 停止播放 stop() - + // 关闭合成器 synthesizer?.close() synthesizer = null - + // 关闭配置 speechConfig?.close() speechConfig = null - + // 关闭自定义音频输出流 try { customAudioOutputStream?.close() @@ -676,10 +711,10 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } catch (e: Exception) { // 忽略关闭异常 } - + // 清空流缓冲区 streamBuffer.clear() - + // 重置状态 isInitialized = false isSpeaking = false @@ -689,7 +724,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { isSpeaking = false } } - + /** * 是否正在播放 */