From 45e3a73914fae6f59d2f1cf011609cbd3891d65c Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 28 Jul 2025 20:58:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E6=AD=A3=E5=B8=B8=E7=AB=AF?= =?UTF-8?q?=E5=88=B0=E7=AB=AF=E8=AF=AD=E9=9F=B3=E5=90=88=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/bindings/initial_binding.dart | 6 +- lib/data/services/asr_service.dart | 6 + lib/data/services/ast_service.dart | 107 ++ .../speech_impl/azure_ast_service.dart | 164 +++ .../speech_impl/volcano_asr_api_service.dart | 6 + .../speech_impl/volcano_asr_service.dart | 6 + .../speech_impl/xunfei_asr_service.dart | 6 + lib/modules/login/views/login_view.dart | 38 +- .../meeting_record_controller.dart | 5 + .../controllers/translation_controller.dart | 148 +- .../yunqiinnovation/agent_service/BleAgent.kt | 4 +- .../agent_service/AgentServiceImpl.swift | 4 + .../azure_speech/android/build.gradle.kts | 1 + .../azure_speech/AzureAsrToAsr.kt | 1223 +++++++++++++++++ .../azure_speech/AzureSpeechPlugin.kt | 338 ++++- .../azure_speech/tools/RecordFile.kt | 2 +- .../yunqiinnovation/ble_service/BleConst.kt | 22 +- .../yunqiinnovation/ble_service/BleService.kt | 454 +++--- .../ble_service/BleServicePlugin.kt | 4 + .../main/kotlin/com/example/ota/OtaPlugin.kt | 3 + 20 files changed, 2336 insertions(+), 211 deletions(-) create mode 100644 lib/data/services/ast_service.dart create mode 100644 lib/data/services/speech_impl/azure_ast_service.dart create mode 100644 local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 73826786b..14d6700d5 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -3,6 +3,8 @@ import '../../../data/services/user_portrait.dart'; import '../../../data/services/location_manager.dart'; import '../../../data/services/music_manager.dart'; import '../../../data/services/navigation_manager.dart'; +import '../../../data/services/ast_service.dart'; +import '../../../data/services/speech_impl/azure_ast_service.dart'; import 'package:get/get.dart'; import '../../data/services/meeting/meeting_task_service.dart'; import '../../data/services/meeting/meeting_upload_service.dart'; @@ -22,7 +24,6 @@ class InitialBinding extends Bindings { void dependencies() { Logger.warning('InitialBinding dependencies'); - // 语言管理器(需要最先初始化) // 语言管理器(需要最先初始化) Get.lazyPut(() => LanguageManager(), fenix: true); @@ -30,6 +31,9 @@ class InitialBinding extends Bindings { Get.lazyPut(() => SpeechFactory(), fenix: true); Get.find().initialize(initialType: SpeechServiceType.azure); + // 注册 AST 服务 + Get.lazyPut(() => AzureAstService(), fenix: true); + // 火山翻译服务 Get.lazyPut(() => VolcanoTranslationService(), fenix: true); diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index e6c67563c..62928938d 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -46,6 +46,12 @@ abstract class AsrService { /// 暂停录音 Future pauseRecord(); + /// 设置音频配置 + Future setAudioConfig({ + int sampleRate = 16000, + int channels = 1, + }); + // /// 移动文件到新路径 Future moveFile(String sourcePath, String destPath); diff --git a/lib/data/services/ast_service.dart b/lib/data/services/ast_service.dart new file mode 100644 index 000000000..53671ced5 --- /dev/null +++ b/lib/data/services/ast_service.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:typed_data'; + +/// 语音识别服务接口 +abstract class AstService { + /// 支持的语言 + List get supportedLanguages; + + /// 初始化语音识别服务 + Future initialize({required List supportedLanguages}); + + /// 开始录音 + Future enableRecord(String filePath); + + /// 停止录音 + Future stopContinuousTranslation(bool isSave); + + /// 开始录音 + Future path(String filePath); +} + +/// 识别事件类型 +// enum RecognitionEventType { +// /// 最终识别结果 +// finalResult, + +// /// 中间识别结果(实时反馈) +// intermediateResult, + +// /// 音频 +// onAudio, + +// /// 会话开始 +// sessionStarted, + +// /// 会话结束 +// sessionStopped, + +// /// 识别取消 +// canceled, + +// /// 识别错误 +// error, +// } + +/// 识别事件 +// class RecognitionEvent { +// /// 事件类型 +// final RecognitionEventType type; + +// /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) +// final String text; + +// /// 检测到的语言 +// final String detectedLanguage; + +// /// 角色 +// final String role; + +// /// 原始音频 +// final Uint8List? audio; + +// /// 错误信息(仅在 error 和 canceled 类型中有效) +// final String error; + +// RecognitionEvent({ +// required this.type, +// this.text = '', +// this.detectedLanguage = '', +// this.role = '', +// this.audio, +// this.error = '', +// }); + +// /// 创建最终结果事件的快捷构造函数 +// factory RecognitionEvent.finalResult({ +// required String text, +// String detectedLanguage = '', +// }) { +// return RecognitionEvent( +// type: RecognitionEventType.finalResult, +// text: text, +// detectedLanguage: detectedLanguage, +// ); +// } + +// /// 创建错误事件的快捷构造函数 +// factory RecognitionEvent.error(String errorMessage) { +// return RecognitionEvent( +// type: RecognitionEventType.error, +// error: errorMessage, +// ); +// } + +// /// 检查是否为最终结果 +// bool get isFinalResult => type == RecognitionEventType.finalResult; + +// /// 检查是否为错误 +// bool get isError => +// type == RecognitionEventType.error || +// type == RecognitionEventType.canceled; + +// @override +// String toString() { +// return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; +// } +// } diff --git a/lib/data/services/speech_impl/azure_ast_service.dart b/lib/data/services/speech_impl/azure_ast_service.dart new file mode 100644 index 000000000..6e9a4af0a --- /dev/null +++ b/lib/data/services/speech_impl/azure_ast_service.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import '../../../data/models/appconfig.dart'; +import 'package:flutter/services.dart'; +import '../../../core/utils/logger.dart'; +import 'package:get/get.dart'; +import '../ast_service.dart'; + +/// 音频源类型 +enum AudioSourceType { + microphone, // 使用设备麦克风 + external // 使用外部提供的音频数据 +} + +/// Azure 语音识别服务 +/// +/// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口 +class AzureAstService extends GetxService implements AstService { + static final AzureAstService to = Get.put(AzureAstService()); + static const MethodChannel _channel = MethodChannel('azure_speech/ast'); + static const EventChannel _eventChannel = + EventChannel('azure_speech/ast_events'); + // final GetStorage _storage = GetStorage(); + bool _isInitialized = false; + late final String _subscriptionKey; + late final String _serviceRegion; + late String _baseUrl; + final String _endpoint = '/'; // 修改为根路径 + late final String _accessKey; + late final String _secretKey; + late final String _region; + late final String _service; + + final List _defaultSupportedLanguages = ['zh-CN', 'en-US']; + @override + List get supportedLanguages => _defaultSupportedLanguages; + + // 连续识别相关 + // bool _isContinuousRecognitionActive = false; + // StreamController? _eventStreamController; + // StreamSubscription? _eventSubscription; + + // 最新的识别结果 + String _latestRecognizedText = ''; + String get latestRecognizedText => _latestRecognizedText; + + // 最新检测到的语言 + String _latestDetectedLanguage = ''; + String get latestDetectedLanguage => _latestDetectedLanguage; + + // 当前音频源类型 + AudioSourceType _audioSourceType = AudioSourceType.microphone; + + AzureAstService() { + _loadConfig(); + } + + /// 从环境变量加载配置 + void _loadConfig() { + // final _env = _storage.read("ENV") as Map; + _subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; + _serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? ''; + _accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? ''; + _secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? ''; + _region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? 'cn-north-1'; + _service = 'translate'; + _baseUrl = 'https://translate.volcengineapi.com'; + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { + throw Exception( + '未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); + } + } + + @override + Future enableRecord(String filePath) async { + try { + final bool result = await _channel.invokeMethod('enableRecord', { + 'filePath': filePath, + }); + + return result; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future path(String filePath) async { + try { + final bool result = await _channel.invokeMethod('path', { + 'filePath': filePath, + }); + + return result; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future stopContinuousTranslation(bool isSave) async { + try { + final bool result = + await _channel.invokeMethod('stopContinuousTranslation', { + 'isSave': isSave, + }); + + return result; + } catch (e) { + Logger.error('停止录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future initialize({ + List? supportedLanguages, + bool useExternalAudio = false, + bool useEchoCancellation = false, + }) async { + try { + final List languages = + supportedLanguages ?? _defaultSupportedLanguages; +//底层会初始化前释放 + // // 检查是否需要重新初始化 + // if (_isInitialized) { + // await dispose(); + // } + + // 设置音频源类型 + _audioSourceType = useExternalAudio + ? AudioSourceType.external + : AudioSourceType.microphone; +// Future initialize({ +// required String subscriptionKey, +// required String region, +// required List supportedLanguages, +// required String audioSourceType, +// required String translationAccessKey, +// required String translationSecretKey, +// String translationRegion = 'cn-north-1', +// }); + + final bool result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'region': _serviceRegion, + 'supportedLanguages': languages, + 'audioSourceType': _audioSourceType.toString().split('.').last, + 'translationAccessKey': _accessKey, + 'translationSecretKey': _secretKey, + 'translationRegion': _region, + }); + + _isInitialized = result; + Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); + return result; + } catch (e) { + Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); + _isInitialized = false; + 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 dabf9e6e0..32844d1bb 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -865,4 +865,10 @@ class VolcanoAsrApiService implements AsrService { // TODO: implement startContinuousRecognition throw UnimplementedError(); } + + @override + Future setAudioConfig({int sampleRate = 16000, int channels = 1}) { + // TODO: implement setAudioConfig + 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 b5e1493ec..412656ddd 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -400,4 +400,10 @@ class VolcanoAsrService extends GetxService implements AsrService { // TODO: implement startContinuousRecognition throw UnimplementedError(); } + + @override + Future setAudioConfig({int sampleRate = 16000, int channels = 1}) { + // TODO: implement setAudioConfig + 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 959f98859..cc94063e8 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -324,4 +324,10 @@ class XunfeiAsrService extends GetxService implements AsrService { // TODO: implement startContinuousRecognition throw UnimplementedError(); } + + @override + Future setAudioConfig({int sampleRate = 16000, int channels = 1}) { + // TODO: implement setAudioConfig + throw UnimplementedError(); + } } diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 4b46914e8..9bc91fd66 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -232,28 +232,26 @@ class LoginView extends GetView { margin: EdgeInsets.symmetric(horizontal: 8.w), ), Expanded( - child: GetBuilder( - builder: (controller) => TextField( - controller: controller.contactController, - decoration: InputDecoration( - hintText: controller.selectedCountryFlag == '✉️' - ? 'email'.tr // 邮箱 - : 'phoneNumber'.tr, // 手机号码 - hintStyle: TextStyle( - fontSize: 14.sp, - color: isDarkMode - ? Colors.grey[500] - : Colors.black38, // 调整提示文字颜色 - ), - border: InputBorder.none, - fillColor: Colors.transparent, - filled: true, - focusedBorder: InputBorder.none, - ), - style: TextStyle( + child: TextField( + controller: controller.contactController, + decoration: InputDecoration( + hintText: controller.selectedCountryFlag == '✉️' + ? 'email'.tr // 邮箱 + : 'phoneNumber'.tr, // 手机号码 + hintStyle: TextStyle( fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, + color: isDarkMode + ? Colors.grey[500] + : Colors.black38, // 调整提示文字颜色 ), + border: InputBorder.none, + fillColor: Colors.transparent, + filled: true, + focusedBorder: InputBorder.none, + ), + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, ), ), ), diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 19ada35c7..26760d3a8 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -256,6 +256,7 @@ class MeetingRecordController extends GetxController // Start audio recording await _asrService.enableRecord("${dir.path}/$fullFileName.wav"); + isRecording.value = true; // Update state fileName.value = fullFileName; @@ -271,12 +272,16 @@ class MeetingRecordController extends GetxController void _startHardwareServices() { switch (audioType.value) { case 0: + // Start audio recording + _asrService.setAudioConfig(sampleRate: 16000, channels: 1); _bleManager.openEncoder(); break; case 1: + _asrService.setAudioConfig(sampleRate: 16000, channels: 1); _bleManager.openDecoder(); break; case 2: + _asrService.setAudioConfig(sampleRate: 16000, channels: 2); _bleManager.openA2DPDecoder(); break; } diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index a58ddb678..1627b595a 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../../data/services/ast_service.dart'; import '../../../data/services/music_manager.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; @@ -34,6 +35,8 @@ class TranslationController extends GetxController { final VolcanoTranslationService _translationService = Get.find(); final TtsService _ttsService = Get.find(); + + final AstService _astService = Get.find(); final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); // 蓝牙服务 @@ -363,6 +366,144 @@ class TranslationController extends GetxController { } } + // 初始化通话模式的语音翻译服务 + Future _initializeCallModeTranslationService() async { + try { + Logger.info('开始初始化通话模式语音翻译服务'); + // 为通话模式配置特殊的ASR设置 + final List callModeLanguages = [ + sourceLanguageCode.value, + targetLanguageCode.value + ]; +// Future initialize({ +// required String subscriptionKey, +// required String region, +// required List supportedLanguages, +// required String audioSourceType, +// required String translationAccessKey, +// required String translationSecretKey, +// String translationRegion = 'cn-north-1', +// }); + // 重新初始化ASR服务以支持通话音频源 + await _astService.initialize(supportedLanguages: callModeLanguages); + await _astService.path("${dir.path}/8_mic.wav"); + // 配置实时翻译参数 + await _configureCallModeTranslation(); + + Logger.info('通话模式语音翻译服务初始化完成'); + return; + } catch (e) { + Logger.error('通话模式语音翻译服务初始化失败: ${e.toString()}'); + } + } + + // 配置通话模式的翻译参数 + Future _configureCallModeTranslation() async { + try { + // 设置通话模式的特殊配置 + // 1. 更短的识别超时时间,适应通话场景 + // 2. 更高的识别敏感度 + // 3. 噪声抑制优化 + + // 这里可以调用ASR服务的特殊配置方法 + // await _asrService.configureForCallMode( + // endSilenceTimeout: 200, // 更短的静音超时 + // noiseReduction: true, // 启用噪声抑制 + // echoCancellation: true, // 启用回声消除 + // ); + + Logger.info('通话模式翻译参数配置完成'); + } catch (e) { + Logger.error('通话模式翻译参数配置失败: ${e.toString()}'); + } + } + + // // 处理通话模式的翻译结果 + // Future _handleCallModeTranslation(String sourceText) async { + // try { + // // 在通话模式下,翻译结果可能需要特殊处理 + // // 例如:发送到蓝牙设备、显示在特定UI等 + + // final translationResult = await _translationService.translateText( + // text: sourceText, + // sourceLanguageCode: sourceLanguageCode.value, + // targetLanguageCode: targetLanguageCode.value, + // ); + + // if (translationResult != null && translationResult.isNotEmpty) { + // // 通话模式下的特殊处理 + // await _processCallModeTranslationResult(sourceText, translationResult); + // } + // } catch (e) { + // Logger.error('通话模式翻译处理失败: ${e.toString()}'); + // } + // } + + // // 处理通话模式的翻译结果 + // Future _processCallModeTranslationResult(String sourceText, String translatedText) async { + // try { + // // 1. 更新UI显示 + // final newItem = TranslationItem( + // sourceText: sourceText, + // translatedText: translatedText, + // sourceLanguageCode: sourceLanguageCode.value, + // targetLanguageCode: targetLanguageCode.value, + // timestamp: DateTime.now(), + // sessionId: currentSessionId ?? DateTime.now().millisecondsSinceEpoch.toString(), + // isFirstInSession: translationHistory.isEmpty, + // isIntermediate: false, + // ); + + // translationHistory.add(newItem); + // translationHistory.refresh(); + // _scrollToBottom(); + // saveTranslationHistory(); + + // // 2. 通话模式下可能需要将翻译结果发送到蓝牙设备 + // // 或者通过其他方式传输给通话对方 + // await _sendTranslationToCallParty(translatedText); + + // // 3. 记录统计信息 + // _recordCallModeUsageStats(sourceText, translatedText); + + // Logger.info('通话模式翻译结果处理完成: $sourceText -> $translatedText'); + // } catch (e) { + // Logger.error('通话模式翻译结果处理失败: ${e.toString()}'); + // } + // } + + // // 将翻译结果发送给通话对方 + // Future _sendTranslationToCallParty(String translatedText) async { + // try { + // // 这里可以实现将翻译结果发送给通话对方的逻辑 + // // 例如:通过蓝牙、网络等方式 + + // // 示例:通过蓝牙发送 + // await bleManager.sendTranslationResult(translatedText); + + // Logger.info('翻译结果已发送给通话对方: $translatedText'); + // } catch (e) { + // Logger.error('发送翻译结果失败: ${e.toString()}'); + // } + // } + + // // 记录通话模式的使用统计 + // void _recordCallModeUsageStats(String sourceText, String translatedText) { + // try { + // _usageService.recordTranslationApiCall( + // sourceText: sourceText, + // targetText: translatedText, + // sourceLanguage: _languageManager.getChineseNameByAsrCode(sourceLanguageCode.value) ?? '未知', + // targetLanguage: _languageManager.getChineseNameByAsrCode(targetLanguageCode.value) ?? '未知', + // mode: 'call', // 明确标记为通话模式 + // ); + + // Logger.info('通话模式使用统计已记录'); + // } catch (e) { + // Logger.error('记录通话模式统计失败: ${e.toString()}'); + // } + // } + @override void onClose() { stopRecognition(); @@ -410,6 +551,8 @@ class TranslationController extends GetxController { final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); await _asrService.enableRecord( "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + await _astService.enableRecord( + "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); } Future stopRecording() async { @@ -475,6 +618,9 @@ class TranslationController extends GetxController { _audioSourceType = true; isTtsEnabled.value = false; Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); + + // 初始化语音翻译服务 + await _initializeCallModeTranslationService(); } else { // 开始连续语音识别 _audioSourceType = false; @@ -526,7 +672,7 @@ class TranslationController extends GetxController { try { await _asrService.stopContinuousRecognition(); - + await _astService.stopContinuousTranslation(true); // 停止ASR活跃时长计时 _stopAsrActiveTracking(); diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index a62e67ffd..dc8a689c4 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -135,7 +135,9 @@ Log.d(TAG, "手动启动语音识别: ") AgentService.pushAudioData(data) // 可选:处理音频数据 } - + override fun onAudioDataReceived1(data: ByteArray) { + + } /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别 diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 753dd87c4..4e1c436bb 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -1166,6 +1166,10 @@ extension AgentServiceImpl: BleService.Callback { pushAudioData(data) } + func onAudioDataReceived1(data: Data) { + + } + func onWakeupSignalReceived() { stopTts() diff --git a/local_plugins/azure_speech/android/build.gradle.kts b/local_plugins/azure_speech/android/build.gradle.kts index 4d6a58288..c77148988 100644 --- a/local_plugins/azure_speech/android/build.gradle.kts +++ b/local_plugins/azure_speech/android/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { // 添加Microsoft语音SDK implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0") implementation(project(":speech")) + implementation("com.squareup.okhttp3:okhttp:4.12.0") add("compileOnly", project(":ble_service")) } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt new file mode 100644 index 000000000..ad89b9409 --- /dev/null +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt @@ -0,0 +1,1223 @@ +package com.yunqiinnovation.azure_speech + +import android.content.Context +import android.media.AudioManager +import android.util.Log +import com.microsoft.cognitiveservices.speech.* +import com.microsoft.cognitiveservices.speech.audio.* +import kotlinx.coroutines.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import kotlin.coroutines.CoroutineContext +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import org.json.JSONObject +import org.json.JSONArray +import java.net.URLEncoder +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import com.yunqiinnovation.azure_speech.tools.RecordFile +/** + * 整合的语音翻译服务 + * 集成ASR语音识别、翻译服务和TTS语音合成 + * 实现音频输入 -> 语音识别 -> 翻译 -> 语音合成 -> 音频输出的完整流程 + */ +class IntegratedSpeechTranslationService( + private val context: Context +) : CoroutineScope { + + companion object { + private const val TAG = "IntegratedSpeechService" + + // 音频配置常量 + private const val SAMPLE_RATE = 16000 + private const val CHANNELS = 1 + private const val BITS_PER_SAMPLE = 16 + private const val BUFFER_SIZE = 4096 + + // 超时配置 + private const val END_SILENCE_TIMEOUT = "300" + private const val SEGMENTATION_SILENCE_TIMEOUT = "300" + private const val INITIAL_SILENCE_TIMEOUT = "200" + } + + // 协程上下文 + private val job = SupervisorJob() + override val coroutineContext: CoroutineContext = Dispatchers.Main + job + + // Azure服务组件 + private var speechConfig: SpeechConfig? = null + private var recognizer: SpeechRecognizer? = null + private var synthesizer: SpeechSynthesizer? = null + + // 翻译服务 + private var translationService: TranslationServiceInterface? = null + + // 音频处理 + private var audioProcessor: AudioProcessor? = null + private var audioConfig: AudioConfig? = null + // 录音文件处理 + var recordfile: RecordFile? = null + var filePath: String? = null + // 配置管理 + private var serviceConfig = ServiceConfiguration() + + // 状态管理 + private val serviceState = ServiceState() + + // 事件回调 + private var eventCallback: ServiceEventCallback? = null + + // 语音映射缓存 + private val voiceCache = ConcurrentHashMap() + + /** + * 服务配置类 + */ + data class ServiceConfiguration( + var sourceLanguage: String = "zh-CN", + var targetLanguage: String = "en-US", + var currentVoice: String = "en-US-AriaNeural", + var speechRate: String = "0%", + var speechPitch: String = "0%", + var speechVolume: String = "100%", + var enableContinuousRecognition: Boolean = true, + var enableAutoLanguageDetection: Boolean = false, + var maxRetryAttempts: Int = 3, + var translationTimeout: Long = 10000L + ) + + /** + * 服务状态类 + */ + data class ServiceState( + val isInitialized: AtomicBoolean = AtomicBoolean(false), + val isRecognizing: AtomicBoolean = AtomicBoolean(false), + val isSynthesizing: AtomicBoolean = AtomicBoolean(false), + val isTranslating: AtomicBoolean = AtomicBoolean(false) + ) + + /** + * 服务事件回调接口 + */ + interface ServiceEventCallback { + fun onServiceInitialized() + fun onRecognizing(text: String, language: String, confidence: Float) + fun onRecognized(text: String, language: String, confidence: Float) + fun onTranslated(originalText: String, translatedText: String, targetLanguage: String) + fun onTranslationStarted(text: String) + fun onTranslationFailed(text: String, error: String) + fun onSynthesisStarted(text: String) + fun onSynthesisCompleted(text: String) + fun onSynthesisFailed(text: String, error: String) + fun onSynthesisProgress(text: String, progress: Float) + fun onRecognitionStarted() + fun onRecognitionStopped() + fun onStateChanged(component: String, isActive: Boolean) + fun onError(component: String, error: String) + } + + /** + * 翻译服务接口 + */ + interface TranslationServiceInterface { + suspend fun initialize(config: Map): Boolean + suspend fun translateText( + text: String, + sourceLanguage: String, + targetLanguage: String + ): TranslationResult + + fun dispose() + } + + /** + * 翻译结果类 + */ + data class TranslationResult( + val success: Boolean, + val translatedText: String? = null, + val error: String? = null, + val confidence: Float = 0f + ) + + /** + * 初始化整合服务 + */ + suspend fun initialize( + azureConfig: AzureConfiguration, + translationConfig: TranslationConfiguration, + serviceConfig: ServiceConfiguration? = null, + callback: ServiceEventCallback + ): Boolean = withContext(Dispatchers.IO) { + try { + this@IntegratedSpeechTranslationService.eventCallback = callback + serviceConfig?.let { this@IntegratedSpeechTranslationService.serviceConfig = it } + + Log.d(TAG, "开始初始化整合服务,azureConfig:${azureConfig},translationConfig:${translationConfig}") + + // 初始化Azure语音服务 + if (!initializeAzureServices(azureConfig)) { + callback.onError("Initialization", "Azure服务初始化失败") + return@withContext false + } + + // 初始化翻译服务 + if (!initializeTranslationService(translationConfig)) { + callback.onError("Initialization", "翻译服务初始化失败") + return@withContext false + } + + // 初始化音频处理器 + initializeAudioProcessor() + + // 设置语音识别器 + setupSpeechRecognizer() + + // 设置语音合成器 + setupSpeechSynthesizer() + + serviceState.isInitialized.set(true) + Log.d(TAG, "整合服务初始化成功") + + withContext(Dispatchers.Main) { + callback.onServiceInitialized() + } +startContinuousTranslation() + recordfile = RecordFile; + return@withContext true + } catch (e: Exception) { + Log.e(TAG, "初始化失败", e) + withContext(Dispatchers.Main) { + callback.onError( + "Initialization", + "初始化失败: ${e.message}" + ) + } + return@withContext false + } + } + + /** + * 初始化Azure服务 + */ + private suspend fun initializeAzureServices(config: AzureConfiguration): Boolean { + return try { + Log.d(TAG, "开始初始化Azure服务${config.subscriptionKey},${config.region}") + speechConfig = + SpeechConfig.fromSubscription(config.subscriptionKey, config.region).apply { + speechRecognitionLanguage = serviceConfig.sourceLanguage + setSpeechSynthesisVoiceName(getVoiceForLanguage(serviceConfig.targetLanguage)) + setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm) + + // 优化配置 + setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", END_SILENCE_TIMEOUT) + setProperty("Speech_SegmentationSilenceTimeoutMs", SEGMENTATION_SILENCE_TIMEOUT) + setProperty( + "SpeechServiceConnection_InitialSilenceTimeoutMs", + INITIAL_SILENCE_TIMEOUT + ) + setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE") + + // 启用详细结果 + setProperty("SpeechServiceResponse_RequestDetailedResultTrueFalse", "true") + + // 自动语言检测 + if (serviceConfig.enableAutoLanguageDetection) { + setProperty("SpeechServiceConnection_LanguageIdMode", "Continuous") + } + } + + Log.d(TAG, "Azure语音服务配置完成") + true + } catch (e: Exception) { + Log.e(TAG, "Azure服务初始化失败", e) + false + } + } + + /** + * 初始化翻译服务 + */ + private suspend fun initializeTranslationService(config: TranslationConfiguration): Boolean { + return try { + translationService = VolcanoTranslationServiceImpl().apply { + initialize(config.toConfigMap()) + } + Log.d(TAG, "翻译服务初始化完成") + true + } catch (e: Exception) { + Log.e(TAG, "翻译服务初始化失败", e) + false + } + } + + /** + * 初始化音频处理器 + */ + private fun initializeAudioProcessor() { + audioProcessor = AudioProcessor().apply { + initialize() + } + audioConfig = AudioConfig.fromStreamInput(audioProcessor?.getAudioStream()) + Log.d(TAG, "音频处理器初始化完成") + } + + /** + * 设置语音识别器 + */ + private fun setupSpeechRecognizer() { + recognizer?.close() + recognizer = SpeechRecognizer(speechConfig, audioConfig).apply { + + // 识别中事件 + recognizing.addEventListener { _, event -> + if (event.result.text.isNotEmpty()) { + val confidence = extractConfidence(event.result) + Log.d(TAG, "识别中事件: ${event.result.text}") + eventCallback?.onRecognizing( + event.result.text, + serviceConfig.sourceLanguage, + confidence + ) + } + } + + // 识别完成事件 + recognized.addEventListener { _, event -> + when (event.result.reason) { + ResultReason.RecognizedSpeech -> { + if (event.result.text.isNotEmpty()) { + val confidence = extractConfidence(event.result) + eventCallback?.onRecognized( + event.result.text, + serviceConfig.sourceLanguage, + confidence + ) + Log.d(TAG, "识别完成事件: ${event.result.text}") + // 触发翻译流程 + launch { + processTranslationAndSynthesis(event.result.text) + } + } + } + + ResultReason.NoMatch -> { + Log.d(TAG, "未识别到语音") + } + + else -> { + Log.w(TAG, "识别结果: ${event.result.reason}") + } + } + } + + // 会话开始事件 + sessionStarted.addEventListener { _, _ -> + Log.d(TAG, "会话开始事件") + serviceState.isRecognizing.set(true) + eventCallback?.onRecognitionStarted() + eventCallback?.onStateChanged("Recognition", true) + } + + // 会话停止事件 + sessionStopped.addEventListener { _, _ -> + Log.d(TAG, "会话停止事件") + serviceState.isRecognizing.set(false) + eventCallback?.onRecognitionStopped() + eventCallback?.onStateChanged("Recognition", false) + } + + // 取消事件 + canceled.addEventListener { _, event -> + Log.d(TAG, "取消事件") + serviceState.isRecognizing.set(false) + val errorDetails = event.errorDetails ?: "未知错误" + eventCallback?.onError("Recognition", "识别被取消: $errorDetails") + eventCallback?.onStateChanged("Recognition", false) + } + } + + Log.d(TAG, "语音识别器设置完成") + } + + /** + * 设置语音合成器 + */ + private fun setupSpeechSynthesizer() { + synthesizer?.close() + synthesizer = + SpeechSynthesizer(speechConfig, AudioConfig.fromDefaultSpeakerOutput()).apply { + + // 合成开始事件 + SynthesisStarted.addEventListener { _, _ -> + Log.d(TAG, "合成开始事件") + serviceState.isSynthesizing.set(true) + eventCallback?.onStateChanged("Synthesis", true) + } + + // 合成进行中事件 + Synthesizing.addEventListener { _, event -> + Log.d(TAG, "合成进行中事件") + // 计算进度(简化版) + val progress = 0.5f // 实际应用中可以根据音频数据计算 + eventCallback?.onSynthesisProgress("", progress) + } + + // 合成完成事件 + SynthesisCompleted.addEventListener { _, event -> + Log.d(TAG, "合成完成事件") + serviceState.isSynthesizing.set(false) + eventCallback?.onSynthesisCompleted("") + eventCallback?.onStateChanged("Synthesis", false) + } + + // 合成取消事件 + SynthesisCanceled.addEventListener { _, event -> + Log.d(TAG, "合成取消事件") + serviceState.isSynthesizing.set(false) + val reason = event.result.reason + eventCallback?.onError("Synthesis", "语音合成取消: $reason") + eventCallback?.onStateChanged("Synthesis", false) + } + } + + Log.d(TAG, "语音合成器设置完成") + } + + /** + * 开始连续语音翻译 + */ + suspend fun startContinuousTranslation(): Boolean { + if (!serviceState.isInitialized.get()) { + eventCallback?.onError("Service", "服务未初始化") + return false + } + + if (serviceState.isRecognizing.get()) { + Log.w(TAG, "语音识别已在进行中") + return true + } + + return try { + + audioProcessor?.startRecording() + + if (serviceConfig.enableContinuousRecognition) { + Log.d(TAG, "startContinuousTranslation") + recognizer?.startContinuousRecognitionAsync() + } + + Log.d(TAG, "开始连续语音翻译") + true + } catch (e: Exception) { + Log.e(TAG, "启动连续翻译失败", e) + eventCallback?.onError("Service", "启动失败: ${e.message}") + false + } + } + fun enableRecord(filePath: String) { + Log.i(TAG, "开启录音:") + + recordfile!!.closeFile(true) + + + recordfile!!.creatingFiles(filePath) + + } + + /** + * 停止连续语音翻译 + */ + fun stopContinuousTranslation() { + try { + recognizer?.stopContinuousRecognitionAsync() + audioProcessor?.stopRecording() + recordfile!!.closeFile(true) + // 等待当前处理完成 + while (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) { + Thread.sleep(100) + } + + Log.d(TAG, "停止连续语音翻译") + } catch (e: Exception) { + Log.e(TAG, "停止翻译失败", e) + eventCallback?.onError("Service", "停止失败: ${e.message}") + } + } + + /** + * 处理翻译和合成流程 + */ + private suspend fun processTranslationAndSynthesis(text: String) { + if (serviceState.isTranslating.get()) { + Log.w(TAG, "翻译正在进行中,跳过当前请求") + return + } + + serviceState.isTranslating.set(true) + eventCallback?.onTranslationStarted(text) + eventCallback?.onStateChanged("Translation", true) + + try { + val translationResult = withTimeout(serviceConfig.translationTimeout) { + translationService?.translateText( + text = text, + sourceLanguage = serviceConfig.sourceLanguage, + targetLanguage = serviceConfig.targetLanguage + ) + } + + serviceState.isTranslating.set(false) + eventCallback?.onStateChanged("Translation", false) + + when { + translationResult?.success == true && !translationResult.translatedText.isNullOrEmpty() -> { + eventCallback?.onTranslated( + text, + translationResult.translatedText, + serviceConfig.targetLanguage + ) + synthesizeText(translationResult.translatedText) + } + + translationResult?.error != null -> { + eventCallback?.onTranslationFailed(text, translationResult.error) + } + + else -> { + eventCallback?.onTranslationFailed(text, "翻译服务返回空结果") + } + } + + } catch (e: TimeoutCancellationException) { + serviceState.isTranslating.set(false) + eventCallback?.onStateChanged("Translation", false) + eventCallback?.onTranslationFailed(text, "翻译超时") + } catch (e: Exception) { + serviceState.isTranslating.set(false) + eventCallback?.onStateChanged("Translation", false) + eventCallback?.onTranslationFailed(text, "翻译异常: ${e.message}") + } + } + + /** + * 语音合成 + */ + private suspend fun synthesizeText(text: String) { + if (serviceState.isSynthesizing.get()) { + Log.w(TAG, "语音合成正在进行中") + return + } + + try { + Log.d(TAG, "开始语音合成: $text") + serviceState.isSynthesizing.set(true) + eventCallback?.onSynthesisStarted(text) + + val ssml = generateOptimizedSsml(text) + + // 使用同步方法确保合成完成后再播放 + val result = synthesizer?.SpeakSsml(ssml) + + if (result?.reason == ResultReason.SynthesizingAudioCompleted) { + Log.d(TAG, "语音合成成功,音频已播放") + eventCallback?.onSynthesisCompleted(text) + } else { + Log.e(TAG, "语音合成失败: ${result?.reason}") + eventCallback?.onSynthesisFailed(text, "合成失败: ${result?.reason}") + } + + } catch (e: Exception) { + Log.e(TAG, "语音合成失败", e) + eventCallback?.onSynthesisFailed(text, "合成失败: ${e.message}") + } finally { + serviceState.isSynthesizing.set(false) + } + } + + /** + * 生成优化的SSML + */ + private fun generateOptimizedSsml(text: String): String { + val cleanText = cleanTextForSynthesis(text) + val escapedText = escapeXmlText(cleanText) + + return """ + + + + $escapedText + + + + """.trimIndent() + } + + /** + * 清理文本用于合成 + */ + private fun cleanTextForSynthesis(text: String): String { + return text + .replace(Regex("https?://[^\\s]+"), "") // 移除URL + .replace(Regex("[\\p{So}\\p{Sk}]"), "") // 移除表情符号 + .replace(Regex("\\s+"), " ") // 合并多个空格 + .trim() + } + + /** + * XML文本转义 + */ + private fun escapeXmlText(text: String): String { + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + } + + /** + * 根据语言获取对应的语音 + */ + private fun getVoiceForLanguage(language: String): String { + return voiceCache.getOrPut(language) { + when (language) { + "zh-CN" -> "zh-CN-XiaoxiaoNeural" + "zh-TW" -> "zh-TW-HsiaoChenNeural" + "en-US" -> "en-US-AriaNeural" + "en-GB" -> "en-GB-SoniaNeural" + "ja-JP" -> "ja-JP-NanamiNeural" + "ko-KR" -> "ko-KR-SunHiNeural" + "fr-FR" -> "fr-FR-DeniseNeural" + "de-DE" -> "de-DE-KatjaNeural" + "es-ES" -> "es-ES-ElviraNeural" + "ru-RU" -> "ru-RU-SvetlanaNeural" + "ar-SA" -> "ar-SA-ZariyahNeural" + "pt-BR" -> "pt-BR-FranciscaNeural" + "it-IT" -> "it-IT-ElsaNeural" + else -> "en-US-AriaNeural" + }.also { + serviceConfig.currentVoice = it + } + } + } + + /** + * 提取识别置信度 + */ + private fun extractConfidence(result: SpeechRecognitionResult): Float { + return try { + // 从结果中提取置信度信息 + // 这里需要根据实际的Azure SDK API API来实现 + 0.8f // 默认值 + } catch (e: Exception) { + 0.5f + } + } + + /** + * 更新服务配置 + */ + fun updateConfiguration(newConfig: ServiceConfiguration) { + val needsRecognizerUpdate = newConfig.sourceLanguage != serviceConfig.sourceLanguage || + newConfig.enableAutoLanguageDetection != serviceConfig.enableAutoLanguageDetection + + val needsSynthesizerUpdate = newConfig.targetLanguage != serviceConfig.targetLanguage || + newConfig.speechRate != serviceConfig.speechRate || + newConfig.speechPitch != serviceConfig.speechPitch || + newConfig.speechVolume != serviceConfig.speechVolume + + serviceConfig = newConfig + + if (needsRecognizerUpdate) { + speechConfig?.speechRecognitionLanguage = serviceConfig.sourceLanguage + setupSpeechRecognizer() + } + + if (needsSynthesizerUpdate) { + speechConfig?.setSpeechSynthesisVoiceName(getVoiceForLanguage(serviceConfig.targetLanguage)) + setupSpeechSynthesizer() + } + + Log.d(TAG, "服务配置已更新") + } + + /** + * 设置音频输出设备 + */ +fun setAudioOutputDevice(device: com.deep_voice.speech.tts.AudioOutputDevice) { + try { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + audioManager?.let { manager -> + when (device) { + com.deep_voice.speech.tts.AudioOutputDevice.SPEAKER -> { + manager.mode = AudioManager.MODE_IN_COMMUNICATION + manager.isSpeakerphoneOn = true + } + + com.deep_voice.speech.tts.AudioOutputDevice.HEADPHONES -> { + manager.mode = AudioManager.MODE_NORMAL + manager.isSpeakerphoneOn = false + } + + com.deep_voice.speech.tts.AudioOutputDevice.DEFAULT -> { + manager.mode = AudioManager.MODE_NORMAL + manager.isSpeakerphoneOn = false + } + } + } + Log.d(TAG, "音频输出设备设置为: $device") + } catch (e: Exception) { + Log.e(TAG, "设置音频输出设备失败", e) + eventCallback?.onError("Audio", "设置音频输出设备失败: ${e.message}") + } +} + + /** + * 切换语言对 + */ + fun switchLanguages() { + val newConfig = serviceConfig.copy( + sourceLanguage = serviceConfig.targetLanguage, + targetLanguage = serviceConfig.sourceLanguage + ) + updateConfiguration(newConfig) + Log.d(TAG, "语言对已切换: ${newConfig.sourceLanguage} <-> ${newConfig.targetLanguage}") + } + + /** + * 推送外部音频数据 + */ + fun pushAudioData(audioData: ByteArray) { + audioProcessor?.pushAudioData(audioData) + } + + /** + * 获取服务状态 + */ + fun getServiceStatus(): Map { + return mapOf( + "initialized" to serviceState.isInitialized.get(), + "recognizing" to serviceState.isRecognizing.get(), + "translating" to serviceState.isTranslating.get(), + "synthesizing" to serviceState.isSynthesizing.get() + ) + } + + /** + * 释放资源 + */ + fun dispose() { + try { + // 停止所有活动 + launch { + stopContinuousTranslation() + } + + // 释放Azure资源 + recognizer?.close() + synthesizer?.close() + speechConfig?.close() + + // 释放音频处理器 + audioProcessor?.dispose() + + // 释放翻译服务 + translationService?.dispose() + + // 取消协程 + job.cancel() + + // 清理缓存 + voiceCache.clear() + + serviceState.isInitialized.set(false) + + Log.d(TAG, "服务资源已释放") + } catch (e: Exception) { + Log.e(TAG, "释放资源失败", e) + } + } + + /** + * 音频处理器类 + */ + inner class AudioProcessor { + private var pushAudioStream: PushAudioInputStream? = null + private val audioQueue = LinkedBlockingQueue() + private val isRunning = AtomicBoolean(false) + private var processingThread: Thread? = null + + fun initialize() { + val format = AudioStreamFormat.getWaveFormatPCM( + SAMPLE_RATE.toLong(), + BITS_PER_SAMPLE.toShort(), + CHANNELS.toShort() + ) + pushAudioStream = AudioInputStream.createPushStream(format) + isRunning.set(true) + startProcessingThread() + } + + private fun startProcessingThread() { + processingThread = Thread { + while (isRunning.get()) { + try { + val audioData = + audioQueue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS) + audioData?.let { + Log.d(TAG, "音频处理: ${it}") + pushAudioStream?.write(it) + recordfile?.saveAudioDataToWav(it) + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + break + } catch (e: Exception) { + Log.e(TAG, "音频处理异常", e) + } + } + }.apply { + name = "AudioProcessingThread" + start() + } + } + + fun getAudioStream(): PushAudioInputStream? = pushAudioStream + + fun startRecording() { + Log.d(TAG, "开始音频录制") + } + + fun stopRecording() { + Log.d(TAG, "停止音频录制") + } + + fun pushAudioData(audioData: ByteArray) { + if (isRunning.get()) { + audioQueue.offer(audioData) + } + } + + fun dispose() { + isRunning.set(false) + processingThread?.interrupt() + pushAudioStream?.close() + audioQueue.clear() + } + } +} + +/** + * Azure配置类 + */ +data class AzureConfiguration( + val subscriptionKey: String, + val region: String +) + +/** + * 翻译配置类 + */ +data class TranslationConfiguration( + val accessKey: String, + val secretKey: String, + val region: String = "cn-north-1" +) { + fun toConfigMap(): Map { + return mapOf( + "accessKey" to accessKey, + "secretKey" to secretKey, + "region" to region + ) + } +} + + + +/** + * 火山翻译服务实现 + */ +class VolcanoTranslationServiceImpl : + IntegratedSpeechTranslationService.TranslationServiceInterface { + companion object { + private const val TAG = "VolcanoTranslationService" + private const val BASE_URL = "https://translate.volcengineapi.com" + private const val ENDPOINT = "/" + private const val SERVICE = "translate" + private const val VERSION = "2020-06-01" + private const val ACTION = "TranslateText" + private const val ALGORITHM = "HMAC-SHA256" + } + + private var isInitialized = false + private var accessKey: String = "" + private var secretKey: String = "" + private var region: String = "cn-north-1" + private var maxRetryAttempts: Int = 3 + private var timeout: Long = 10000L + + // HTTP客户端 + private val httpClient = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + // 语言代码映射 + private val languageCodeMap = mapOf( + "zh-CN" to "zh", + "zh-TW" to "zh-Hant", + "en-US" to "en", + "en-GB" to "en", + "ja-JP" to "ja", + "ko-KR" to "ko", + "fr-FR" to "fr", + "de-DE" to "de", + "es-ES" to "es", + "ru-RU" to "ru", + "ar-SA" to "ar", + "pt-BR" to "pt", + "it-IT" to "it", + "th-TH" to "th", + "vi-VN" to "vi", + "hi-IN" to "hi" + ) + + override suspend fun initialize(config: Map): Boolean { + return try { + accessKey = config["accessKey"] ?: "" + secretKey = config["secretKey"] ?: "" + region = config["region"] ?: "cn-north-1" + maxRetryAttempts = config["maxRetryAttempts"]?.toIntOrNull() ?: 3 + timeout = config["timeout"]?.toLongOrNull() ?: 10000L + + if (accessKey.isEmpty() || secretKey.isEmpty()) { + Log.e(TAG, "缺少必要的API密钥") + return false + } + + isInitialized = true + Log.d(TAG, "火山翻译服务初始化成功") + true + } catch (e: Exception) { + Log.e(TAG, "初始化失败", e) + false + } + } + + override suspend fun translateText( + text: String, + sourceLanguage: String, + targetLanguage: String + ): IntegratedSpeechTranslationService.TranslationResult { + if (!isInitialized) { + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "翻译服务未初始化" + ) + } + + return withContext(Dispatchers.IO) { + performTranslationWithRetry(text, sourceLanguage, targetLanguage) + } + } + + private suspend fun performTranslationWithRetry( + text: String, + sourceLanguage: String, + targetLanguage: String + ): IntegratedSpeechTranslationService.TranslationResult { + var lastException: Exception? = null + + repeat(maxRetryAttempts) { attempt -> + try { + val result = performTranslation(text, sourceLanguage, targetLanguage) + if (result.success) { + return result + } + lastException = Exception(result.error) + } catch (e: Exception) { + lastException = e + Log.w(TAG, "翻译请求失败,尝试 ${attempt + 1}/$maxRetryAttempts", e) + + // 重试延迟 + if (attempt < maxRetryAttempts - 1) { + delay(500L * (attempt + 1)) + } + } + } + + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "翻译失败: ${lastException?.message ?: "未知错误"}" + ) + } + + private suspend fun performTranslation( + text: String, + sourceLanguage: String, + targetLanguage: String + ): IntegratedSpeechTranslationService.TranslationResult { + try { + // 获取语言代码 + val sourceCode = getLanguageCode(sourceLanguage) + val targetCode = getLanguageCode(targetLanguage) + + if (sourceCode.isEmpty() || targetCode.isEmpty()) { + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "不支持的语言代码: $sourceLanguage -> $targetLanguage" + ) + } + + // 构建请求体 + val requestBody = JSONObject().apply { + put("SourceLanguage", sourceCode) + put("TargetLanguage", targetCode) + put("TextList", JSONArray().put(text)) + } + + // 查询参数 + val queryParams = mapOf( + "Action" to ACTION, + "Version" to VERSION, + "Region" to region, + "Service" to SERVICE + ) + + // 生成签名 + val headers = generateSignature("POST", requestBody, queryParams) + + // 构建URL + val urlBuilder = BASE_URL.toHttpUrl().newBuilder() + queryParams.forEach { (key, value) -> + urlBuilder.addQueryParameter(key, value) + } + val url = urlBuilder.build() + + // 构建请求 + val requestBodyObj = RequestBody.create( + "application/json; charset=utf-8".toMediaType(), + requestBody.toString() + ) + + val request = Request.Builder() + .url(url) + .post(requestBodyObj) + .apply { + headers.forEach { (key, value) -> + addHeader(key, value) + } + } + .build() + + // 发送请求 + val response = httpClient.newCall(request).execute() + + if (response.isSuccessful) { + val responseBody = response.body?.string() + if (responseBody != null) { + val jsonResponse = JSONObject(responseBody) + val translation = extractTranslation(jsonResponse) + + if (translation != null) { + return IntegratedSpeechTranslationService.TranslationResult( + success = true, + translatedText = translation, + confidence = 0.9f + ) + } else { + val error = extractError(jsonResponse) + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = error ?: "翻译结果为空" + ) + } + } + } + + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "HTTP错误: ${response.code}" + ) + + } catch (e: Exception) { + Log.e(TAG, "翻译请求异常", e) + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "请求异常: ${e.message}" + ) + } + } + + private fun getLanguageCode(languageCode: String): String { + return languageCodeMap[languageCode] ?: "" + } + + private fun extractTranslation(jsonResponse: JSONObject): String? { + try { + // 1. 标准响应结构 + if (jsonResponse.has("TranslationList")) { + val translationList = jsonResponse.getJSONArray("TranslationList") + if (translationList.length() > 0) { + val translationItem = translationList.getJSONObject(0) + if (translationItem.has("Translation")) { + return translationItem.getString("Translation") + } + } + } + + // 2. 其他可能的响应结构 + if (jsonResponse.has("Translation")) { + return jsonResponse.getString("Translation") + } + + if (jsonResponse.has("Result")) { + val result = jsonResponse.getJSONObject("Result") + if (result.has("Translation")) { + return result.getString("Translation") + } + } + + if (jsonResponse.has("Data")) { + val data = jsonResponse.getJSONObject("Data") + if (data.has("Translation")) { + return data.getString("Translation") + } + if (data.has("TranslationList")) { + val translationList = data.getJSONArray("TranslationList") + if (translationList.length() > 0) { + val translationItem = translationList.getJSONObject(0) + if (translationItem.has("Translation")) { + return translationItem.getString("Translation") + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "提取翻译结果失败", e) + } + + return null + } + + private fun extractError(jsonResponse: JSONObject): String? { + try { + if (jsonResponse.has("ResponseMetadata")) { + val metadata = jsonResponse.getJSONObject("ResponseMetadata") + if (metadata.has("Error")) { + val error = metadata.getJSONObject("Error") + return error.optString("Message", error.optString("Code", "未知错误")) + } + } + + if (jsonResponse.has("Error")) { + val error = jsonResponse.getJSONObject("Error") + return error.optString("Message", error.optString("Code", "未知错误")) + } + } catch (e: Exception) { + Log.e(TAG, "提取错误信息失败", e) + } + + return null + } + + private fun generateSignature( + method: String, + requestBody: JSONObject, + queryParams: Map + ): Map { + try { + // 1. 准备时间相关参数 + val now = Date() + val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + val timestampFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + + val date = dateFormat.format(now) + val timestamp = timestampFormat.format(now) + + // 2. 构建规范查询字符串 + val sortedParams = queryParams.toSortedMap() + val canonicalQueryString = sortedParams.map { (key, value) -> + "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" + }.joinToString("&") + + // 3. 创建规范请求 + val contentType = "application/json" + val payloadHash = sha256(requestBody.toString()) + val host = "translate.volcengineapi.com" + + val canonicalHeaders = "host:$host\nx-date:$timestamp\n" + val signedHeaders = "host;x-date" + + val canonicalRequest = + "$method\n$ENDPOINT\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash" + + // 4. 创建待签字符串 + val credentialScope = "$date/$region/$SERVICE/request" + val stringToSign = + "$ALGORITHM\n$timestamp\n$credentialScope\n${sha256(canonicalRequest)}" + + // 5. 计算签名 + val kSecret = secretKey.toByteArray(Charsets.UTF_8) + val kDate = hmacSha256(kSecret, date) + val kRegion = hmacSha256(kDate, region) + val kService = hmacSha256(kRegion, SERVICE) + val kSigning = hmacSha256(kService, "request") + val signature = + hmacSha256(kSigning, stringToSign).joinToString("") { "%02x".format(it) } + + // 6. 构建授权头 + val authorization = + "$ALGORITHM Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature" + + return mapOf( + "Content-Type" to contentType, + "X-Date" to timestamp, + "Authorization" to authorization, + "Host" to host + ) + + } catch (e: Exception) { + Log.e(TAG, "生成签名失败", e) + throw e + } + } + + private fun sha256(input: String): String { + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(input.toByteArray(Charsets.UTF_8)) + return hash.joinToString("") { "%02x".format(it) } + } + + private fun hmacSha256(key: ByteArray, data: String): ByteArray { + val mac = Mac.getInstance("HmacSHA256") + val secretKeySpec = SecretKeySpec(key, "HmacSHA256") + mac.init(secretKeySpec) + return mac.doFinal(data.toByteArray(Charsets.UTF_8)) + } + + override fun dispose() { + isInitialized = false + // 清理资源 + try { + httpClient.dispatcher.executorService.shutdown() + httpClient.connectionPool.evictAll() + } catch (e: Exception) { + Log.e(TAG, "清理资源失败", e) + } + } +} \ No newline at end of file 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 30137c898..acd098897 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 +import kotlinx.coroutines.* /** AzureSpeechPlugin */ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { @@ -35,6 +36,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { private var ttsEventSink: EventChannel.EventSink? = null private lateinit var azureTtsHelper: AzureTtsHelper + // AST相关 + private lateinit var astChannel: MethodChannel + private lateinit var astEventChannel: EventChannel + private var astEventSink: EventChannel.EventSink? = null + private lateinit var azureAstHelper: IntegratedSpeechTranslationService + // 是否已添加TTS事件监听器 private var isTtsListenerAdded = false @@ -74,6 +81,22 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } + // AST 事件发送方法 + private fun sendAstEvent(event: Map) { + if (astEventSink == null) { + FileLogger.w(tag, "无法发送AST事件:事件通道未准备好") + return + } + + mainHandler.post { + try { + astEventSink?.success(event) + } catch (e: Exception) { + FileLogger.e(tag, "发送AST事件失败: ${e.message}") + } + } + } + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext @@ -84,7 +107,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { // 初始化TTS通道 ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts") ttsChannel.setMethodCallHandler(TtsMethodHandler()) - + // 初始化AST通道 + astChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast") + astChannel.setMethodCallHandler(AsTMethodHandler()) // 初始化ASR事件通道 asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events") @@ -111,11 +136,24 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { ttsEventSink = null } }) + // 初始化AST事件通道 + astEventChannel = + EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast_events") + astEventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + astEventSink = events + //setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器 + } + + override fun onCancel(arguments: Any?) { + astEventSink = null + } + }) // 初始化Azure语音服务 azureTtsHelper = AzureTtsHelper(context) azureAsrHelper = AzureAsrHelper(context) - + azureAstHelper = IntegratedSpeechTranslationService(context) // 2. 初始化BleService并注册回调 if (BleService.initialize(context)) { @@ -437,7 +475,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val filePath = call.argument("filePath") ?: "" try { FileLogger.d(tag, "音频文件名称为: ${filePath}") // - + azureAsrHelper.enableRecord(filePath) result.success(true) } catch (e: Exception) { @@ -506,6 +544,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val outputStream = customPlayer.getAudioOutputStream() if (outputStream != null) { azureTtsHelper.setCustomAudioOutputStream(outputStream) + } else { + FileLogger.w(tag, "无法获取自定义音频输出流") } } @@ -584,6 +624,287 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } + // ASt方法处理器 + inner class AsTMethodHandler : MethodCallHandler { + private suspend fun streamAudioFile(file: java.io.File) { + try { + val inputStream = file.inputStream() + val buffer = ByteArray(1024) // 每次读取1KB + + // WAV文件参数(假设16kHz, 16bit, 单声道) + val sampleRate = 16000 // 采样率 + val bytesPerSample = 2 // 16bit = 2字节 + val channels = 1 // 单声道 + + // 计算每秒需要的字节数 + val bytesPerSecond = sampleRate * bytesPerSample * channels + + // 计算每个缓冲区对应的播放时间(毫秒) + val bufferDurationMs = (buffer.size * 1000L) / bytesPerSecond + + FileLogger.d("TAG", "开始流式读取音频文件,缓冲区大小: ${buffer.size}, 播放间隔: ${bufferDurationMs}ms") + + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + // 只发送实际读取的字节数 + val audioChunk = if (bytesRead < buffer.size) { + buffer.copyOf(bytesRead) + } else { + buffer + } + + // 推送音频数据块 + withContext(Dispatchers.Main) { + azureAstHelper.pushAudioData(audioChunk) + FileLogger.d("TAG", "推送音频数据块: ${audioChunk.size} 字节") + } + } + + inputStream.close() + FileLogger.d("TAG", "音频文件流式读取完成") + + } catch (e: Exception) { + FileLogger.e("TAG", "流式读取音频文件失败: ${e.message}") + } +} + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "enableRecord" -> { + val filePath = call.argument("filePath") ?: "" + try { + FileLogger.d(tag, "音频文件名称为: ${filePath}") // + + azureAstHelper.enableRecord(filePath) + result.success(true) + } catch (e: Exception) { + result.error("ENABLERECORD_ERROR", e.message, null) + } + } + + "stopContinuousTranslation" -> { + val isSave = call.argument("isSave") ?: false + try { + FileLogger.d(tag, "停止翻译") + azureAstHelper.stopContinuousTranslation() + result.success(true) + } catch (e: Exception) { + result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null) + } + } + "path" -> { + val filePath = call.argument("filePath") ?: "" + try { + + // 读取这个wav音频文件,取里面的音频数据进行播放 + val file = java.io.File(filePath) + if (file.exists()) { + // 启动协程来按播放速度读取音频文件 + CoroutineScope(Dispatchers.IO).launch { + streamAudioFile(file) + } + result.success(true) + } else { + result.error("FILE_NOT_FOUND", "音频文件不存在: $filePath", null) + } + + } catch (e: Exception) { + result.error("READ_FILE_ERROR", "读取音频文件失败: ${e.message}", null) + } + } + + "initialize" -> { + val subscriptionKey = call.argument("subscriptionKey") ?: "" + val region = call.argument("region") ?: "" + val supportedLanguages = + call.argument>("supportedLanguages") ?: listOf("zh-CN") + val useExternalAudio = call.argument("useExternalAudio") ?: false + + // 获取翻译服务配置参数(需要从Flutter端传递) + val translationAccessKey = call.argument("translationAccessKey") ?: "" + val translationSecretKey = call.argument("translationSecretKey") ?: "" + val translationRegion = + call.argument("translationRegion") ?: "cn-north-1" + + FileLogger.d(tag, "初始化AST服务") + + // 创建Azure配置 + val azureConfig = AzureConfiguration( + subscriptionKey = subscriptionKey, + region = region + ) + + // 创建翻译配置 + val translationConfig = + TranslationConfiguration( + accessKey = translationAccessKey, + secretKey = translationSecretKey, + region = translationRegion + ) + + // 创建服务配置 + val serviceConfig = IntegratedSpeechTranslationService.ServiceConfiguration( + sourceLanguage = if (supportedLanguages.isNotEmpty()) supportedLanguages[0] else "zh-CN", + targetLanguage = if (supportedLanguages.size > 1) supportedLanguages[1] else "en-US" + ) + + // 创建事件回调 + val callback = object : IntegratedSpeechTranslationService.ServiceEventCallback { + override fun onServiceInitialized() { + // sendAstEvent( + // mapOf( + // "type" to "serviceInitialized" + // ) + // ) + } + + override fun onRecognizing(text: String, language: String, confidence: Float) { + // sendAstEvent( + // mapOf( + // "type" to "recognizing", + // "text" to text, + // "language" to language, + // "confidence" to confidence + // ) + // ) + } + + override fun onRecognized(text: String, language: String, confidence: Float) { + // sendAstEvent( + // mapOf( + // "type" to "recognized", + // "text" to text, + // "language" to language, + // "confidence" to confidence + // ) + // ) + } + + override fun onTranslated( + originalText: String, + translatedText: String, + targetLanguage: String + ) { + // sendAstEvent( + // mapOf( + // "type" to "translated", + // "originalText" to originalText, + // "translatedText" to translatedText, + // "targetLanguage" to targetLanguage + // ) + // ) + } + + override fun onTranslationStarted(text: String) { + // sendAstEvent( + // mapOf( + // "type" to "translationStarted", + // "text" to text + // ) + // ) + } + + override fun onTranslationFailed(text: String, error: String) { + // sendAstEvent( + // mapOf( + // "type" to "translationFailed", + // "text" to text, + // "error" to error + // ) + // ) + } + + override fun onSynthesisStarted(text: String) { + // sendAstEvent( + // mapOf( + // "type" to "synthesisStarted", + // "text" to text + // ) + // ) + } + + override fun onSynthesisCompleted(text: String) { + // sendAstEvent( + // mapOf( + // "type" to "synthesisCompleted", + // "text" to text + // ) + // ) + } + + override fun onSynthesisFailed(text: String, error: String) { + // sendAstEvent( + // mapOf( + // "type" to "synthesisFailed", + // "text" to text, + // "error" to error + // ) + // ) + } + + override fun onSynthesisProgress(text: String, progress: Float) { + // sendAstEvent( + // mapOf( + // "type" to "synthesisProgress", + // "text" to text, + // "progress" to progress + // ) + // ) + } + + override fun onRecognitionStarted() { + // sendAstEvent( + // mapOf( + // "type" to "recognitionStarted" + // ) + // ) + } + + override fun onRecognitionStopped() { + // sendAstEvent( + // mapOf( + // "type" to "recognitionStopped" + // ) + // ) + } + + override fun onStateChanged(component: String, isActive: Boolean) { + // sendAstEvent( + // mapOf( + // "type" to "stateChanged", + // "component" to component, + // "isActive" to isActive + // ) + // ) + } + + override fun onError(component: String, error: String) { + // sendAstEvent( + // mapOf( + // "type" to "error", + // "component" to component, + // "error" to error + // ) + // ) + } + } + + // 使用协程调用异步初始化方法 + GlobalScope.launch(Dispatchers.Main) { + azureAstHelper.initialize( + azureConfig = azureConfig, + translationConfig = translationConfig, + serviceConfig = serviceConfig, + callback = callback + ) + } + result.success(true) + } + } + } + } + + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { asrChannel.setMethodCallHandler(null) ttsChannel.setMethodCallHandler(null) @@ -624,6 +945,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { // 可选:处理音频数据 } + override fun onAudioDataReceived1(data: ByteArray) { + + azureAstHelper.pushAudioData(data) + + // 可选:处理音频数据 + } /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别 @@ -635,4 +962,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { override fun onDeviceInfoReceived(infoType: Int, infoData: Map) { // 不处理设备信息 } -} \ No newline at end of file +} + + + \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt index cddc6a245..0d0cf6afc 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt @@ -24,7 +24,7 @@ object RecordFile { // 音频配置 - 可配置的采样率和声道数 private var sampleRate = 16000 - private var channels = 2 // 1=单声道, 2=立体声 + private var channels = 1 // 1=单声道, 2=立体声 // 新增:用于存储最后成功保存的文件 private var lastSavedFile: File? = null diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index fe2eefa6e..dd3988a04 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -10,12 +10,17 @@ object BleConst { /** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000abc0-0000-1111-2222-123456789abc") + /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ + val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc") - /** 音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */ - val AUDIO_SERVICE_UUID1: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC") + /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ + val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc") + + /** 通话音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */ + val CALL_AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC") - /** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */ - val WRITE_AUDIO_CHAR_UUID1: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC") + /** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */ + val CALL_WRITE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC") /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb") @@ -23,15 +28,6 @@ object BleConst { /** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */ val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb") - /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc") - - /** 音频写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - val AUDIO_WRITE_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC") - - /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ - val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc") - /** 客户端特征配置描述符UUID */ val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index d0f52906e..cda3594a4 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -26,6 +26,7 @@ import android.os.Handler import java.util.concurrent.atomic.AtomicBoolean import android.util.Log import androidx.annotation.RequiresPermission +import java.util.concurrent.TimeUnit /** * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 @@ -53,6 +54,10 @@ object BleService { // 数据相关回调 fun onAudioDataReceived(data: ByteArray) + // 数据相关回调 + fun onAudioDataReceived1(data: ByteArray) + + // 唤醒信号相关回调 fun onWakeupSignalReceived() // 设备信息相关回调 - 统一回调接口 @@ -77,9 +82,10 @@ object BleService { private var notifyChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null private var audioChar: BluetoothGattCharacteristic? = null - private var writeChar1: BluetoothGattCharacteristic? = null + private var callWriteChar: BluetoothGattCharacteristic? = null var recordfile: RecordingFile? = null var recordfile1: RecordingFile? = null + // 扫描相关 private lateinit var scanHandler: Handler private val scanResults = ArrayList() @@ -102,19 +108,26 @@ object BleService { // Opus解码器实例 private var opusManager: OpusManager? = null + private var option: OpusOption? = null private val mainHandler = Handler(Looper.getMainLooper()) - - // 音频解码处理线程 - private var audioDecodeThread: HandlerThread? = null - private var audioDecodeHandler: Handler? = null - - // 音频数据队列和处理线程 + + // 解码音频数据队列和处理线程 private val audioDataQueue = LinkedBlockingQueue() private var audioQueueProcessorThread: Thread? = null - + // 音频数据缓存 private val audioDataBuffer = mutableListOf() - private val AUDIO_BUFFER_SIZE = 1280// 1280字节缓存阈值 + //private val AUDIO_BUFFER_SIZE = 1280// 1280字节缓存阈值 + + // 音频数据发送相关 + private val audioSendQueue = LinkedBlockingQueue() + private var audioSendThread: Thread? = null + private val audioSendHandler = Handler(Looper.getMainLooper()) + private val isAudioSending = AtomicBoolean(false) + + // 音频数据分块发送的常量 + private val AUDIO_CHUNK_SIZE = 40 // 每次发送40字节 + private val AUDIO_SEND_INTERVAL = 10L // 发送间隔10ms // 初始化状态 private var isInitialized = false @@ -138,20 +151,6 @@ object BleService { try { this.context = appContext.applicationContext - - // otaManager = OTAManager(this.context).apply { - // // 设置数据回调 - // setDataCallback(object : OTAManager.DataCallback { - // override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) { - // data?.let { - // Log.i("BleService", "收到数据:${it.size} 字节") - // Log.i("BleService", "认证交互数据${it.contentToString()}") - // // 移除次数限制,除非明确需要 - // otaManager?.onReceiveDeviceData(device, it) - // } - // } - // }) - // } // 初始化蓝牙管理器和适配器 bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager @@ -160,22 +159,14 @@ object BleService { // 初始化Handler scanHandler = Handler(Looper.getMainLooper()) - - // 初始化音频解码线程 - audioDecodeThread = HandlerThread("AudioDecodeThread").apply { - start() - audioDecodeHandler = Handler(looper) - } - Log.d(TAG, "音频解码线程初始化成功") - + // 启动队列处理 startAudioQueueProcessing() - // 初始化OpusManager和OTAManager + // 初始化OpusManager和OpusOption try { opusManager = OpusManager() - startOpusStreamDecoding() - startEncodeStream() + option = OpusOption() Log.d(TAG, "OpusManager初始化成功") } catch (e: OpusException) { Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) @@ -184,7 +175,7 @@ object BleService { recordfile = RecordingFile(this.context) recordfile!!.fileName = "不拆分" - recordfile1 = RecordingFile(this.context) + recordfile1 = RecordingFile(this.context) recordfile1!!.fileName = "重新压缩" isInitialized = true Log.d(TAG, "BLE服务初始化成功") @@ -230,6 +221,25 @@ object BleService { Log.d(TAG, "已清除所有BLE回调") } + fun writeExternalAudioData(data: ByteArray) { + // 检查OpusManager是否已初始化 + if (opusManager == null) { + Log.e(TAG, "opusManager 未初始化") + return + } + + // 如果已经在编码流中,直接写入数据 + if (opusManager?.isEncodeStream == true) { + Log.d(TAG, "正在进行Opus编码流,写入音频数据") + // 将外部音频数据写入编码流,每次处理1280字节 + opusManager?.writeEncodeStream(data) + } else { + Log.w(TAG, "Opus编码流未启动,无法写入音频数据") + // 可选:自动启动编码流 + // startOpusEncodeStream() + } + } + // ====================================================================================================== // 扫描功能 // ====================================================================================================== @@ -385,15 +395,16 @@ object BleService { } return null; } + fun getDeviceInfos(deviceName: String): List> { // 更新缓存 val matchedResults = scanResults.filter { it.device.name == deviceName } Log.d(TAG, "找到 ${matchedResults.size} 个名称为 $deviceName 的设备") - + val ret = mutableListOf>() - + // 如果找到匹配设备名的结果,返回匹配设备的信息 if (matchedResults.isNotEmpty()) { for (result in matchedResults) { @@ -411,9 +422,10 @@ object BleService { } } } - + return ret } + /** * 打印设备信息和服务UUID */ @@ -542,7 +554,7 @@ object BleService { notifyChar = null writeChar = null audioChar = null - writeChar1 = null + callWriteChar = null } } @@ -560,37 +572,17 @@ object BleService { */ private val gattCallback = object : BluetoothGattCallback() { - - // override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { - // if (status == BluetoothGatt.GATT_SUCCESS) { - // Log.i(TAG, "MTU 更新成功: $mtu") - // configureOTA() - - // startOTA() - // } else { - // Log.e(TAG, "MTU 更新失败: status=$status") - // } - // } - override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { when { status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { updateConnectionState(BleConst.STATE_CONNECTED) g.discoverServices() - // Log.i(TAG, "连接成功,开始 MTU 协商") - - // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK) - // g.requestMtu(512) // 触发 MTU 修改流程 } newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) disconnectGatt() - - // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING) - // g.close() - // otaManager?.release(); } else -> { @@ -617,17 +609,18 @@ object BleService { audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) - // 获取音频服务1特征 - val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1) - writeChar1 = audioSvc1?.getCharacteristic(BleConst.WRITE_AUDIO_CHAR_UUID1) - if (writeChar1 == null) { - Log.e(TAG, "未找到主服务所需特征") + // 获取通话音频服务特征 + val callAudioSvc = g.getService(BleConst.CALL_AUDIO_SERVICE_UUID) + callWriteChar = callAudioSvc?.getCharacteristic(BleConst.CALL_WRITE_AUDIO_CHAR_UUID) + if (callWriteChar == null) { + Log.e(TAG, "未找到通话音频主服务所需特征") updateConnectionState(BleConst.STATE_ERROR) return } - Log.d(TAG, "找到writeChar1服务所需特征") + Log.d(TAG, "找到通话音频服务所需特征") + writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - writeChar1?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE + callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE //要先设置音频服务的通知,否则接收不到 // 设置音频服务的通知(如果存在) if (audioChar != null) { @@ -636,10 +629,9 @@ object BleService { } else { Log.w(TAG, "音频服务特征未找到") } - + // 设置主服务的通知 setupNotifications(g, notifyChar) -//开启解码 } @@ -649,12 +641,6 @@ object BleService { // 根据特征UUID区分处理 when (c.uuid) { - // 音频特征数据 - // BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> { - - // Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1") - // processAudioData(data) - // } // 音频特征数据 BleConst.RECEIVE_AUDIO_CHAR_UUID -> { @@ -736,26 +722,26 @@ object BleService { try { if (opusManager?.isDecodeStream == true) { recordfile?.saveAudioDataToWav(data) - - // 将接收到的数据添加到缓存中 - synchronized(audioDataBuffer) { - audioDataBuffer.addAll(data.toList()) - - // 当缓存达到阈值时,提取完整的帧进行解码 - while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) { - // 提取一个完整的帧(1280字节) - val frameData = ByteArray(AUDIO_BUFFER_SIZE) - for (i in 0 until AUDIO_BUFFER_SIZE) { - frameData[i] = audioDataBuffer.removeAt(0) - } - - // 将完整的帧加入队列,由专门的线程处理 - audioDataQueue.offer(frameData) - Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码") - } - } + // 将完整的帧加入队列,由专门的线程处理 + audioDataQueue.offer(data) + // // 将接收到的数据添加到缓存中 + // synchronized(audioDataBuffer) { + // audioDataBuffer.addAll(data.toList()) + + // // 当缓存达到阈值时,提取完整的帧进行解码 + // while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) { + // // 提取一个完整的帧(1280字节) + // val frameData = ByteArray(AUDIO_BUFFER_SIZE) + // for (i in 0 until AUDIO_BUFFER_SIZE) { + // frameData[i] = audioDataBuffer.removeAt(0) + // } + + + // Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码") + // } + // } } else { - // Log.d(TAG, "Opus解码流未启动,忽略音频数据") + Log.d(TAG, "Opus解码流未启动,忽略音频数据") } } catch (e: Exception) { Log.e(TAG, "处理音频数据异常: ${e.message}", e) @@ -959,8 +945,8 @@ object BleService { codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON ) { -recordfile1!!.closeFile() - recordfile1!!.creatingFiles() + recordfile1!!.closeFile() + recordfile1!!.creatingFiles() recordfile!!.closeFile() recordfile!!.creatingFiles() } else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) { @@ -1140,18 +1126,17 @@ recordfile1!!.closeFile() opusManager?.stopDecodeStream() Log.d(TAG, "已停止正在进行的Opus解码流") } - + // 清理音频数据缓存,确保开始时是干净的状态 synchronized(audioDataBuffer) { audioDataBuffer.clear() Log.d(TAG, "开始解码前已清理音频数据缓存") } - val option = OpusOption() - .setHasHead(hasHeader) - .setChannel(channel) - .setSampleRate(sampleRate) - .setPacketSize(packetSize) + option!!.setHasHead(hasHeader) + option!!.setChannel(channel) + option!!.setSampleRate(sampleRate) + option!!.setPacketSize(packetSize) Log.d(TAG, "准备开始Opus数据流解码, 参数: $option") @@ -1160,31 +1145,40 @@ recordfile1!!.closeFile() override fun onDecodeStream(data: ByteArray?) { if (data != null) { // Log.d(TAG, "Opus解码数据: ${data.size} bytes") - //解码数据再重新编码回去 - val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节) - val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区 - val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区 - - // 拆分交错的左右声道数据 - for (i in 0 until sampleCount) { - val stereoIndex = i * 4 - val monoIndex = i * 2 - - // 左声道(低位字节在前,高位字节在后) - leftBuffer[monoIndex] = data[stereoIndex] - leftBuffer[monoIndex + 1] = data[stereoIndex + 1] - - // 右声道 - rightBuffer[monoIndex] = data[stereoIndex + 2] - rightBuffer[monoIndex + 1] = data[stereoIndex + 3] - } + if (option!!.getChannel() == 2) { + + //解码数据再重新编码回去 + + + val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节) + val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区 + val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区 + + // 拆分交错的左右声道数据 + for (i in 0 until sampleCount) { + val stereoIndex = i * 4 + val monoIndex = i * 2 - // // 将左右声道数据分别放入队列 - // leftChannelQueue.offer(leftBuffer) - // rightChannelQueue.offer(rightBuffer) - opusManager?.writeEncodeStream(rightBuffer) + // 左声道(低位字节在前,高位字节在后) + leftBuffer[monoIndex] = data[stereoIndex] + leftBuffer[monoIndex + 1] = data[stereoIndex + 1] - notifyAudioDataReceived(rightBuffer) + // 右声道 + rightBuffer[monoIndex] = data[stereoIndex + 2] + rightBuffer[monoIndex + 1] = data[stereoIndex + 3] + } + + // //重新编码 + // opusManager?.writeEncodeStream(rightBuffer) + notifyAudioDataReceived1(leftBuffer) + //回调 + notifyAudioDataReceived(rightBuffer)//对方的 + // notifyAudioDataReceived1(leftBuffer)//自己的 + } else if (option!!.getChannel() == 1) { + notifyAudioDataReceived(data) + } else { + Log.e(TAG, "Opus解码数据错误: ${data.size} bytes") + } } } @@ -1212,13 +1206,13 @@ recordfile1!!.closeFile() private fun stopOpusStreamDecoding(): Boolean { if (opusManager?.isDecodeStream == true) { opusManager?.stopDecodeStream() - + // 清理音频数据缓存 synchronized(audioDataBuffer) { audioDataBuffer.clear() Log.d(TAG, "已清理音频数据缓存") } - + Log.i(TAG, "已停止Opus数据流解码") return true } @@ -1230,55 +1224,167 @@ recordfile1!!.closeFile() // Opus 编码相关 // ====================================================================================================== - private fun startEncodeStream( - hasHeader: Boolean = false, // 通常BLE传输的Opus没有文件头 - channel: Int = 1, - sampleRate: Int = 16000, // 确认设备端Opus编码采样率 - packetSize: Int = 40 // 确认设备端Opus编码帧长,必须与iOS版本frameSize保持一致 - ): Boolean { + private fun startOpusEncodeStream(): Boolean { if (opusManager == null) { Log.e(TAG, "OpusManager未初始化,无法开始编码") return false } - // 如果已经在解码流,先停止 + + // 如果已经在编码流,先停止 if (opusManager?.isEncodeStream == true) { - opusManager?.stopEncodeStream() - Log.d(TAG, "已停止正在进行的Opus编码流") + Log.d(TAG, "Opus编码流已在运行") + return true } - + + // 启动音频发送线程 + startAudioSendThread() var streamStartedSuccessfully = false opusManager?.startEncodeStream(object : OnEncodeStreamCallback { override fun onEncodeStream(data: ByteArray?) { if (data != null) { - // Log.d(TAG, "Opus解码数据: ${data.size} bytes") - //notifyAudioDataReceived(data) - writeChar1!!.value=data - recordfile1!!.saveAudioDataToWav(data) - val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar1) - if (isSuccess == true) { - - Log.d(TAG, "发送opus数据") - } + // 编码完成的数据处理: + // 1. 保存到WAV文件 + recordfile1?.saveAudioDataToWav(data) + // 2. 将编码后的数据加入发送队列进行分块发送 + addAudioDataToSendQueue(data) + } else { + Log.w(TAG, "编码回调收到空数据") } } - + override fun onStart() { streamStartedSuccessfully = true Log.i(TAG, "Opus数据流编码已开始") - // 可以通过回调通知上层解码已开始 } override fun onComplete(outPath: String?) { - Log.i(TAG, "Opus数据流编码完成: $outPath (通常流式编码不会调用此方法)") + Log.i(TAG, "Opus数据流编码完成: $outPath") } override fun onError(code: Int, message: String?) { Log.e(TAG, "Opus数据流编码错误: [$code] $message") - // 可以通过回调通知上层解码错误 } }) - return true // 暂定为调用即成功 + + return true + } + + private fun stopOpusEncodeStream(): Boolean { + if (opusManager?.isEncodeStream == true) { + opusManager?.stopEncodeStream() + stopAudioSendThread() // 停止音频发送线程 + Log.i(TAG, "已停止Opus数据流编码") + return true + } + Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化") + return false + } + + /** + * 将音频数据分块并加入发送队列 + */ + private fun addAudioDataToSendQueue(data: ByteArray) { + try { + // 将大数据分成40字节的小块 + var offset = 0 + while (offset < data.size) { + val chunkSize = minOf(AUDIO_CHUNK_SIZE, data.size - offset) + val chunk = ByteArray(chunkSize) + System.arraycopy(data, offset, chunk, 0, chunkSize) + + // 将分块数据加入队列 + if (!audioSendQueue.offer(chunk)) { + Log.w(TAG, "音频发送队列已满,丢弃数据块") + } + + offset += chunkSize + } + + Log.d(TAG, "音频数据已分块加入队列,原始大小: ${data.size} 字节,分块数: ${(data.size + AUDIO_CHUNK_SIZE - 1) / AUDIO_CHUNK_SIZE}") + } catch (e: Exception) { + Log.e(TAG, "分块音频数据异常: ${e.message}", e) + } + } + + /** + * 启动音频数据发送线程 + */ + private fun startAudioSendThread() { + if (audioSendThread?.isAlive == true) { + Log.d(TAG, "音频发送线程已在运行") + return + } + + isAudioSending.set(true) + audioSendThread = Thread { + Log.i(TAG, "音频发送线程已启动") + + while (isAudioSending.get() && !Thread.currentThread().isInterrupted) { + try { + // 从队列中取出音频数据块 + val audioChunk = audioSendQueue.poll(100, TimeUnit.MILLISECONDS) + + if (audioChunk != null) { + // 发送音频数据块 + sendAudioChunk(audioChunk) + + // 控制发送频率,避免蓝牙缓冲区溢出 + //Thread.sleep(AUDIO_SEND_INTERVAL) + } + } catch (e: InterruptedException) { + Log.d(TAG, "音频发送线程被中断") + break + } catch (e: Exception) { + Log.e(TAG, "音频发送线程异常: ${e.message}", e) + } + } + + Log.i(TAG, "音频发送线程已停止") + }.apply { + name = "AudioSendThread" + start() + } + } + + /** + * 停止音频数据发送线程 + */ + private fun stopAudioSendThread() { + isAudioSending.set(false) + audioSendThread?.interrupt() + audioSendQueue.clear() + Log.i(TAG, "音频发送线程已停止,队列已清空") + } + + /** + * 发送单个音频数据块 + */ + private fun sendAudioChunk(chunk: ByteArray) { + try { + if (callWriteChar == null || bluetoothGatt == null) { + Log.e(TAG, "蓝牙连接或特征值未准备就绪") + return + } + + // 在主线程中执行蓝牙写入操作 + audioSendHandler.post { + try { + callWriteChar?.value = chunk + val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) + + if (isSuccess == true) { + Log.d(TAG, "成功发送音频数据块,大小: ${chunk.size} 字节") + } else { + Log.e(TAG, "发送音频数据块失败,大小: ${chunk.size} 字节") + } + } catch (e: Exception) { + Log.e(TAG, "发送音频数据块异常: ${e.message}", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "发送音频数据块异常: ${e.message}", e) + } } // ====================================================================================================== // 公开的命令接口 @@ -1361,7 +1467,9 @@ recordfile1!!.closeFile() */ fun openA2DPDecoder(): Boolean { Log.i(TAG, "打开编码 0xA2") - startOpusStreamDecoding(false,2, 16000, 80) + startOpusEncodeStream() + //双声道 80字节 + startOpusStreamDecoding(false, 2, 16000, 80) // Log.i(TAG, "打开解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( @@ -1452,7 +1560,7 @@ recordfile1!!.closeFile() * 连接状态检查 */ private fun checkConn(): Boolean = - bluetoothGatt != null && writeChar != null && writeChar1 != null && + bluetoothGatt != null && writeChar != null && callWriteChar != null && connectionState.value == BleConst.STATE_CONNECTED /** @@ -1480,34 +1588,28 @@ recordfile1!!.closeFile() stopScan() scanHandler.removeCallbacksAndMessages(null) disconnectGatt() - - // 清理音频解码线程和队列 - audioDecodeHandler?.removeCallbacksAndMessages(null) - audioDecodeThread?.quitSafely() - audioDecodeThread = null - audioDecodeHandler = null - // 停止音频队列处理线程 audioQueueProcessorThread?.interrupt() audioQueueProcessorThread = null audioDataQueue.clear() + // 停止音频发送线程 + stopAudioSendThread() + // 清理音频数据缓存 synchronized(audioDataBuffer) { audioDataBuffer.clear() Log.d(TAG, "已清理音频数据缓存") } - + Log.d(TAG, "音频解码线程和队列处理线程已清理") - + // 释放OpusManager - opusManager?.let { - if (it.isDecodeStream) { - it.stopDecodeStream() - } - it.release() - } + stopOpusEncodeStream() + stopOpusStreamDecoding() + opusManager?.release() opusManager = null + option = null Log.d(TAG, "OpusManager已释放") isInitialized = false // 标记为未初始化 } @@ -1573,8 +1675,7 @@ recordfile1!!.closeFile() isReply = false replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 return true - } else - { + } else { commandQueue.poll() return false } @@ -1660,6 +1761,18 @@ recordfile1!!.closeFile() } } } + /** + * 向所有回调监听器分发音频数据 + */ + private fun notifyAudioDataReceived1(data: ByteArray) { + for (callback in callbacks) { + try { + callback.onAudioDataReceived1(data) + } catch (e: Exception) { + Log.e(TAG, "分发音频数据回调异常", e) + } + } + } /** * 向所有回调监听器分发唤醒信号 @@ -1686,7 +1799,7 @@ recordfile1!!.closeFile() } } } - + /** * 启动音频队列处理 */ @@ -1696,6 +1809,7 @@ recordfile1!!.closeFile() try { // 从队列中取出音频数据进行解码 val audioData = audioDataQueue.take() // 阻塞等待数据 + opusManager?.writeAudioStream(audioData) } catch (e: InterruptedException) { Log.d(TAG, "音频队列处理线程被中断") diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 0e43d9188..e911e41c3 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -354,6 +354,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, // sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") } + override fun onAudioDataReceived1(data: ByteArray) { + // sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") + } + override fun onWakeupSignalReceived() { // 将唤醒事件发送到Flutter sendEvent(statusEventSink, mapOf("type" to "wakeup"), "发送唤醒信号异常") diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index c030cb725..1830237b1 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -383,6 +383,9 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { // 可选:处理音频数据 } + override fun onAudioDataReceived1(data: ByteArray) { + + } /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别