From 54735fc580b816a6eca57a1699230b9157666c26 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Wed, 9 Jul 2025 12:04:22 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E9=9F=B3=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/meeting_upload_service.dart | 43 ++++++++++++++----- .../controllers/meeting_controller.dart | 9 ++-- .../meeting_details_controller.dart | 3 ++ lib/modules/meeting/views/meeting_view.dart | 41 ++++++++++++++++++ 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/lib/data/services/meeting/meeting_upload_service.dart b/lib/data/services/meeting/meeting_upload_service.dart index 9dc4653e5..78cd097eb 100644 --- a/lib/data/services/meeting/meeting_upload_service.dart +++ b/lib/data/services/meeting/meeting_upload_service.dart @@ -1,11 +1,12 @@ import 'package:get/get.dart'; +import '../../../core/utils/logger.dart'; import '../../../core/utils/upload_oss.dart'; import '../db/sqflite_api.dart'; class MeetingUploadService extends GetxService { RxInt meetingId = 0.obs; - RxDouble uploadProgress = 0.0.obs; + RxDouble uploadProgress = 0.6.obs; RxList uploadList = [].obs; bool _isUpload = false; @@ -32,19 +33,39 @@ class MeetingUploadService extends GetxService { while (_handled < uploadList.length) { Map element = uploadList[_handled]; _handled++; - if (element['audiourl'].isNotEmpty) { + if (element['audiourl'].isEmpty) { meetingId.value = element['id']; - final audiourl = await UploadOss.upload( - filepath: element['filepath'], - rootDir: element['rootdir'], - ); - await SqfliteApi.editMeetingTitle( - element['id'], - {'audiourl': audiourl}, - ); - element['audiourl'] = audiourl; + try { + final audiourl = await UploadOss.upload( + filepath: element['filepath'], + rootDir: element['rootdir'], + ); + await SqfliteApi.editMeetingTitle( + element['id'], + {'audiourl': audiourl}, + ); + element['audiourl'] = audiourl; + } catch (e) { + Logger.error('Upload failed: $e'); + } } } + meetingId.value = 0; _isUpload = false; } + + void removeUpload(int id, {bool isDeleteTask = false}) { + Map uploadData = uploadList.firstWhere( + (element) => element['id'] == id, + orElse: () => {}, + ); + if (uploadData.isNotEmpty) { + if (isDeleteTask || uploadData['audiourl'].isNotEmpty) { + uploadList.remove(uploadData); + if (uploadList.isEmpty) { + _handled = 0; + } + } + } + } } diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index ef3eaedd3..363383c13 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -23,7 +23,7 @@ import '../../../data/services/meeting/meeting_upload_service.dart'; import '../../../routes/app_routes.dart'; class MeetingController extends GetxController { - final _meetingUploadService = Get.find(); + final meetingUploadService = Get.find(); final ScrollController listController = ScrollController(); final AudioPlayer _audioPlayer = AudioPlayer(); @@ -245,7 +245,7 @@ class MeetingController extends GetxController { final meetingId = await SqfliteApi.insertMeeting(data); if (meetingId != null) { // 上传OSS - _meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio'); + meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio'); // 更新UI数据 dataList.insert(0, { @@ -293,7 +293,7 @@ class MeetingController extends GetxController { }; final meetingId = await SqfliteApi.insertMeeting(data); if (meetingId != null) { - _meetingUploadService.addUpload(meetingId, filePath, 'TranslatAudio'); + meetingUploadService.addUpload(meetingId, filePath, 'TranslatAudio'); dataList.insert(0, { 'id': meetingId, @@ -356,7 +356,7 @@ class MeetingController extends GetxController { }; final meetingId = await SqfliteApi.insertMeeting(data); if (meetingId != null) { - _meetingUploadService.addUpload(meetingId, filePath, 'ExternalAudio'); + meetingUploadService.addUpload(meetingId, filePath, 'ExternalAudio'); dataList.insert(0, { 'id': meetingId, @@ -400,6 +400,7 @@ class MeetingController extends GetxController { // 删除数据库中的记录 SqfliteApi.deleteMeeting(id); _deleteFile(filepath); + meetingUploadService.removeUpload(id, isDeleteTask: true); addQuantity--; } diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index c97e7ed53..90c08b4d9 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -19,6 +19,7 @@ import '../../../core/utils/upload_oss.dart'; import '../../../data/models/appconfig.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/deapsound_ai_service.dart'; +import '../../../data/services/meeting/meeting_upload_service.dart'; import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; import 'meeting_controller.dart'; @@ -30,6 +31,7 @@ class MeetingDetailsController extends GetxController late AnimationController lottieController; + final _meetingUploadService = Get.find(); final _meetingController = Get.find(); final PlayerController playerController = PlayerController(); @@ -141,6 +143,7 @@ class MeetingDetailsController extends GetxController _meetingController.dataListTask, isAdd: false, ); + _meetingUploadService.removeUpload(id); } void _getAgent() async { diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index 58573ba89..a45184d51 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -562,6 +562,47 @@ class MeetingView extends GetView { ), ), ), + Obx(() { + Map uploadData = + controller.meetingUploadService.uploadList.firstWhere( + (element) => element['id'] == data.id, + orElse: () => {}, + ); + return uploadData.isNotEmpty + ? Stack( + alignment: Alignment.center, + children: [ + if (uploadData['id'] == + controller.meetingUploadService.meetingId.value) + Obx( + () => SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + value: controller.meetingUploadService + .uploadProgress.value, + valueColor: + const AlwaysStoppedAnimation( + Colors.red), + strokeWidth: 1.w, + ), + ), + ), + SizedBox( + width: 20.w, + height: 20.w, + child: Icon( + uploadData['audiourl'].isNotEmpty + ? Icons.cloud_done + : Icons.cloud_upload, + color: Colors.blue, + size: 16.w, + ), + ), + ], + ) + : const SizedBox(); + }), if (data.tasktype == 1) Padding( padding: EdgeInsets.only(top: 5.w), From 4c365f31336181375737b6d8df5c864ec029a551 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Wed, 9 Jul 2025 12:17:55 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/upload_oss.dart | 1 + lib/data/services/meeting/meeting_upload_service.dart | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/core/utils/upload_oss.dart b/lib/core/utils/upload_oss.dart index 1adeefe8e..a2fb54bbf 100644 --- a/lib/core/utils/upload_oss.dart +++ b/lib/core/utils/upload_oss.dart @@ -51,6 +51,7 @@ class UploadOss { bucket, pathName, filePath: filepath, + progressCallBack: onSendProgress, resultListener: ResultListener( (Map? header, CosXmlResult? result) { final url = result?.accessUrl ?? diff --git a/lib/data/services/meeting/meeting_upload_service.dart b/lib/data/services/meeting/meeting_upload_service.dart index 78cd097eb..294a13bd4 100644 --- a/lib/data/services/meeting/meeting_upload_service.dart +++ b/lib/data/services/meeting/meeting_upload_service.dart @@ -6,7 +6,7 @@ import '../db/sqflite_api.dart'; class MeetingUploadService extends GetxService { RxInt meetingId = 0.obs; - RxDouble uploadProgress = 0.6.obs; + RxDouble uploadProgress = 0.0.obs; RxList uploadList = [].obs; bool _isUpload = false; @@ -39,6 +39,9 @@ class MeetingUploadService extends GetxService { final audiourl = await UploadOss.upload( filepath: element['filepath'], rootDir: element['rootdir'], + onSendProgress: (int count, int total) { + uploadProgress.value = count / total; + }, ); await SqfliteApi.editMeetingTitle( element['id'], From 4b176f5c2cf07600780a880403f48f9428951b5f Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Wed, 9 Jul 2025 14:59:13 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E9=9F=B3=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/meeting/meeting_upload_service.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/data/services/meeting/meeting_upload_service.dart b/lib/data/services/meeting/meeting_upload_service.dart index 294a13bd4..85ac61dac 100644 --- a/lib/data/services/meeting/meeting_upload_service.dart +++ b/lib/data/services/meeting/meeting_upload_service.dart @@ -1,10 +1,13 @@ import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/upload_oss.dart'; import '../db/sqflite_api.dart'; class MeetingUploadService extends GetxService { + final GetStorage _storage = GetStorage(); + RxInt meetingId = 0.obs; RxDouble uploadProgress = 0.0.obs; RxList uploadList = [].obs; @@ -18,7 +21,10 @@ class MeetingUploadService extends GetxService { _getUploadList(); } - void _getUploadList() {} + void _getUploadList() { + uploadList.value = _storage.read("upload_list") ?? []; + _executeUpload(); + } void addUpload(int id, String filePath, String rootDir) async { uploadList.add({ @@ -27,6 +33,11 @@ class MeetingUploadService extends GetxService { 'rootdir': rootDir, 'audiourl': '', }); + _storage.write("upload_list", uploadList); + _executeUpload(); + } + + void _executeUpload() async { if (_isUpload) return; _isUpload = true; _handled = 0; @@ -48,6 +59,8 @@ class MeetingUploadService extends GetxService { {'audiourl': audiourl}, ); element['audiourl'] = audiourl; + uploadProgress.value = 0.0; + _storage.write("upload_list", uploadList); } catch (e) { Logger.error('Upload failed: $e'); } @@ -65,6 +78,7 @@ class MeetingUploadService extends GetxService { if (uploadData.isNotEmpty) { if (isDeleteTask || uploadData['audiourl'].isNotEmpty) { uploadList.remove(uploadData); + _storage.write("upload_list", uploadList); if (uploadList.isEmpty) { _handled = 0; } From 009199a1af267117690680ed61bef560bdf884dd Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 9 Jul 2025 15:53:51 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=A4=A9=E6=B0=94?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/agent/views/message_bubble.dart | 32 ++++++++++----------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 51f7cf8e2..825f6b2f1 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -1,7 +1,3 @@ -import 'dart:convert'; -import 'dart:ffi'; - -import 'package:common_utils/common_utils.dart'; import 'package:deep_voice/modules/agent/views/navigation_card.dart'; import 'weather_card.dart'; import 'music_card.dart'; @@ -9,7 +5,6 @@ import 'travel_card.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; -import 'package:get_storage/get_storage.dart'; import 'dart:io'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -257,18 +252,21 @@ class MessageBubble extends GetView { return buildMusicContent(context, playlist); } } else if (meta!.keys.contains('maps_weather')) { - final city = meta!['maps_weather']['city'] as String? ?? '未知城市'; - final rawList = meta!['maps_weather']['forecasts'] as List? ?? []; - final daylist = - rawList.map((e) => Map.from(e)).toList(); - final now = daylist.isNotEmpty ? daylist[0] : {}; - //天气 - return buildWeatherContent( - context, - city, - now, - daylist, - ); + final maps_weather = meta!['maps_weather'] as Map; + final city = maps_weather['city']; + if (city != null) { + final rawList = maps_weather['forecasts'] as List? ?? []; + final daylist = + rawList.map((e) => Map.from(e)).toList(); + final now = daylist.isNotEmpty ? daylist[0] : {}; + //天气 + return buildWeatherContent( + context, + city, + now, + daylist, + ); + } } else if (meta!.keys.contains('card_travel')) { final rawList = meta!['card_travel']['items'] as List? ?? []; final travellist = From 3f87b9c71932b42eceea0d435974bfd11d70cd38 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Wed, 9 Jul 2025 15:57:51 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E8=AF=86=E5=88=AB=EF=BC=8C=E4=BC=98=E5=8C=96ble=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/asr_service.dart | 11 +- lib/data/services/ble_manager.dart | 38 +- .../speech_impl/azure_asr_service.dart | 57 +- .../speech_impl/volcano_asr_api_service.dart | 18 +- .../speech_impl/volcano_asr_service.dart | 18 +- .../speech_impl/xunfei_asr_service.dart | 100 +-- .../FTFTranslation_controller.dart | 13 +- .../agent/controllers/agent_controller.dart | 90 +- .../meeting_record_controller.dart | 31 +- lib/modules/settings/views/settings_view.dart | 44 +- .../controllers/speech_test_controller.dart | 10 +- .../controllers/translation_controller.dart | 34 +- .../agent_service/AgentService.kt | 676 +++++++------- .../agent_service/AgentServicePlugin.kt | 86 +- .../yunqiinnovation/agent_service/BleAgent.kt | 22 +- .../agent_service/lib/agent_service.dart | 53 +- .../azure_speech/AzureAsrHelper.kt | 824 ++++++++---------- .../azure_speech/AzureSpeechPlugin.kt | 128 +-- .../azure_speech/tools/RecordFile.kt | 3 +- .../yunqiinnovation/ble_service/BleService.kt | 55 +- 20 files changed, 1145 insertions(+), 1166 deletions(-) diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index f0c372115..5d1a616d3 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -19,18 +19,17 @@ abstract class AsrService { /// 开始连续语音识别 /// /// 返回一个包含识别事件的流 - Future> startContinuousRecognition( + Future startContinuousRecognition( bool audioSourceType, ); /// 停止连续语音识别 Future stopContinuousRecognition(); - /// 停止音频输入 - Future setPause(); - - /// 继续音频输入 - Future setContinue(); + /// 设置识别回掉 + /// + /// 返回一个包含识别事件的流 + Future> recognizeCallback(); /// 恢复原始音频状态 Future restoreOriginalAudioState(); diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index fcd6450ed..1c921fa07 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -472,37 +472,13 @@ class BleManager extends GetxService { firmwareProductColor: '', ); } - final commands = [ - _bleService.getBatteryInfo, - _bleService.getDeviceVersion, - _bleService.getProductId, - ]; - - final delays = [ - Duration.zero, // 第一个命令立即执行 - const Duration(milliseconds: 100), // 第二个命令延迟100ms - const Duration(milliseconds: 100), // 第三个命令延迟200ms - ]; - - final futures = commands - .asMap() - .map((index, command) { - final delay = delays[index]; - return MapEntry( - index, - Future.delayed(delay, command), - ); - }) - .values - .toList(); - - final results = await Future.wait(futures); - // // 并行获取所有设备信息,不使用延迟 - // final results = await Future.wait([ - // _bleService.getBatteryInfo(), - // _bleService.getDeviceVersion(), - // _bleService.getProductId(), - // ]); + + // 并行获取所有设备信息,不使用延迟 + final results = await Future.wait([ + _bleService.getBatteryInfo(), + _bleService.getDeviceVersion(), + _bleService.getProductId(), + ]); Logger.info( '设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}'); diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 16261533c..36cb64256 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -150,8 +150,7 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future> startContinuousRecognition( - bool audioSourceType) async { + Future startContinuousRecognition(bool audioSourceType) async { if (!_isInitialized) { await initialize(); } @@ -161,8 +160,6 @@ class AzureAsrService extends GetxService implements AsrService { } try { - _eventStreamController = StreamController.broadcast(); - // 开始连续识别 final bool result = await _channel.invokeMethod('startContinuousRecognition', { @@ -170,15 +167,13 @@ class AzureAsrService extends GetxService implements AsrService { }); if (!result) { - _cleanupEventStream(); throw Exception('启动连续识别失败'); } _isContinuousRecognitionActive = true; - return _eventStreamController!.stream; + return result; } catch (e) { Logger.error('开始连续语音识别失败: ${e.toString()}'); - _cleanupEventStream(); rethrow; } } @@ -192,11 +187,30 @@ class AzureAsrService extends GetxService implements AsrService { try { final bool result = await _channel.invokeMethod('stopContinuousRecognition'); - _cleanupEventStream(); return result; } catch (e) { Logger.error('停止连续语音识别失败: ${e.toString()}'); - _cleanupEventStream(); + rethrow; + } + } + + @override + Future> recognizeCallback() async { + if (!_isInitialized) { + await initialize(); + } + try { + _eventStreamController = StreamController.broadcast(); + + // 开始连续识别 + final bool result = await _channel.invokeMethod('recognizeCallback'); + if (!result) { + _cleanupEventStream(); + } + + return _eventStreamController!.stream; + } catch (e) { + Logger.error('开始连续语音识别失败: ${e.toString()}'); rethrow; } } @@ -415,31 +429,6 @@ class AzureAsrService extends GetxService implements AsrService { } } - @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 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 183f899db..66b67ddc6 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -842,18 +842,6 @@ class VolcanoAsrApiService implements AsrService { throw UnimplementedError(); } - @override - Future setPause() { - // TODO: implement setPause - throw UnimplementedError(); - } - - @override - Future setContinue() { - // TODO: implement setcontinue - throw UnimplementedError(); - } - @override Future disableBluetoothAudio() { // TODO: implement disableBluetoothAudio @@ -865,4 +853,10 @@ class VolcanoAsrApiService implements AsrService { // TODO: implement restoreOriginalAudioState throw UnimplementedError(); } + + @override + Future> recognizeCallback() { + // TODO: implement recognizeCallback + 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 871dccb99..59c2f5ed6 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -377,18 +377,6 @@ class VolcanoAsrService extends GetxService implements AsrService { throw UnimplementedError(); } - @override - Future setPause() { - // TODO: implement setPause - throw UnimplementedError(); - } - - @override - Future setContinue() { - // TODO: implement setcontinue - throw UnimplementedError(); - } - @override Future disableBluetoothAudio() { // TODO: implement disableBluetoothAudio @@ -400,4 +388,10 @@ class VolcanoAsrService extends GetxService implements AsrService { // TODO: implement restoreOriginalAudioState throw UnimplementedError(); } + + @override + Future> recognizeCallback() { + // TODO: implement recognizeCallback + 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 5dc8d3a31..959f98859 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -74,48 +74,48 @@ class XunfeiAsrService extends GetxService implements AsrService { } } - @override - Future> startContinuousRecognition( - bool audioSourceType) async { - if (!_isInitialized) { - await initialize(); - } - - try { - _eventStreamController = StreamController.broadcast(); - - await recorder?.dispose(); - recorder = AudioRecorder(); - Stream? stream = await recorder?.startStream( - const RecordConfig( - sampleRate: 16000, - encoder: AudioEncoder.pcm16bits, - numChannels: 1, - ), - ); - isRecording = true; - stream?.listen((data) { - if (isRecording) { - _buff = _appendToBuffer(data); - _originaBytes.add(data); - } - }); - await _tryConnecting(); - _timer = Timer.periodic(const Duration(milliseconds: 40), (timer) async { - //每40毫秒读取一次数据 - var frame = _getAudioData(); - if (frame.isNotEmpty) { - _channel?.sink.add(frame); - } - }); - - _isContinuousRecognitionActive = true; - return _eventStreamController!.stream; - } catch (e) { - Logger.error('开始连续语音识别失败: ${e.toString()}'); - rethrow; - } - } + // @override + // Future> startContinuousRecognition( + // bool audioSourceType) async { + // if (!_isInitialized) { + // await initialize(); + // } + + // try { + // _eventStreamController = StreamController.broadcast(); + + // await recorder?.dispose(); + // recorder = AudioRecorder(); + // Stream? stream = await recorder?.startStream( + // const RecordConfig( + // sampleRate: 16000, + // encoder: AudioEncoder.pcm16bits, + // numChannels: 1, + // ), + // ); + // isRecording = true; + // stream?.listen((data) { + // if (isRecording) { + // _buff = _appendToBuffer(data); + // _originaBytes.add(data); + // } + // }); + // await _tryConnecting(); + // _timer = Timer.periodic(const Duration(milliseconds: 40), (timer) async { + // //每40毫秒读取一次数据 + // var frame = _getAudioData(); + // if (frame.isNotEmpty) { + // _channel?.sink.add(frame); + // } + // }); + + // _isContinuousRecognitionActive = true; + // return _eventStreamController!.stream; + // } catch (e) { + // Logger.error('开始连续语音识别失败: ${e.toString()}'); + // rethrow; + // } + // } //写入音频数据到缓存区中 Uint8List _appendToBuffer(Uint8List newData) { @@ -302,26 +302,26 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future setPause() { - // TODO: implement setPause + Future disableBluetoothAudio() { + // TODO: implement disableBluetoothAudio throw UnimplementedError(); } @override - Future setContinue() { - // TODO: implement setcontinue + Future restoreOriginalAudioState() { + // TODO: implement restoreOriginalAudioState throw UnimplementedError(); } @override - Future disableBluetoothAudio() { - // TODO: implement disableBluetoothAudio + Future> recognizeCallback() { + // TODO: implement recognizeCallback throw UnimplementedError(); } @override - Future restoreOriginalAudioState() { - // TODO: implement restoreOriginalAudioState + Future startContinuousRecognition(bool audioSourceType) { + // TODO: implement startContinuousRecognition throw UnimplementedError(); } } diff --git a/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart b/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart index 53cffe68d..8b72d435e 100644 --- a/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart +++ b/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart @@ -49,6 +49,7 @@ class FTFTranslationController extends GetxController Get.put(XunfeiAsrService()); final TtsService _ttsService = Get.find(); _asrService = Get.find(); + _InfoRecorder(); } @override @@ -58,15 +59,19 @@ class FTFTranslationController extends GetxController super.onClose(); } - //开始录音 - Future _startRecorder() async { - final recognitionStream = - await _asrService.startContinuousRecognition(false); +//开始录音 + Future _InfoRecorder() async { + final recognitionStream = await _asrService.recognizeCallback(); _recognitionSubscription = recognitionStream.listen( _handleRecognitionEvent, ); } + //开始录音 + Future _startRecorder() async { + await _asrService.startContinuousRecognition(false); + } + //结束录音 Future _stopRecorder() async { await _asrService.stopContinuousRecognition(); diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index c2b9e4291..8ed39cb4d 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -293,7 +293,8 @@ class AgentController extends GetxController { currentPage.value = 1; hasMoreMessages.value = result.totalPages > 1; - Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); + Logger.i(TAG, + '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); _logMessageDetails(historyMessages); } else { // 没有历史记录时添加欢迎消息 @@ -316,7 +317,8 @@ class AgentController extends GetxController { Future loadMoreMessages() async { // 防止重复加载 if (isLoadingMore.value || !hasMoreMessages.value) { - Logger.d(TAG, '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreMessages=${hasMoreMessages.value}'); + Logger.d(TAG, + '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreMessages=${hasMoreMessages.value}'); return; } @@ -351,7 +353,8 @@ class AgentController extends GetxController { currentPage.value = nextPage; hasMoreMessages.value = nextPage < result.totalPages; - Logger.i(TAG, '成功加载 ${newMessages.length} 条历史消息,当前页: $nextPage/${result.totalPages}'); + Logger.i(TAG, + '成功加载 ${newMessages.length} 条历史消息,当前页: $nextPage/${result.totalPages}'); } else { // 没有更多消息 hasMoreMessages.value = false; @@ -414,7 +417,8 @@ class AgentController extends GetxController { void _setupScrollListener() { scrollController.addListener(() { // 检查是否滚动到顶部(因为使用了reverse: true,顶部实际是最大滚动位置) - if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 100) { // 当距离顶部100像素时触发加载更多 if (hasMoreMessages.value && !isLoadingMore.value) { Logger.d(TAG, '触发加载更多历史消息'); @@ -430,7 +434,8 @@ class AgentController extends GetxController { Logger.i(TAG, '手动触发加载更多历史消息'); loadMoreMessages(); } else { - Logger.w(TAG, '无法加载更多:hasMoreMessages=${hasMoreMessages.value}, isLoadingMore=${isLoadingMore.value}'); + Logger.w(TAG, + '无法加载更多:hasMoreMessages=${hasMoreMessages.value}, isLoadingMore=${isLoadingMore.value}'); } } @@ -979,7 +984,8 @@ class AgentController extends GetxController { permissionType: Permission.microphone, permissionName: 'microphonePermission'.tr, // 麦克风 explanationText: 'voiceRecognitionPermissionText'.tr, // 语音识别功能需要访问麦克风。 - permanentDenialText: 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 + permanentDenialText: + 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 settingsButtonText: 'goToSettings'.tr, // 去设置 cancelButtonText: 'cancel'.tr, // 取消 ); @@ -1005,13 +1011,14 @@ class AgentController extends GetxController { // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; - print('startVoiceInput'.tr); // 开始语音输入 + print('开始语音输入'); // 开始语音输入 final hasPermission = await PermissionUtil.instance.requestPermission( // context: Get.context!, permissionType: Permission.microphone, permissionName: 'microphonePermission'.tr, // 麦克风 explanationText: 'voiceTranslationPermissionText'.tr, // 语音识别与翻译功能需要访问麦克风。 - permanentDenialText: 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 + permanentDenialText: + 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 settingsButtonText: 'goToSettings'.tr, // 去设置 cancelButtonText: 'cancel'.tr, // 取消 ); @@ -1020,11 +1027,11 @@ class AgentController extends GetxController { return; } - try { - await AgentService.startConversation(); - } catch (e) { - Logger.e(TAG, '启动语音输入失败: $e'); - } + // try { + + // } catch (e) { + // Logger.e(TAG, '启动语音输入失败: $e'); + // } } // 停止语音输入 @@ -1276,32 +1283,32 @@ class AgentController extends GetxController { isPhoneCallMode.value = true; // 如果当前是文本模式,切换到语音模式???? - if (isListening.value) { - // 检查麦克风权限 - final hasPermission = await PermissionUtil.instance.requestPermission( - permissionType: Permission.microphone, - permissionName: 'microphonePermission'.tr, // 麦克风 - explanationText: 'phoneCallPermissionText'.tr, // 打电话功能需要访问麦克风进行语音通话。 - permanentDenialText: 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 - settingsButtonText: 'goToSettings'.tr, // 去设置 - cancelButtonText: 'cancel'.tr, // 取消 - ); - - if (!hasPermission) { - // 权限未获取,退出打电话模式并恢复之前状态 - isPhoneCallMode.value = false; - _previousInputMode = null; // 清除历史记录 - return; - } + // if (isListening.value) { + // 检查麦克风权限 + final hasPermission = await PermissionUtil.instance.requestPermission( + permissionType: Permission.microphone, + permissionName: '麦克风', + explanationText: '打电话功能需要访问麦克风进行语音通话。', + permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); - // 切换到语音模式并开始语音输入 - isTextInputMode.value = false; - // 打电话模式不使用按住说话模式,而是持续语音输入 - //isPushToTalkMode.value = false; - // 开始语音识别 - print('startPhoneCallMode'.tr); // 开始打电话模式,开始语音输入 - await AgentService.audioContinue(); + if (!hasPermission) { + // 权限未获取,退出打电话模式并恢复之前状态 + isPhoneCallMode.value = false; + _previousInputMode = null; // 清除历史记录 + return; } + + // 切换到语音模式并开始语音输入 + isTextInputMode.value = false; + // 打电话模式不使用按住说话模式,而是持续语音输入 + //isPushToTalkMode.value = false; + // 开始语音识别 + print("开始打电话模式,开始语音输入"); + await AgentService.startConversation(); + //} } // 结束打电话模式 @@ -1314,7 +1321,7 @@ class AgentController extends GetxController { // 停止语音输入 //await stopVoiceInput(); // 开始语音识别 - await AgentService.audioPause(); + await AgentService.stopConversation(); // 恢复之前的输入模式,而不是强制切换到文字模式 _restorePreviousInputMode(); } @@ -1409,9 +1416,9 @@ class AgentController extends GetxController { // 设置按住说话状态 isPushToTalkActive.value = true; - + //await AgentService.recognizeCallback(); // 开始语音识别 - await AgentService.audioContinue(); + await AgentService.startConversation(); // await startVoiceInput(); } @@ -1424,7 +1431,8 @@ class AgentController extends GetxController { // 清除按住说话状态(但保持按住说话模式) isPushToTalkActive.value = false; - await AgentService.audioPause(); + + await AgentService.stopConversation(); // 停止语音识别 // await stopVoiceInput(); } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 8e9523d93..189422b40 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -66,6 +66,7 @@ class MeetingRecordController extends GetxController _initializeComponents(); _setupFromArguments(); generateWaveData(); + _reinitializeAsrService(); } @override @@ -74,6 +75,18 @@ class MeetingRecordController extends GetxController super.onClose(); } + // 重新初始化ASR服务以支持新的语言 + Future _reinitializeAsrService() async { + try { + // 重新初始化ASR服务 + await _asrService.initialize(); + + Logger.info('ASR服务重新初始化成功'); + } catch (e) { + Logger.error('重新初始化ASR服务失败: ${e.toString()}'); + } + } + // 请求录音权限 Future _requestRecordPermission() async { try { @@ -132,6 +145,7 @@ class MeetingRecordController extends GetxController void _cleanupResources() { _timer?.cancel(); _recognitionSubscription?.cancel(); + _recognitionSubscription = null; titleEditingController.dispose(); titleFocusNode.dispose(); if (audioType.value != 0) { @@ -170,7 +184,13 @@ class MeetingRecordController extends GetxController } /// Toggles visibility of transcribed text content - void toggleTextDisplay() => showTextContent.toggle(); + Future toggleTextDisplay() async { + final recognitionStream = await _asrService.recognizeCallback(); + _recognitionSubscription?.cancel(); + _recognitionSubscription = + recognitionStream.listen(_handleRecognitionEvent); + showTextContent.toggle(); + } /// Toggles important mark status void toggleMarked() => isMarked.toggle(); @@ -211,10 +231,7 @@ class MeetingRecordController extends GetxController // Initialize speech recognition final recognitionStream = await _asrService.startContinuousRecognition(_audioSourceType); - _recognitionSubscription?.cancel(); - _recognitionSubscription = - recognitionStream.listen(_handleRecognitionEvent); - _asrService.setContinue(); + // Prepare storage directory final appDir = await appDocDir; final dir = Directory("${appDir.path}/MeetingAudio/"); @@ -263,9 +280,7 @@ class MeetingRecordController extends GetxController Future _resumeRecording() async { final recognitionStream = await _asrService.startContinuousRecognition(_audioSourceType); - _recognitionSubscription?.cancel(); - _recognitionSubscription = - recognitionStream.listen(_handleRecognitionEvent); + startTimer(); } diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 7a00a0d8b..a7d241f80 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -567,28 +567,28 @@ class SettingsView extends GetView { // color: isDarkMode // ? Colors.white.withOpacity(0.1) // : Colors.grey[200]), - // // BLE测试 - // _buildSimpleNavigationSetting( - // title: 'opus解码测试', - // subtitle: '测试opus解码', - // icon: Icons.bluetooth_searching, - // iconBgColor: isDarkMode - // ? Colors.green[900]!.withOpacity(0.3) - // : Colors.green[100]!, - // iconColor: - // isDarkMode ? Colors.green[300]! : Colors.green[600]!, - // titleColor: isDarkMode ? Colors.white : null, - // subtitleColor: isDarkMode ? Colors.white70 : null, - // onTap: () { - // Get.toNamed(Routes.opusTest); - // }, - // isDarkMode: isDarkMode, - // ), - // Divider( - // height: 1, - // color: isDarkMode - // ? Colors.white.withOpacity(0.1) - // : Colors.grey[200]), + // BLE测试 + _buildSimpleNavigationSetting( + title: 'opus解码测试', + subtitle: '测试opus解码', + icon: Icons.bluetooth_searching, + iconBgColor: isDarkMode + ? Colors.green[900]!.withOpacity(0.3) + : Colors.green[100]!, + iconColor: + isDarkMode ? Colors.green[300]! : Colors.green[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + Get.toNamed(Routes.opusTest); + }, + isDarkMode: isDarkMode, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), // BLE测试 // _buildSimpleNavigationSetting( diff --git a/lib/modules/speech_test/controllers/speech_test_controller.dart b/lib/modules/speech_test/controllers/speech_test_controller.dart index 6cc840e5a..541ef58e7 100644 --- a/lib/modules/speech_test/controllers/speech_test_controller.dart +++ b/lib/modules/speech_test/controllers/speech_test_controller.dart @@ -76,7 +76,10 @@ class SpeechTestController extends GetxController { // 监听TTS事件 _setupTtsEventListener(); - + // 获取识别事件流 - 这启动了异步识别过程 + final stream = await _asrService.recognizeCallback(); + // 设置事件监听 - 异步处理识别结果 + _setupAsrEventListener(stream); isInitialized.value = true; ttsStatus.value = '准备就绪'; recognitionStatus.value = '准备就绪'; @@ -346,9 +349,6 @@ class SpeechTestController extends GetxController { // 获取识别事件流 - 这启动了异步识别过程 final stream = await _asrService.startContinuousRecognition(_audioSourceType); - - // 设置事件监听 - 异步处理识别结果 - _setupAsrEventListener(stream); } catch (e) { recognitionStatus.value = '启动连续识别异常: $e'; Logger.error('启动连续识别异常: ${e.toString()}'); @@ -413,4 +413,4 @@ class SpeechTestController extends GetxController { super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index f6451b14f..1d060b311 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -122,7 +122,9 @@ class TranslationController extends GetxController { // 监听识别结果 if (activeSpeaker.value != 0) { isPlayback = false; - _asrService.setContinue(); + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); + lastActiveSpeaker.value = id; } else { if (fTFTranslationResult != '' && !isPlayback) { @@ -134,7 +136,7 @@ class TranslationController extends GetxController { } else if ((translationHistory.isEmpty || !translationHistory.last.isIntermediate) && currentMode.value == 'faceToFace') { - _asrService.setPause(); + _asrService.stopContinuousRecognition(); } } } @@ -305,6 +307,12 @@ class TranslationController extends GetxController { await _asrService.initialize( supportedLanguages: asrSupportedLanguages, ); + // 获取识别事件流 - 这启动了异步识别过程 + var recognitionStream = await _asrService.recognizeCallback(); + // 监听识别结果 + _recognitionSubscription?.cancel(); + _recognitionSubscription = + recognitionStream.listen(_handleRecognitionEvent); final bool storage = await _requestStoragePermission(); if (!storage) { Logger.d("Permission", "未授予存储权限,无法提供录音功能"); @@ -325,6 +333,7 @@ class TranslationController extends GetxController { void onClose() { stopRecognition(); _recognitionSubscription?.cancel(); + _recognitionSubscription = null; _voiceInteractionSubscription?.cancel(); _translationDebounceTimer?.cancel(); scrollController.dispose(); @@ -425,22 +434,18 @@ class TranslationController extends GetxController { _audioSourceType = false; isTtsEnabled.value = true; } - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); + if (currentMode.value == 'faceToFace') { - _asrService.setPause(); + _asrService.stopContinuousRecognition(); } else { await _ttsService.setAudioOutputDevice(0); - _asrService.setContinue(); + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); } isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { startRecording(); } - // 监听识别结果 - _recognitionSubscription?.cancel(); - _recognitionSubscription = - recognitionStream.listen(_handleRecognitionEvent); } catch (e) { isRecognizing.value = false; Logger.error('启动语音识别失败: ${e.toString()}'); @@ -473,10 +478,9 @@ class TranslationController extends GetxController { if (!isRecognizing.value) return; try { + restoreOriginalAudioState(); await _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); - _recognitionSubscription = null; isRecognizing.value = false; // 如果当前有中间结果,将其作为最终结果处理 @@ -523,7 +527,7 @@ class TranslationController extends GetxController { ); // 添加新项后滚动到底部 if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { - _asrService.setPause(); + _asrService.stopContinuousRecognition(); } else { translationHistory.add(newItem); } @@ -563,7 +567,7 @@ class TranslationController extends GetxController { _scrollToBottom(); saveTranslationHistory(); if (activeSpeaker.value == 0 && currentMode.value == 'faceToFace') { - _asrService.setPause(); + _asrService.stopContinuousRecognition(); } // 使用项目的时间戳进行翻译 translateText(text, translationHistory.last.timestamp, isFinal: true); @@ -582,7 +586,7 @@ class TranslationController extends GetxController { isIntermediate: false, ); if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { - _asrService.setPause(); + _asrService.stopContinuousRecognition(); } //else { translationHistory.add(newItem); translationHistory.refresh(); diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 9f946c361..113ae86cb 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -47,81 +47,84 @@ interface AgentServiceListener { */ object AgentService : CoroutineScope { private val TAG = "AgentService" - + // 协程相关 private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.IO + job - + // 上下文和监听器 private lateinit var context: Context private val listeners = Collections.synchronizedSet(mutableSetOf()) - + // Azure服务 - var azureAsrHelper: AzureAsrHelper? = null - + private var azureAsrHelper: AzureAsrHelper? = null + // 使用BytedanceTTS作为唯一的TTS实现 private var ttsService: AzureTtsHelper? = null - + // ChatAPI服务 - 使用新的ChatApiService private lateinit var chatApiService: ChatApiService - + // 聊天存储服务 private lateinit var chatStorageHelper: ChatStorageHelper - + // 会话ID,用于区分不同聊天上下文 private val sessionId = "default_agent" - + // 历史聊天消息缓存 private val historyMessages = JSONArray() - + // 系统提示词 private var systemPrompt = "" - + // 音频播放器 private var audioPlayer: AudioPlayer? = null // 初始化音频播放器 - + // 状态 - 使用原子类型确保线程安全 private val _isInitialized = AtomicBoolean(false) val isInitialized: Boolean get() = _isInitialized.get() - + private val _isRecognitionActive = AtomicBoolean(false) val isRecognitionActive: Boolean get() = _isRecognitionActive.get() - + private val _isTtsSpeaking = AtomicBoolean(false) val isTtsSpeaking: Boolean get() = _isTtsSpeaking.get() - + private val _hasSpeechDetected = AtomicBoolean(false) val hasSpeechDetected: Boolean get() = _hasSpeechDetected.get() - + private val _isAiStreaming = AtomicBoolean(false) val isAiStreaming: Boolean get() = _isAiStreaming.get() - + // 用于保护复杂状态操作的互斥锁 private val stateMutex = Mutex() - + // AI流生成相关 private var currentAiJob: Job? = null - private var currsessionId:String = "" + private var currsessionId: String = "" + // 空闲检测相关 private var idleCheckJob: Job? = null private val maxIdleSeconds = 10 // 最大空闲秒数 - + // 打断防抖相关 private var lastInterruptTime = 0L private val INTERRUPT_DEBOUNCE_MS = 300L - - private var audioSourceType= AzureAsrHelper.AudioSourceType.MICROPHONE + + var audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE + // 语气词过滤列表(中英文) private val FILLER_WORDS = setOf( // 中文语气词 - "嗯", "啊", "呃", "哦", "唉", "哎", "哈", "呵", "嘿", "喂", + "嗯", "啊", "呃", "哦", "唉", "哎", "哈", "呵", "嘿", "喂", "哇", "呀", "哟", "嘛", "吧", "呢", "啦", "咦", "嘻", "嗨", "额", "这个", "那个", "就是", "然后", // 英文语气词 - "um", "uh", "ah", "oh", "eh", "er", "hmm", "huh", "wow", + "um", "uh", "ah", "oh", "eh", "er", "hmm", "huh", "wow", "well", "like", "you know", "i mean", "so", "actually" ) + /** * 初始化系统提示词 */ @@ -142,6 +145,7 @@ object AgentService : CoroutineScope { 你与用户的交流风格应始终温暖、亲切、充满同理心,随时表达关怀与鼓励,让用户感受到真诚的陪伴与支持。 """.trimIndent() } + /** * 初始化 * @param appContext 应用上下文 @@ -149,9 +153,13 @@ object AgentService : CoroutineScope { * @param config 配置参数,包含所需的所有API密钥和设置 * @return 是否初始化成功 */ - fun initialize(appContext: Context, config: Map, isExternalActive: Boolean): Boolean { + fun initialize( + appContext: Context, + config: Map, + isExternalActive: Boolean + ): Boolean { if (isInitialized) return true - + try { // 初始化上下文和服务 this.context = appContext.applicationContext @@ -169,17 +177,28 @@ object AgentService : CoroutineScope { } // 初始化Azure ASR azureAsrHelper = AzureAsrHelper(context).apply { - initialize( - subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", - region = config["azureSpeechRegion"]?.toString() ?: "", - audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL - else AzureAsrHelper.AudioSourceType.MICROPHONE - ) + // 使用协程处理异步初始化 + CoroutineScope(Dispatchers.IO).launch { + val asrInitSuccess = initialize( + subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", + region = config["azureSpeechRegion"]?.toString() ?: "", + audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL + else AzureAsrHelper.AudioSourceType.MICROPHONE + ) + + if (asrInitSuccess) { + withContext(Dispatchers.Main) { + recognizeCallback() } - + } else { + Log.w(TAG, "Azure ASR初始化失败") + } + } + } + // 初始化TTS服务 initializeTtsEngine(config) - + // 初始化ChatAPI服务 chatApiService.initialize( config["openaiApiKey"]?.toString() ?: "", @@ -188,10 +207,12 @@ object AgentService : CoroutineScope { config["openaivisionModel"]?.toString() ?: "", config["mcpServer"]?.toString() ?: "" ) + + // 加载最近的聊天记录 loadChatHistory() - + _isInitialized.set(true) return true } catch (e: Exception) { @@ -199,7 +220,7 @@ object AgentService : CoroutineScope { return false } } - + /** * 释放所有资源 */ @@ -209,22 +230,22 @@ object AgentService : CoroutineScope { stopTts() stopAiStream() stopIdleCheck() - + audioPlayer?.release() audioPlayer = null - + azureAsrHelper?.dispose() - + // 释放ChatAPI服务 if (::chatApiService.isInitialized) { chatApiService.cancelCurrentStream() chatApiService.dispose() } - + // 释放TTS服务 ttsService?.release() ttsService = null - + job.cancel() clearListeners() _isInitialized.set(false) @@ -232,7 +253,7 @@ object AgentService : CoroutineScope { Log.e(TAG, "释放资源异常: ${e.message}") } } - + /** * 初始化TTS引擎,只使用火山语音引擎 */ @@ -242,17 +263,17 @@ object AgentService : CoroutineScope { ttsService?.stop() ttsService = null } - + try { // 从配置中获取TTS参数 val ttsAppId = config["volcanoAppId"]?.toString() ?: "" val ttsAppToken = config["volcanoToken"]?.toString() ?: "" val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN" - + // 创建并初始化BytedanceTTS val azureTtsHelper = AzureTtsHelper(context) ttsService = azureTtsHelper - + // 初始化Bytedance TTS val success = azureTtsHelper.initialize( ttsAppId = "", // Azure TTS不需要appId @@ -260,11 +281,11 @@ object AgentService : CoroutineScope { ttsResource = config["azureSpeechRegion"]?.toString() ?: "", // Azure需要region信息 language = ttsLanguage ) - + if (!success) { Log.e(TAG, "Bytedance TTS服务初始化失败") } - + // 添加TTS事件监听 ttsService?.addListener(object : TtsEventListener { override fun onEvent(event: TtsEvent) { @@ -274,45 +295,55 @@ object AgentService : CoroutineScope { restartIdleCheck() sendEvent("tts_started", mapOf("status" to "started")) } + TtsEventType.SYNTHESIS_COMPLETED -> { _isTtsSpeaking.set(false) restartIdleCheck() sendEvent("tts_completed", mapOf("status" to "completed")) } + TtsEventType.SYNTHESIS_CANCELED -> { _isTtsSpeaking.set(false) restartIdleCheck() sendEvent("tts_canceled", mapOf("status" to "canceled")) } + TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容 restartIdleCheck() sendEvent("playback_started", mapOf("status" to "playback_started")) // 发送AI说话开始广播 - val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply { - setPackage(context.packageName) - } + val intent = + Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply { + setPackage(context.packageName) + } context.sendBroadcast(intent) } + TtsEventType.PLAYBACK_COMPLETED -> { //结束AI播报内容 restartIdleCheck() sendEvent("playback_completed", mapOf("status" to "playback_completed")) // 发送AI说话开始广播 - val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_END").apply { - setPackage(context.packageName) - } + val intent = + Intent("com.yunqiinnovation.music_service.AI_SAY_END").apply { + setPackage(context.packageName) + } context.sendBroadcast(intent) } + TtsEventType.ERROR -> { _isTtsSpeaking.set(false) restartIdleCheck() val params = event.params val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR" val message = params["errorMessage"] as? String ?: "未知错误" - sendEvent("error", mapOf( - "code" to "TTS_ERROR", - "message" to "语音合成错误: $code, $message" - )) + sendEvent( + "error", mapOf( + "code" to "TTS_ERROR", + "message" to "语音合成错误: $code, $message" + ) + ) } + else -> { // 处理其他类型的事件 } @@ -323,7 +354,7 @@ object AgentService : CoroutineScope { Log.e(TAG, "初始化TTS引擎失败: ${e.message}") } } - + /** * 添加事件监听器 * @param listener 要添加的监听器 @@ -331,7 +362,7 @@ object AgentService : CoroutineScope { fun addListener(listener: AgentServiceListener) { listeners.add(listener) } - + /** * 移除事件监听器 * @param listener 要移除的监听器 @@ -339,28 +370,31 @@ object AgentService : CoroutineScope { fun removeListener(listener: AgentServiceListener) { listeners.remove(listener) } - + /** * 移除所有事件监听器 */ fun clearListeners() { listeners.clear() } - + /** * 启动空闲检测 */ private fun startIdleCheck() { stopIdleCheck() // 先停止现有的检查 if (!isRecognitionActive) return - + idleCheckJob = launch { try { delay(maxIdleSeconds * 1000L) - + if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { - //stopRecognition() - sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds)) + stopRecognition() + sendEvent( + "auto_stop", + mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds) + ) } } catch (e: CancellationException) { // 协程被取消,正常行为 @@ -369,7 +403,7 @@ object AgentService : CoroutineScope { } } } - + /** * 停止空闲检测 */ @@ -377,7 +411,7 @@ object AgentService : CoroutineScope { idleCheckJob?.cancel() idleCheckJob = null } - + /** * 重启空闲检测 * 当状态发生变化时调用 @@ -387,7 +421,7 @@ object AgentService : CoroutineScope { startIdleCheck() } } - + /** * 开始语音识别 */ @@ -396,244 +430,236 @@ object AgentService : CoroutineScope { Log.d(TAG, "服务未初始化") return false } - if(!(azureAsrHelper!!.isPaused)&&isExternalActive) - { - Log.d(TAG, "麦克风音频未暂停,无法设置") - BleService.closeCodec() - return false - } - if (isRecognitionActive) return true - + Log.d("TAG", "开始语音识别startRecognition?") + stopRecognition() + BleService.openEncoder() + // if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){ + // Log.d(TAG, "开始语音识别stopRecognition") + // // BleService.closeCodec() + // stopRecognition() + + // } + // if((isExternalActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE)||!isRecognitionActive) + // { + // Log.d(TAG, "开始语音识别BleService.openEncoder") + // BleService.openEncoder() + + // } + // if(isRecognitionActive) + // { + // Log.d(TAG, "开始语音识别azureAsrHelper?.startContinuousRecognition(audioSourceType)") + // return true + // } // 检查录音权限 if (!isExternalActive && !checkRecordAudioPermission()) { Log.e(TAG, "需要录音权限") sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) return false } - + _isRecognitionActive.set(true) _hasSpeechDetected.set(false) - + try { - audioSourceType = if (isExternalActive) { + audioSourceType = if (isExternalActive) { AzureAsrHelper.AudioSourceType.EXTERNAL } else { AzureAsrHelper.AudioSourceType.MICROPHONE } - azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { + Log.d("TAG", "azureAsrHelper?.startContinuousRecognition") + azureAsrHelper?.startContinuousRecognition(audioSourceType) + + return true + } catch (e: Exception) { + _isRecognitionActive.set(false) + Log.e(TAG, "启动语音识别失败: ${e.message}") + sendEvent( + "error", mapOf( + "code" to "RECOGNITION_START_ERROR", + "message" to e.message.toString() + ) + ) + return false + } + } + + fun recognizeCallback(): Boolean { + try { + Log.d(TAG, "recognizeCallback") + val success = azureAsrHelper?.setupEventListeners(object : + AzureAsrHelper.ContinuousRecognizeCallback { override fun onRecognizing(recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()) { // 检测到语音,更新状态 val previousHasSpeech = hasSpeechDetected _hasSpeechDetected.set(true) - + if (!previousHasSpeech) { restartIdleCheck() } - - sendEvent("recognizing", mapOf( - "text" to recognizing, - "language" to detectedLanguage - )) - + + sendEvent( + "recognizing", mapOf( + "text" to recognizing, + "language" to detectedLanguage + ) + ) + // 如果TTS正在播放或AI正在生成,检查是否需要打断 if (isTtsSpeaking || isAiStreaming) { val currentTime = System.currentTimeMillis() - + // 防抖处理:避免过于频繁的打断 if (currentTime - lastInterruptTime < INTERRUPT_DEBOUNCE_MS) { return } - + // 简单过滤:太短的内容可能是噪音 if (recognizing.trim().length < 2) { return } - + // 过滤纯语气词 val trimmedText = recognizing.trim().lowercase() if (FILLER_WORDS.contains(trimmedText)) { return } - + // 执行打断 lastInterruptTime = currentTime interruptCurrentResponse() } } } - + override fun onResult(result: String, detectedLanguage: String) { if (result.isNotEmpty()) { - sendEvent("recognition_result", mapOf( - "text" to result, - "language" to detectedLanguage - )) - + sendEvent( + "recognition_result", mapOf( + "text" to result, + "language" to detectedLanguage + ) + ) + processWithChatApi(result) } - + // 重置状态,继续识别 val previousHasSpeech = hasSpeechDetected _hasSpeechDetected.set(false) - + if (previousHasSpeech) { restartIdleCheck() } } - + override fun onSessionStarted() { sendEvent("recognition_started", mapOf("status" to "started")) startIdleCheck() - Log.d(TAG, "onSessionStarted") audioPlayer?.playAudio(R.raw.start) } - + override fun onSessionStopped() { sendEvent("recognition_stopped", mapOf("status" to "stopped")) - _isRecognitionActive.set(false) stopIdleCheck() audioPlayer?.playAudio(R.raw.stop) } - + override fun onCanceled(reason: String, errorDetails: String) { _isRecognitionActive.set(false) stopIdleCheck() - + Log.d(TAG, "recognition_canceled:") + recognizeCallback() BleService.closeCodec() - sendEvent("recognition_canceled", mapOf( - "reason" to reason, - "details" to errorDetails - )) + sendEvent( + "recognition_canceled", mapOf( + "reason" to reason, + "details" to errorDetails + ) + ) } - + override fun onError(error: String) { _isRecognitionActive.set(false) stopIdleCheck() - +recognizeCallback() BleService.closeCodec() - Log.e(TAG, "语音识别错误: $error") - sendEvent("error", mapOf( - "code" to "RECOGNITION_ERROR", - "message" to error - )) + Log.d(TAG, "语音识别错误: $error") + sendEvent( + "error", mapOf( + "code" to "RECOGNITION_ERROR", + "message" to error + ) + ) } - }, audioSourceType) + }) return true } catch (e: Exception) { - _isRecognitionActive.set(false) - Log.e(TAG, "启动语音识别失败: ${e.message}") - sendEvent("error", mapOf( - "code" to "RECOGNITION_START_ERROR", - "message" to e.message.toString() - )) return false } } - + fun pushAudioData(data: ByteArray) { - azureAsrHelper?.pushAudioData(data) + azureAsrHelper?.audioStream?.saveAudioDataTo(data) } + /** * 停止语音识别 */ fun stopRecognition() { - if (!isRecognitionActive) return - + if (!isRecognitionActive) return + try { azureAsrHelper?.stopContinuousRecognition() BleService.closeCodec() - _isRecognitionActive.set(false) + _isRecognitionActive.set(false) stopIdleCheck() + return } catch (e: Exception) { Log.e(TAG, "停止语音识别异常: ${e.message}") _isRecognitionActive.set(false) stopIdleCheck() + return } } - /** - * 继续音频输入 - */ - fun audioContinue() { - - - - try { - if(audioSourceType== AzureAsrHelper.AudioSourceType.EXTERNAL) - { - - BleService.closeCodec() - stopRecognition() - startRecognition(false) - } - - // if (!isRecognitionActive) { - - - // } - - azureAsrHelper?.audioContinue() - //BleService.closeCodec() - //_isRecognitionActive.set(false) - //stopIdleCheck() - } catch (e: Exception) { - Log.e(TAG, "暂停音频输入异常: ${e.message}") - // _isRecognitionActive.set(false) - //stopIdleCheck() - } - } - /** - * 暂停音频输入 - */ - fun audioPause() { - if (!isRecognitionActive) return - - try { - azureAsrHelper?.audioPause() - Log.d(TAG, "暂停音频输入") - BleService.closeCodec() - //_isRecognitionActive.set(false) - //stopIdleCheck() - } catch (e: Exception) { - Log.e(TAG, "暂停音频输入异常: ${e.message}") - // _isRecognitionActive.set(false) - //stopIdleCheck() - } - } + + /** * 打断当前响应 * 停止TTS播放和AI流输出 */ fun interruptCurrentResponse() { - if(isAiStreaming || isTtsSpeaking) { + if (isAiStreaming || isTtsSpeaking) { val startTime = System.currentTimeMillis() - + // 并行执行停止操作,加快响应速度 stopTts() stopAiStream() - + // 记录打断耗时 - + val duration = System.currentTimeMillis() - startTime - sendEvent("response_interrupted", mapOf( - "status" to "interrupted", - "duration_ms" to duration - )) - + sendEvent( + "response_interrupted", mapOf( + "status" to "interrupted", + "duration_ms" to duration + ) + ) + } } - + /** * 停止AI流输出 */ - fun stopAiStream() { + fun stopAiStream() { if (isAiStreaming) { try { // 先更新状态,避免回调时的状态不一致 _isAiStreaming.set(false) - + // 取消当前AI生成任务 currentAiJob?.cancel() currentAiJob = null @@ -641,7 +667,7 @@ object AgentService : CoroutineScope { // 通知ChatAPI服务终止当前流式请求 chatApiService.cancelCurrentStream() - + } catch (e: Exception) { Log.e(TAG, "停止AI流输出异常", e) // 确保状态被重置,即使发生异常 @@ -650,7 +676,7 @@ object AgentService : CoroutineScope { } } } - + /** * 处理文本输入 */ @@ -660,28 +686,28 @@ object AgentService : CoroutineScope { sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) return false } - + if (text.isEmpty()) { Log.e(TAG, "文本输入不能为空") sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) return false } - + // 使用ChatAPI处理文本 processWithChatApi(text, speakResponse) return true } - + /** * 使用ChatAPI处理语音识别结果 */ private fun processWithChatApi(text: String) { processWithChatApi(text, true) } - + /** * 使用ChatAPI处理文本消息 - * + * * @param text 用户输入文本 * @param speakResponse 是否使用TTS朗读回复 */ @@ -690,15 +716,15 @@ object AgentService : CoroutineScope { speakResponse: Boolean = true ) { Log.d(TAG, "用户问题: $text") - + // 创建用户文本消息并处理 val userMessage = createUserMessage(text) processWithChatApiInternal(userMessage, text, speakResponse) } - + /** * 使用ChatAPI处理图片 - * + * * @param imageBase64 Base64编码的图片数据 * @param text 可选的文本描述或问题 * @param speakResponse 是否朗读回复 @@ -715,10 +741,10 @@ object AgentService : CoroutineScope { val displayText = text.ifEmpty { "[图片]" } processWithChatApiInternal(userMessage, displayText, speakResponse, true) } - + /** * 内部方法:通用的ChatAPI处理逻辑 - * + * * @param userMessage 用户消息(可以是文本或图片格式) * @param displayText 用于显示和存储的文本 * @param speakResponse 是否朗读回复 @@ -743,11 +769,11 @@ object AgentService : CoroutineScope { currsessionId = UUID.randomUUID().toString() // 设置状态为正在流式输出 _isAiStreaming.set(true) - Log.d(TAG, "processWithChatApiInternal") - audioPlayer?.playAudio(R.raw.await, true,0.3f) + Log.d(TAG, "processWithChatApiInternal") + audioPlayer?.playAudio(R.raw.await, true, 0.3f) // 使用历史记录作为上下文发送到OpenAI val responseBuilder = StringBuilder() - var aiMetadata:String = "" + var aiMetadata: String = "" var userMetadata = JSONObject() var reply = true; //是否回复 var broadcast = true //是否播发 @@ -759,25 +785,25 @@ object AgentService : CoroutineScope { // 添加系统提示词到历史消息的副本中 val messagesWithSystemPrompt = JSONArray() - + // 先添加系统提示词 - if (systemPrompt.isNotEmpty()) { - val location = LocationService.getCachedLocation()?: mapOf( - "user_location" to "定位未开启", - "user_lat" to "定位未开启", - "user_lng" to "定位未开启", - ) - val _systemPrompt = Utils.fillTemplate(systemPrompt,location) - messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) - // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") - } - + if (systemPrompt.isNotEmpty()) { + val location = LocationService.getCachedLocation() ?: mapOf( + "user_location" to "定位未开启", + "user_lat" to "定位未开启", + "user_lng" to "定位未开启", + ) + val _systemPrompt = Utils.fillTemplate(systemPrompt, location) + messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) + // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") + } + // 再添加历史消息 for (i in 0 until historyMessages.length()) { messagesWithSystemPrompt.put(historyMessages.getJSONObject(i)) } messagesWithSystemPrompt.put(userMessage) - + // 将用户消息添加到历史记录(注意:要去掉图片数据再保存) if (userMessage.has("content")) { val content = userMessage.get("content") @@ -810,9 +836,9 @@ object AgentService : CoroutineScope { currsessionId, messagesWithSystemPrompt, object : StreamCallback { - override fun onToken(sessionid:String,token: String) { + override fun onToken(sessionid: String, token: String) { try { - if (sessionid != currsessionId){ + if (sessionid != currsessionId) { return } responseBuilder.append(token) @@ -827,8 +853,8 @@ object AgentService : CoroutineScope { Log.e(TAG, "liwei--------------- AI Call onToken 异常", e) } } - - override fun onComplete(sessionid:String) { + + override fun onComplete(sessionid: String) { try { // 视情况决定是否朗读回复 if (speakResponse && reply && broadcast && sessionid == currsessionId){ @@ -866,8 +892,8 @@ object AgentService : CoroutineScope { Log.e(TAG, "liwei--------------- AI Call onComplete 异常", e) } } - - override fun onError(sessionid:String,e: Exception) { + + override fun onError(sessionid: String, e: Exception) { try { Log.e(TAG, "AI处理出错", e) sendEvent( @@ -880,28 +906,34 @@ object AgentService : CoroutineScope { // 标记AI流式输出已完成 _isAiStreaming.set(false) currentAiJob = null - }catch (e: Exception) { + } catch (e: Exception) { Log.e(TAG, "liwei--------------- AI Call onError 异常", e) } } - - override fun onFunctionCall(sessionid:String,call: JSONObject) { + + override fun onFunctionCall(sessionid: String, call: JSONObject) { try { // audioPlayer?.playAudio(R.raw.calling, true) val name = call.getString("name") - sendEvent("function_call", mapOf( - "name" to name, - "arguments" to call.toString() - )) - if(name == "exit_interaction") { + sendEvent( + "function_call", mapOf( + "name" to name, + "arguments" to call.toString() + ) + ) + if (name == "exit_interaction") { stopRecognition() } } catch (e: Exception) { Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e) } } - - override fun onFunctionCallResult(sessionid:String,functionCall: JSONObject, functionCallResult: JSONObject) { + + override fun onFunctionCallResult( + sessionid: String, + functionCall: JSONObject, + functionCallResult: JSONObject + ) { try { audioPlayer?.stopAudio() val name = functionCall.get("name") as String; @@ -917,27 +949,33 @@ object AgentService : CoroutineScope { ) ) } catch (e: Exception) { - Log.e(TAG, "liwei--------------- AI Call onFunctionCallResult 异常", e) + Log.e( + TAG, + "liwei--------------- AI Call onFunctionCallResult 异常", + e + ) } } } ) - + } catch (e: Exception) { val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" Log.e(TAG, "liwei--------------- AI处理出错", e) - sendEvent("error", mapOf( - "code" to errorType, - "message" to e.message.toString() - )) - + sendEvent( + "error", mapOf( + "code" to errorType, + "message" to e.message.toString() + ) + ) + // 确保状态被重置 _isAiStreaming.set(false) currentAiJob = null } } } - + /** * 加载最近的聊天历史记录 */ @@ -949,18 +987,18 @@ object AgentService : CoroutineScope { historyMessages.remove(0) } } - + // 获取最近消息 val recentMessages = chatStorageHelper.getRecentMessages(sessionId, 10) if (recentMessages.isEmpty()) { return } - + // 添加消息到历史记录 for (message in recentMessages) { val sender = message["sender"] as String val content = message["message"] as String - + if (sender == "user") { addToHistoryMessages(createUserMessage(content)) } else if (sender == "assistant") { @@ -971,7 +1009,7 @@ object AgentService : CoroutineScope { Log.e(TAG, "加载聊天历史失败: ${e.message}") } } - + /** * 添加消息到历史记录,保持最近10条 */ @@ -979,47 +1017,52 @@ object AgentService : CoroutineScope { synchronized(historyMessages) { // 添加新消息 historyMessages.put(message) - + // 如果超过10条,删除最早的消息 while (historyMessages.length() > 10) { historyMessages.remove(0) } } } - + /** * TTS播放函数 - * + * * @param text 要播放的文本 */ fun speakText(text: String) { if (text.isEmpty()) return - + // 更新状态 _isTtsSpeaking.set(true) restartIdleCheck() // 状态变化,重启检测 - + // 直接调用TTS,无需协程包装 ttsService?.speakOnce(text) } - + /** * 停止TTS播放 */ fun stopTts() { if (isTtsSpeaking) { ttsService?.stop() - + _isTtsSpeaking.set(false) restartIdleCheck() // 状态变化,重启检测 sendEvent("tts_stopped", mapOf("status" to "stopped")) } } - + /** * 保存聊天消息 */ - private fun saveChatMessage(userMessage: String, assistantMessage: String,aiMetadata : String,userMetadata : String) { + private fun saveChatMessage( + userMessage: String, + assistantMessage: String, + aiMetadata: String, + userMetadata: String + ) { // 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中 launch(Dispatchers.IO) { try { @@ -1030,7 +1073,7 @@ object AgentService : CoroutineScope { sender = "user", metadata = userMetadata ) - + if (userMessageId != -1L) { if (assistantMessage.isNotEmpty() || aiMetadata.isNotEmpty()) { // 保存AI回复 @@ -1054,7 +1097,7 @@ object AgentService : CoroutineScope { } } } - + /** * 清除聊天历史 */ @@ -1062,7 +1105,7 @@ object AgentService : CoroutineScope { launch { try { val success = chatStorageHelper.deleteMessages(sessionId) - + if (success) { synchronized(historyMessages) { while (historyMessages.length() > 0) { @@ -1072,7 +1115,7 @@ object AgentService : CoroutineScope { } else { Log.e(TAG, "清除聊天历史失败") } - + // 由于launch已在Main上下文中,可以直接调用回调 callback(success) } catch (e: Exception) { @@ -1082,7 +1125,7 @@ object AgentService : CoroutineScope { } } } - + /** * 发送事件 */ @@ -1098,7 +1141,7 @@ object AgentService : CoroutineScope { } } } - + /** * 检查录音权限 */ @@ -1107,7 +1150,7 @@ object AgentService : CoroutineScope { val result = context.checkCallingOrSelfPermission(permission) return result == android.content.pm.PackageManager.PERMISSION_GRANTED } - + /** * 创建用户消息 */ @@ -1117,7 +1160,7 @@ object AgentService : CoroutineScope { put("content", content) } } - + /** * 创建系统消息 */ @@ -1127,7 +1170,7 @@ object AgentService : CoroutineScope { put("content", content) } } - + /** * 创建助手消息 */ @@ -1137,7 +1180,7 @@ object AgentService : CoroutineScope { put("content", content) } } - + /** * 创建带图片的用户消息 */ @@ -1157,35 +1200,42 @@ object AgentService : CoroutineScope { }) }) } - + return JSONObject().apply { put("role", "user") put("content", content) } } - + /** * 处理图片输入 */ - fun processImageInput(imagePath: String, imageUrl: String,text: String = "", speakResponse: Boolean = false): Boolean { + fun processImageInput( + imagePath: String, + imageUrl: String, + text: String = "", + speakResponse: Boolean = false + ): Boolean { if (!isInitialized) { Log.e(TAG, "服务未初始化") sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) return false } - + if (imagePath.isEmpty()) { Log.e(TAG, "图片路径不能为空") sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空")) return false } - + // 通知开始处理图片 - sendEvent("image_processing", mapOf( - "status" to "processing", - "imagePath" to imagePath - )) - + sendEvent( + "image_processing", mapOf( + "status" to "processing", + "imagePath" to imagePath + ) + ) + // 处理图片 launch { try { @@ -1202,21 +1252,25 @@ object AgentService : CoroutineScope { val userMessage = createUserMessageWithImage(text, imageUrl) // 图片描述用于存储 val displayText = text.ifEmpty { "[图片]" } - + // 通知图片准备完成 - sendEvent("image_ready", mapOf( - "status" to "ready", - "imagePath" to imagePath - )) + sendEvent( + "image_ready", mapOf( + "status" to "ready", + "imagePath" to imagePath + ) + ) Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}") // 处理消息并传入存储路径 processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath) } catch (e: Exception) { Log.e(TAG, "处理图片失败: ${e.message}") - sendEvent("error", mapOf( - "code" to "IMAGE_PROCESSING_ERROR", - "message" to e.message.toString() - )) + sendEvent( + "error", mapOf( + "code" to "IMAGE_PROCESSING_ERROR", + "message" to e.message.toString() + ) + ) } } return true @@ -1229,7 +1283,7 @@ object AgentService : CoroutineScope { private var mediaPlayer: MediaPlayer? = null private var isInitialized = false private val playbackLock = Object() // 添加同步锁 - + /** * 安全检查 MediaPlayer 是否正在播放 */ @@ -1244,7 +1298,7 @@ object AgentService : CoroutineScope { false } } - + /** * 播放音频资源 * @param resId 资源ID @@ -1254,7 +1308,7 @@ object AgentService : CoroutineScope { fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) { synchronized(playbackLock) { try { - Log.d(TAG, "playAudio${isLooping}") + Log.d(TAG, "playAudio${isLooping}") // 强制停止并释放之前的资源 forceStop() @@ -1291,14 +1345,14 @@ object AgentService : CoroutineScope { } } } - + /** * 停止音频播放 */ fun stopAudio() { synchronized(playbackLock) { try { - Log.d(TAG, "stopAudio") + Log.d(TAG, "stopAudio") mediaPlayer?.apply { if (isMediaPlayerPlaying()) { stop() @@ -1316,7 +1370,7 @@ object AgentService : CoroutineScope { } } } - + /** * 强制停止播放(不释放资源) */ @@ -1341,7 +1395,7 @@ object AgentService : CoroutineScope { Log.w(TAG, "强制停止过程中异常: ${e.message}") } } - + /** * 释放资源 */ @@ -1357,14 +1411,14 @@ object AgentService : CoroutineScope { Log.w(TAG, "停止播放时状态异常: ${e.message}") } } - + // 安全重置 try { reset() } catch (e: IllegalStateException) { Log.w(TAG, "重置 MediaPlayer 时状态异常: ${e.message}") } - + // 安全释放 - 修复递归调用问题 try { release() // 这里调用的是 MediaPlayer.release(),不是递归 @@ -1381,7 +1435,7 @@ object AgentService : CoroutineScope { } } } - + /** * 强制释放资源,不进行状态检查 */ @@ -1408,7 +1462,7 @@ object AgentService : CoroutineScope { } } } - + /** * 检查是否正在播放 */ @@ -1418,11 +1472,14 @@ object AgentService : CoroutineScope { } } } - + /* * 自动播放音乐 * */ - fun autoHandleFcunCallResult(toolname:String,functionCallResult: JSONObject): Map{ + fun autoHandleFcunCallResult( + toolname: String, + functionCallResult: JSONObject + ): Map { println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}") try { val contentStr = functionCallResult.optString("context") @@ -1435,10 +1492,10 @@ object AgentService : CoroutineScope { var notreply = false; if (textStr.isNotEmpty()) { val meta = JSONObject(textStr) - if (meta.has("broadcast")){ + if (meta.has("broadcast")) { broadcast = meta["broadcast"] as Boolean } - if (meta.has("notreply")){ + if (meta.has("notreply")) { notreply = meta["notreply"] as Boolean } metadata[toolname] = meta // 现在可以赋值 @@ -1501,13 +1558,13 @@ object AgentService : CoroutineScope { val end = cardNavigation.optString("end", "") processNavigation(start, end); } - return mapOf("meta" to metaStr, "broadcast" to broadcast,"notreply" to notreply); + return mapOf("meta" to metaStr, "broadcast" to broadcast, "notreply" to notreply); } } catch (e: Exception) { // 捕获其他类型的异常(可选) println("为解析到卡片数据!") } - return mapOf("meta" to "", "broadcast" to true,"notreply" to false); + return mapOf("meta" to "", "broadcast" to true, "notreply" to false); } /** @@ -1520,7 +1577,7 @@ object AgentService : CoroutineScope { * @param singer 歌手名称,默认为空 * @return 是否成功开始处理 */ - fun processMusicPlay(song: Map){ + fun processMusicPlay(song: Map) { // 在其他 Service、BroadcastReceiver 或 Application 中调用 Log.i(TAG, "播放音乐: ${song}") MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) @@ -1535,10 +1592,14 @@ object AgentService : CoroutineScope { * @param singer 歌手,默认为false * @return 是否成功开始处理 */ - fun processMusicPlayList(songs:List>){ + fun processMusicPlayList(songs: List>) { // 如果 playlist 中有有效的歌曲,开始播放 if (songs.isNotEmpty()) { - MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs) + MusicServiceStarter.startServiceWithPlaylist( + context, + command = "playlist", + songs = songs + ) } else { Log.e(TAG, "音乐列表为空,未启动播放服务") } @@ -1552,10 +1613,15 @@ object AgentService : CoroutineScope { * @param singer 歌手,默认为false * @return 是否成功开始处理 */ - fun processNavigation(start:String,end:String){ + fun processNavigation(start: String, end: String) { // 如果 playlist 中有有效的歌曲,开始播放 if (!start.isNullOrEmpty() && !end.isNullOrEmpty()) { - NavigationServiceHelper.startNavigation(context,"start", startpos = start, endpos = end) + NavigationServiceHelper.startNavigation( + context, + "start", + startpos = start, + endpos = end + ) } else { Log.i(TAG, "启动导航服务失败") } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 579f20d23..819bd6ad7 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -15,7 +15,8 @@ import kotlinx.coroutines.launch import android.content.Intent import android.os.Build -class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, AgentServiceListener { +class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, + AgentServiceListener { private val TAG = "AgentServicePlugin" private lateinit var context: Context private lateinit var methodChannel: MethodChannel @@ -24,12 +25,16 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext - methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.agent_service") + methodChannel = + MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.agent_service") methodChannel.setMethodCallHandler(this) - - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.agent_service/events") + + eventChannel = EventChannel( + flutterPluginBinding.binaryMessenger, + "com.yunqiinnovation.agent_service/events" + ) eventChannel.setStreamHandler(this) - + // 添加监听器 AgentService.addListener(this) @@ -45,37 +50,42 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("INVALID_ARGUMENTS", "配置参数不能为空", null) return } - + Log.d(TAG, "启动BleMediaService,配置参数: $config") - + // 创建Intent启动BleMediaService val intent = Intent(context, BleMediaService::class.java) - + // 将参数添加到Intent - config["azureSpeechKey"]?.toString()?.let { intent.putExtra("azureSpeechKey", it) } - config["azureSpeechRegion"]?.toString()?.let { intent.putExtra("azureSpeechRegion", it) } + config["azureSpeechKey"]?.toString() + ?.let { intent.putExtra("azureSpeechKey", it) } + config["azureSpeechRegion"]?.toString() + ?.let { intent.putExtra("azureSpeechRegion", it) } config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) } - config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) } + config["openaiBaseUrl"]?.toString() + ?.let { intent.putExtra("openaiBaseUrl", it) } config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) } - config["openaivisionModel"]?.toString()?.let { intent.putExtra("openaivisionModel", it) } + config["openaivisionModel"]?.toString() + ?.let { intent.putExtra("openaivisionModel", it) } config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) } config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) } - + // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) } else { context.startService(intent) } - + result.success(true) } catch (e: Exception) { Log.e(TAG, "启动AgentService失败", e) result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null) } } + "stopAgentService" -> { try { val intent = Intent(context, BleMediaService::class.java) @@ -86,6 +96,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null) } } + "startConversation" -> { try { val isStarted = AgentService.startRecognition(false) @@ -95,6 +106,11 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("START_ERROR", "启动对话失败: ${e.message}", null) } } + "recognizeCallback" -> { + val isStarted = AgentService.recognizeCallback() + result.success(isStarted) + } + "stopConversation" -> { try { AgentService.stopRecognition() @@ -104,45 +120,29 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("STOP_ERROR", "停止对话失败: ${e.message}", null) } } - "audioContinue" -> { - try { - AgentService.audioContinue() - result.success(true) - } catch (e: Exception) { - Log.e(TAG, "audioContinue失败", e) - result.error("STOP_ERROR", "audioContinue失败: ${e.message}", null) - } - } - "audioPause" -> { - try { - AgentService.audioPause() - result.success(true) - } catch (e: Exception) { - Log.e(TAG, "audioContinue失败", e) - result.error("STOP_ERROR", "audioContinue失败: ${e.message}", null) - } - } + "processTextInput" -> { val text = call.argument("text") if (text.isNullOrEmpty()) { result.error("INVALID_ARGUMENTS", "文本不能为空", null) return } - + val speakResponse = call.argument("speakResponse") ?: false - + try { val isProcessing = AgentService.processTextInput( text = text, speakResponse = speakResponse ) - + result.success(isProcessing) } catch (e: Exception) { Log.e(TAG, "处理文本输入失败", e) result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null) } } + "processImageInput" -> { val imagePath = call.argument("imagePath") if (imagePath.isNullOrEmpty()) { @@ -156,7 +156,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } val text = call.argument("text") ?: "" val speakResponse = call.argument("speakResponse") ?: false - + try { val isProcessing = AgentService.processImageInput( imagePath = imagePath, @@ -164,20 +164,21 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream text = text, speakResponse = speakResponse ) - + result.success(isProcessing) } catch (e: Exception) { Log.e(TAG, "处理图片输入失败", e) result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null) } } + "speakText" -> { val text = call.argument("text") if (text.isNullOrEmpty()) { result.error("INVALID_ARGUMENTS", "文本不能为空", null) return } - + try { AgentService.speakText(text) result.success(true) @@ -186,6 +187,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("TTS_ERROR", "语音合成失败: ${e.message}", null) } } + "stopTts" -> { try { AgentService.stopTts() @@ -195,6 +197,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) } } + "stopAiSteam" -> { try { AgentService.stopAiStream() @@ -215,6 +218,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null) } } + "interruptCurrentResponse" -> { try { AgentService.interruptCurrentResponse() @@ -224,6 +228,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null) } } + "dispose" -> { try { AgentService.dispose() @@ -233,6 +238,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null) } } + else -> { result.notImplemented() } @@ -246,7 +252,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream override fun onCancel(arguments: Any?) { this.eventSink = null } - + /** * 实现AgentServiceListener接口 * 接收来自AgentService的事件,并转发到Flutter @@ -257,7 +263,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream put("event", eventName) put("data", JSONObject(data)) } - + // 在主线程发送事件到Flutter MainScope().launch { eventSink?.success(eventData.toString()) 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 58d46a5f9..a62e67ffd 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 @@ -65,7 +65,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { Log.e(TAG, "语音代理未初始化") return false } - +Log.d(TAG, "手动启动语音识别: ") return AgentService.startRecognition(true) } @@ -73,7 +73,12 @@ object BleAgent : BleService.Callback, AgentServiceListener { * 停止语音识别 */ fun stopListening() { - AgentService.stopRecognition() + if (!isInitialized) { + Log.e(TAG, "语音代理未初始化") + return + } + AgentService.stopRecognition() + return } /** @@ -121,7 +126,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { } else if (state == BleConst.STATE_DISCONNECTED) { Log.d(TAG, "Ble设备已断开,停止语音服务") // 设备断开连接,停止语音服务 - // stopListening() + stopListening() } } @@ -136,18 +141,19 @@ object BleAgent : BleService.Callback, AgentServiceListener { * 在收到唤醒信号时启动语音识别 */ override fun onWakeupSignalReceived() { - Log.d(TAG, "收到唤醒信号,启动语音识别") + Log.d(TAG, "收到唤醒信号,启动语音识别${AgentService.isRecognitionActive}") // 停止当前TTS,避免冲突 AgentService.interruptCurrentResponse() // AgentService.speakText("我在!") // 启动语音识别 - if (AgentService.isRecognitionActive&&AgentService.azureAsrHelper!!.isPaused) { + // 如果已经在识别,则重新启动 - stopListening() - } + //stopListening() + - startListening() + //stopListening() + startListening() } override fun onDeviceInfoReceived(infoType: Int, infoData: Map) { diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 6ac0411df..664b3faad 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -248,16 +248,16 @@ class AgentService { } } - /// 停止对话 + /// 设备识别器 /// - /// 停止语音识别 - /// 返回是否成功停止对话 - static Future stopConversation() async { + /// 启动语音识别,开始监听用户语音输入 + /// 返回是否成功开始对话 + static Future recognizeCallback() async { try { - final bool result = await _channel.invokeMethod('stopConversation'); + final bool result = await _channel.invokeMethod('recognizeCallback'); return result; } on PlatformException catch (e) { - throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details); + throw AgentServiceException(e.code, e.message ?? '设置识别器失败', e.details); } } @@ -265,27 +265,40 @@ class AgentService { /// /// 停止语音识别 /// 返回是否成功停止对话 - static Future audioContinue() async { + static Future stopConversation() async { try { - final bool result = await _channel.invokeMethod('audioContinue'); + final bool result = await _channel.invokeMethod('stopConversation'); return result; } on PlatformException catch (e) { throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details); } } - /// 停止对话 - /// - /// 停止语音识别 - /// 返回是否成功停止对话 - static Future audioPause() async { - try { - final bool result = await _channel.invokeMethod('audioPause'); - return result; - } on PlatformException catch (e) { - throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details); - } - } + // /// 停止对话 + // /// + // /// 停止语音识别 + // /// 返回是否成功停止对话 + // static Future audioContinue() async { + // try { + // final bool result = await _channel.invokeMethod('audioContinue'); + // return result; + // } on PlatformException catch (e) { + // throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details); + // } + // } + + // /// 停止对话 + // /// + // /// 停止语音识别 + // /// 返回是否成功停止对话 + // static Future audioPause() async { + // try { + // final bool result = await _channel.invokeMethod('audioPause'); + // return result; + // } on PlatformException catch (e) { + // throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details); + // } + // } /// 处理文本输入 /// 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 661e51ec8..415bbae21 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 @@ -22,6 +22,7 @@ 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 +import java.util.concurrent.atomic.AtomicBoolean /** @@ -52,8 +53,7 @@ class AzureAsrHelper(private val context: Context) { var audioSourceType = AudioSourceType.MICROPHONE // 音频处理 - private var audioStream: AudioStream? = null - private var externalAudioStream: ExternalAudioPullStream? = null + var audioStream: AudioStream? = null // 音频模式管理 private var audioManager: AudioManager? = null @@ -61,7 +61,6 @@ class AzureAsrHelper(private val context: Context) { // 录音文件处理 var recordfile: RecordFile? = null - var isPaused = true /** * 音频来源类型 @@ -74,29 +73,6 @@ class AzureAsrHelper(private val context: Context) { EXTERNAL } - /** - * 获取设备支持的最佳音频格式 - * 优先选择16000Hz,若不支持则降级到8000Hz - */ - private fun getOptimalAudioFormat(): AudioStreamFormat { - // 支持的采样率列表(按优先级排序) - val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) - - // 查找设备支持的最佳采样率 - val sampleRate = supportedSampleRates.firstOrNull { rate -> - val bufferSize = AudioRecord.getMinBufferSize( - rate, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT - ) - bufferSize > 0 // 返回正值表示支持 - } ?: 16000 // 默认回退值 - - Log.i(tag, "使用采样率: ${sampleRate}Hz") - - // 创建对应的音频格式 - return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1) - } /** * 初始化Azure语音服务 @@ -122,7 +98,6 @@ class AzureAsrHelper(private val context: Context) { // 释放之前的资源 dispose() - // 初始化音频管理器 audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL @@ -169,49 +144,32 @@ class AzureAsrHelper(private val context: Context) { } // 录音文件类 recordfile = RecordFile; - //进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请 - // // 创建识别器 - // val setupSuccess = setupRecognizer() - // if (setupSuccess) { - // // 优化:初始化完成后进行预热 - // warmupRecognizer() - // } + + return true } catch (e: Exception) { Log.e(tag, "初始化失败: ${e.message}") return false } } - +private fun isRecognizerValid(): Boolean { + return recognizer != null +} /** * 设置识别器 */ private fun setupRecognizer(): Boolean { try { - - // 清理旧的识别器 - recognizer?.close() - recognizer = null - - // 设置音频配置 - when (audioSourceType) { - AudioSourceType.MICROPHONE -> { - // 总是使用拉流方式并启用回音消除 - Log.i(tag, "使用拉流方式并启用回音消除") - setupMicrophoneStream() - } - - AudioSourceType.EXTERNAL -> { - // 改用拉流方式处理外部音频 - Log.d(tag, "改用拉流方式处理外部音频") - - - setupExternalAudioStream() - - } + // 如果正在进行连续识别,先停止 + if (isContinuousRecognitionActive) { + // 直接停止,不等待结果 + recognizer?.stopContinuousRecognitionAsync() + isContinuousRecognitionActive = false } + setupMicrophoneStream() + - // 创建识别器 + //创建识别器 recognizer = if (isAutoDetectLanguage) { val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) @@ -233,23 +191,14 @@ class AzureAsrHelper(private val context: Context) { */ private fun setupMicrophoneStream() { try { - Log.d(tag, "设置麦克风流 - 使用拉流方式: }") - restoreOriginalAudioState() + Log.d(tag, "设置麦克风流 - 使用拉流方式: }") + if (audioStream == null) { // 创建外部音频拉流对象 audioStream = AudioStream() audioStream!!.initAudioRecord() } - // else - // { - // audioStream.stopCaptureThread() - // audioStream.startCaptureThread() - // } - // 创建麦克风流 - // microphoneStream = MicrophoneStream() - - // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 - // audioConfig = AudioConfig.fromStreamInput(microphoneStream) + audioConfig = AudioConfig.fromStreamInput(audioStream!!.pushAudioStream) @@ -259,60 +208,6 @@ class AzureAsrHelper(private val context: Context) { } } - /** - * 预热识别器(减少首次识别延迟) - */ - private fun warmupRecognizer() { - try { - // 创建极短的音频数据进行预热 - val warmupData = ByteArray(320) // 10ms 16kHz 单声道 - // 模拟静音数据 - warmupData.fill(0) - - // 如果是外部音频流,进行预热 - if (audioSourceType == AudioSourceType.EXTERNAL) { - externalAudioStream?.pushAudio(warmupData) - } - Log.d(tag, "ASR预热完成") - } catch (e: Exception) { - // 预热失败不影响正常使用 - Log.d(tag, "ASR预热失败: ${e.message}") - } - } - - - /** - * 设置外部音频流 - 使用拉流方式 - */ - private fun setupExternalAudioStream() { - try { - Log.d(tag, "设置外部音频流 - 使用拉流方式: }") - //disableBluetoothAudio() - // 创建外部音频拉流对象 - externalAudioStream = ExternalAudioPullStream() - - // 创建音频配置 - audioConfig = AudioConfig.fromStreamInput(externalAudioStream) - } catch (e: Exception) { - Log.e(tag, "设置外部音频流失败: ${e.message}") - externalAudioStream = null - } - } - - /** - * 向音频流写入音频数据 - * 仅当音频源设置为EXTERNAL时有效 - * - * @param data 音频数据字节数组 - */ - fun pushAudioData(data: ByteArray) { - if (audioSourceType != AudioSourceType.EXTERNAL) { - return - } - // 使用拉流模式,将数据推入队列 - externalAudioStream?.pushAudio(data) - - } /** * 执行一次性语音识别 @@ -327,21 +222,18 @@ class AzureAsrHelper(private val context: Context) { callback.onError("语音服务未初始化") return } + this.audioSourceType = audioSourceType // 确保不在连续识别中 if (isContinuousRecognitionActive) { stopContinuousRecognition() } - // 重置识别器 - if (!setupRecognizer()) { - callback.onError("重置识别器失败") - return - } + try { // 启动音频处理 - startAudioProcessing() + audioStream!!.startAudioRecord() // 执行同步识别 val result = recognizer?.recognizeOnceAsync()?.get() @@ -369,43 +261,43 @@ class AzureAsrHelper(private val context: Context) { * @return 是否成功开始识别 */ fun startContinuousRecognition( - callback: ContinuousRecognizeCallback, + audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE ): Boolean { - if (speechConfig == null) { - callback.onError("语音服务未初始化") - return false - } + Log.d(tag, "startContinuousRecognition:$isContinuousRecognitionActive ") if (isContinuousRecognitionActive) { return true } - this.audioSourceType = audioSourceType - // 重置识别器 + if (!isRecognizerValid()) { + Log.w(tag, "识别器已失效,正在重新创建...") if (!setupRecognizer()) { - callback.onError("重置识别器失败") + Log.e(tag, "重新创建识别器失败") return false } + } + this.audioSourceType = audioSourceType - try { - // 设置各种事件监听 - setupEventListeners(callback) + try { + + Log.d(tag, "startContinuousRecognition: ") // 启动音频处理 //startAudioProcessing() - // 开始连续识别 - recognizer?.startContinuousRecognitionAsync() - audioStream?.startCaptureThread() + + // 启动音频处理 + audioStream!!.startAudioRecord() + recognizer?.startContinuousRecognitionAsync() isContinuousRecognitionActive = true return true } catch (e: Exception) { - stopAudioProcessing() + audioStream!!.stopMicrophoneCapture() isContinuousRecognitionActive = false - callback.onError("启动连续识别失败: ${e.message}") + return false } } @@ -413,68 +305,88 @@ class AzureAsrHelper(private val context: Context) { /** * 设置事件监听器 */ - private fun setupEventListeners(callback: ContinuousRecognizeCallback) { - // 优化:识别中事件 - 添加文本长度检查 - recognizer?.recognizing?.addEventListener( - EventHandler { _, event -> - // 优化:只处理非空结果 - if (event.result.text.isNotEmpty()) { - val detectedLanguage = if (isAutoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" - } else { - currentLanguage + fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { + Log.d(tag, "设置ssssss监听器:${speechConfig} ") + if (speechConfig == null) { + callback.onError("语音服务未初始化") + return false + } + + // 重设识别器 + if (!setupRecognizer()) { + Log.d(tag, "初始化失败: ") + return false + } + try { + Log.d(tag, "设置ssssss监听器: ") + // 优化:识别中事件 - 添加文本长度检查 + recognizer?.recognizing?.addEventListener( + EventHandler { _, event -> + // 优化:只处理非空结果 + if (event.result.text.isNotEmpty()) { + val detectedLanguage = if (isAutoDetectLanguage) { + AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" + } else { + currentLanguage + } + callback.onRecognizing(event.result.text, detectedLanguage) } - callback.onRecognizing(event.result.text, detectedLanguage) } - } - ) - - // 优化:识别完成事件 - 优化语言检测 - recognizer?.recognized?.addEventListener( - EventHandler { _, event -> - if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) { - val detectedLanguage = if (isAutoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result)?.language - ?: supportedLanguages[0] - } else { - currentLanguage + ) + + // 优化:识别完成事件 - 优化语言检测 + recognizer?.recognized?.addEventListener( + EventHandler { _, event -> + if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) { + val detectedLanguage = if (isAutoDetectLanguage) { + AutoDetectSourceLanguageResult.fromResult(event.result)?.language + ?: supportedLanguages[0] + } else { + currentLanguage + } + callback.onResult(event.result.text, detectedLanguage) } - callback.onResult(event.result.text, detectedLanguage) } - } - ) - - // 会话开始事件 - recognizer?.sessionStarted?.addEventListener( - EventHandler { _, _ -> - // 直接在当前线程调用回调 - Log.d(tag, "会话开始事件") - callback.onSessionStarted() - } - ) - - // 会话结束事件 - recognizer?.sessionStopped?.addEventListener( - EventHandler { _, _ -> - // 直接在当前线程调用回调 - Log.d(tag, "会话结束事件") - if (audioSourceType == AudioSourceType.EXTERNAL) { + ) + + // 会话开始事件 + recognizer?.sessionStarted?.addEventListener( + EventHandler { _, _ -> + // 直接在当前线程调用回调 + Log.d(tag, "会话开始事件") + callback.onSessionStarted() + } + ) + + // 会话结束事件 + recognizer?.sessionStopped?.addEventListener( + EventHandler { _, _ -> + // 直接在当前线程调用回调 + Log.d(tag, "会话结束事件") + if (audioSourceType == AudioSourceType.EXTERNAL) { + + callback.onSessionStopped() + isContinuousRecognitionActive = false + // stopAudioProcessing() + } + } + ) + + // 取消事件 + recognizer?.canceled?.addEventListener( + EventHandler { _, event -> + val errorDetails = event.errorDetails ?: "未知错误" + val reason = event.reason.toString() + callback.onCanceled(reason, errorDetails) - callback.onSessionStopped() - isContinuousRecognitionActive = false - stopAudioProcessing() - } - } - ) - - // 取消事件 - recognizer?.canceled?.addEventListener( - EventHandler { _, event -> - val errorDetails = event.errorDetails ?: "未知错误" - val reason = event.reason.toString() - callback.onCanceled(reason, errorDetails) - } - ) + } + ) + + } catch (e: Exception) { + callback.onError("启动连续识别失败: ${e.message}") + return false + } + return true } @@ -494,36 +406,34 @@ class AzureAsrHelper(private val context: Context) { } try { - if (recognizer == null) { - Log.w(tag, "识别器为空,重置状态") - isContinuousRecognitionActive = false - return true - } + + isContinuousRecognitionActive = false + if (audioSourceType == AudioSourceType.EXTERNAL) { - pushAudioData(ByteArray(0)) + //pushAudioData(ByteArray(0)) } - + Log.d(tag, "停止连续语音识别: ") // 直接停止连续识别(SDK内部已是异步操作) - recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) +recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) // 停止音频处理 - stopAudioProcessing() + audioStream!!.stopMicrophoneCapture() // 会话结束事件会设置isContinuousRecognitionActive = false return true } catch (e: Exception) { // 强制重置状态 isContinuousRecognitionActive = false Log.e(tag, "停止连续识别失败: ${e.message}") - +recognizer?.stopContinuousRecognitionAsync() // 停止音频处理 - stopAudioProcessing() + audioStream!!.stopMicrophoneCapture() // 尝试强制关闭识别器 - try { - recognizer?.close() - recognizer = null - } catch (ex: Exception) { - Log.e(tag, "关闭识别器失败: ${ex.message}") - } + // try { + // recognizer?.close() + // recognizer = null + // } catch (ex: Exception) { + // Log.e(tag, "关闭识别器失败: ${ex.message}") + // } return false } @@ -545,7 +455,7 @@ class AzureAsrHelper(private val context: Context) { recognizer?.stopContinuousRecognitionAsync() isContinuousRecognitionActive = false } - + // recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) // 停止音频处理 stopAudioProcessing() // 停止录音 @@ -566,7 +476,7 @@ class AzureAsrHelper(private val context: Context) { // 确保状态被重置 isContinuousRecognitionActive = false //microphoneStream = null - externalAudioStream = null + audioConfig = null recognizer = null speechConfig = null @@ -575,7 +485,7 @@ class AzureAsrHelper(private val context: Context) { isContinuousRecognitionActive = false //microphoneStream = null - externalAudioStream = null + audioConfig = null recognizer = null speechConfig = null @@ -583,30 +493,13 @@ class AzureAsrHelper(private val context: Context) { } // 音频处理相关方法 - - /** - * 启动音频处理 - */ - private fun startAudioProcessing() { - when (audioSourceType) { - AudioSourceType.MICROPHONE -> { - // 拉流模式不需要额外启动,SDK会自动拉取数据 - // 无需执行任何操作 - } - - AudioSourceType.EXTERNAL -> { - // 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData - } - } - } - /** * 停止音频处理 */ private fun stopAudioProcessing() { audioStream?.let { try { - it.stopMicrophoneCapture() + Log.e(tag, "停止音频处理: }") it.releaseAudioResources() it.pushAudioStream?.close() @@ -617,49 +510,9 @@ class AzureAsrHelper(private val context: Context) { e.printStackTrace() } } // 关闭音频流(根据实际实现可能需要) - externalAudioStream?.let { - try { - it.close() - externalAudioStream = null - } catch (e: Exception) { - Log.e(tag, "关闭外部音频流失败: ${e.message}") - e.printStackTrace() - } - } - } - - - /* - 暂停音频输入 - */ - fun audioPause() { - Log.d(tag, "关闭麦克风stopAudioPause: ") - // 清理旧的识别器 - isPaused = true - // 如果正在进行连续识别,先停止 - if (isContinuousRecognitionActive) { - // 直接停止,不等待结果 - recognizer?.stopContinuousRecognitionAsync() - isContinuousRecognitionActive = false - } } - /* - 继续音频输入 - */ - fun audioContinue() { - Log.d(tag, "继续麦克风stopAudioPause: ") -// 如果正在进行连续识别,先停止 - if (!isContinuousRecognitionActive) { - // 直接停止,不等待结果 - recognizer?.startContinuousRecognitionAsync() - isContinuousRecognitionActive = true - } - // 开始连续识别 - isPaused = false - - } /** * 禁用蓝牙音频功能,切换回正常音频模式 @@ -753,218 +606,278 @@ class AzureAsrHelper(private val context: Context) { recordfile!!.closeFile(isSave) } + /** * 麦克风流 - 拉流模式 * 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据 */ inner class AudioStream { - - private var echoCanceler: AcousticEchoCanceler? = null - private var noiseSuppressor: NoiseSuppressor? = null - private var automaticGainControl: AutomaticGainControl? = null - - + private val bufferSize = 4096 // 可根据需要调整 var audioRecord: AudioRecord? = null - private var captureThread: Thread? = null var pushAudioStream: PushAudioInputStream? = null - + // 新增:用于异步写入的队列和线程 + private val writeQueue = LinkedBlockingQueue() + private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在 + private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据 + private var writeThread: Thread? = null + + // 音频配置 private val channelConfig = AudioFormat.CHANNEL_IN_MONO private val audioFormat = AudioFormat.ENCODING_PCM_16BIT /** - * 开始麦克风捕获 + * 获取设备支持的最佳音频格式 + * 优先选择16000Hz,若不支持则降级到8000Hz */ - fun initAudioRecord() { - - val format = getOptimalAudioFormat() - pushAudioStream = AudioInputStream.createPushStream(format) + private fun getOptimalAudioFormat(): AudioStreamFormat { + // 支持的采样率列表(按优先级排序) val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + // 查找设备支持的最佳采样率 val sampleRate = supportedSampleRates.firstOrNull { rate -> val bufferSize = AudioRecord.getMinBufferSize( rate, - channelConfig, - audioFormat + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT ) bufferSize > 0 // 返回正值表示支持 } ?: 16000 // 默认回退值 + Log.i(tag, "使用采样率: ${sampleRate}Hz") - // 计算最小缓冲区大小(避免出现音频欠载) - val minBufferSize = AudioRecord.getMinBufferSize( - sampleRate, - channelConfig, - audioFormat - ) -// 创建录音对象 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - Log.d("AzureASR", "android.os.Build.VERSION.SDK_INT") - val format = AudioFormat.Builder() - .setSampleRate(sampleRate) - .setEncoding(audioFormat) - .setChannelMask(channelConfig) - .build() - - audioRecord = AudioRecord.Builder() - .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) - .setAudioFormat(format) - .setBufferSizeInBytes(minBufferSize * 2) - .build() - } else { - // 创建 AudioRecord 对象 - audioRecord = AudioRecord( - MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除) - sampleRate, - channelConfig, - audioFormat, - minBufferSize * 2 // 使用两倍缓冲区防止溢出 - ) + // 创建对应的音频格式 + return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1) + } - } + /** + * 初始化 + */ + fun initAudioRecord() { + val format = getOptimalAudioFormat() + pushAudioStream = AudioInputStream.createPushStream(format) + isRunning.set(true) + startWriteThread() // 再启动数据读取线程 - // 检查初始化状态 - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { - throw IllegalStateException("AudioRecord初始化失败") - } - startCaptureThread() // 再启动数据读取线程 } - // 3. 单独封装线程启动逻辑 - fun startCaptureThread() { - try { - // 确保之前的线程已停止 - stopCaptureThread() - - // 开始录音 - audioRecord?.startRecording() - - captureThread = Thread({ - android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO) - - val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) - var bufferSize = 0 - val sampleRate = supportedSampleRates.firstOrNull { rate -> - bufferSize = AudioRecord.getMinBufferSize( - rate, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT - ) - bufferSize > 0 - } ?: 16000 - - val buffer = ByteArray(bufferSize) - - try { - while (!Thread.currentThread().isInterrupted && - audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { - - if (!isPaused) { - val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 - if (bytesRead > 0) { - // 安全地写入Azure流 - try { - pushAudioStream?.write(buffer) - recordfile?.saveAudioDataToWav(buffer) - } catch (e: Exception) { - Log.w(tag, "写入音频流失败: ${e.message}") - } - } else if (bytesRead < 0) { - Log.w(tag, "AudioRecord读取错误: $bytesRead") - break + private fun startWriteThread() { + writeThread = Thread { + try { + + while (isRunning.get()) { + // 等待录音信号 + if (!isWriting.get()) { + Thread.sleep(10) // 短暂休眠避免空转 + continue + } + var data: ByteArray? = null + var bytesToWrite = 0 + // 情况1:正在录制中 -> 直接从AudioRecord读取 + if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + data = ByteArray(bufferSize) + val bytesRead = audioRecord?.read(data, 0, bufferSize) ?: -1 + + when { + bytesRead < 0 -> { + Log.e("tag", "读取音频失败,错误码: $bytesRead") + continue } - } else { - Thread.sleep(10) // 暂停时短暂休眠 + + bytesRead == 0 -> continue // 无数据可读 + else -> bytesToWrite = bytesRead // 有效数据 } } - } catch (e: InterruptedException) { - Log.d(tag, "音频捕获线程被中断") - } catch (e: Exception) { - Log.e(tag, "音频捕获异常: ${e.message}", e) + // 情况2:不在录制但队列有数据 -> 从队列获取 + else if (writeQueue.isNotEmpty()) { + data = writeQueue.poll() + Log.d("tag", "写入数据: ${ data?.size}") + bytesToWrite = data?.size ?: 0 + } + + // 确保有有效数据再写入 + if (data != null && bytesToWrite > 0) { + // 处理实际读取长度 < bufferSize 的情况 + val finalData = + if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data + + try { + pushAudioStream?.write(finalData) + recordfile?.saveAudioDataToWav(finalData) + } catch (e: Exception) { + Log.e("tag", "写入失败: ${e.message}") + } + } else { + Thread.yield() // 避免空转消耗CPU + } } - }, "AudioCaptureThread") - - captureThread?.start() + } catch (e: Exception) { + Log.e("tag", "写入线程异常: ${e.stackTraceToString()}") + } finally { + Log.d("tag", "音频写入线程退出") + writeQueue.clear() + } + }.apply { + name = "AudioWriteThread" + start() + } + } + + + /** + * 外部音频输入 + */ + fun saveAudioDataTo(buffer: ByteArray) { + + if (audioSourceType == AudioSourceType.MICROPHONE) return + // 放入队列,由写线程写入 + writeQueue.offer(buffer.copyOf()) + + } + + fun startAudioRecord() { + + isWriting.set(true) + + when (audioSourceType) { + + AudioSourceType.MICROPHONE -> runMicrophoneCapture() + AudioSourceType.EXTERNAL -> runExternalCapture() + } + + } + + private fun runMicrophoneCapture() { + try { + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + val bufferSize = AudioRecord.getMinBufferSize( + rate, + channelConfig, + audioFormat + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 + + + // 计算最小缓冲区大小(避免出现音频欠载) + val minBufferSize = AudioRecord.getMinBufferSize( + sampleRate, + channelConfig, + audioFormat + ) +// 创建录音对象 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + Log.d("AzureASR", "android.os.Build.VERSION.SDK_INT") + val format = AudioFormat.Builder() + .setSampleRate(sampleRate) + .setEncoding(audioFormat) + .setChannelMask(channelConfig) + .build() + + audioRecord = AudioRecord.Builder() + .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) + .setAudioFormat(format) + .setBufferSizeInBytes(minBufferSize * 2) + .build() + } else { + // 创建 AudioRecord 对象 + audioRecord = AudioRecord( + MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除) + sampleRate, + channelConfig, + audioFormat, + minBufferSize * 2 // 使用两倍缓冲区防止溢出 + ) + + } + // 检查初始化状态 + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + throw IllegalStateException("AudioRecord初始化失败") + } + + + // 4. 开始录音 + audioRecord?.startRecording() + Log.d("TAG", "录音开始,采样率: $sampleRate Hz") + + } catch (e: Exception) { - Log.e(tag, "启动音频捕获线程失败: ${e.message}", e) - throw e + Log.e("TAG", "音频捕获异常: ${e.message}") } } - private fun stopCaptureThread() { - captureThread?.let { thread -> - if (thread.isAlive) { - thread.interrupt() - try { - thread.join(500) // 等待最多500ms - } catch (e: InterruptedException) { - Log.w(tag, "等待音频线程结束被中断") - } + private fun runExternalCapture() { + Log.d("TAG", "外部音频捕获启动") + try { // TODO: 实现外部音频源捕获逻辑 + // 停止录音 + if (audioRecord?.recordingState == AudioRecord.STATE_INITIALIZED) { + Log.d("TAG", "外部音频捕获启动 释放audioRecord") + audioRecord?.stop() + // 释放录音实例 + audioRecord?.release() + audioRecord = null } + } catch (e: Exception) { + Log.e("TAG", "外部音频捕获异常: ${e.message}") + } finally { + } - captureThread = null } /** * 停止麦克风捕获并释放所有相关资源 */ - fun stopMicrophoneCapture() { + fun stopMicrophoneCapture() { try { - if (captureThread != null) { - // 中断并等待捕获线程结束 - captureThread?.interrupt() - captureThread?.join(300) // 最多等待300ms - - // 停止录音并释放AudioRecord资源 - audioRecord?.stop() // 必须先停止再释放 - isPaused= true + if (!isWriting.get()) return + + + isWriting.set(false) + + // 停止录音 + if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + audioRecord?.stop() } + // 停止录音并释放AudioRecord资源 + // 释放录音实例 + audioRecord?.release() + audioRecord = null } catch (e: Exception) { Log.e("AudioConfig", "Error releasing resources: ${e.message}") } finally { + writeQueue.clear() // 确保恢复原始音频状态 // restoreOriginalAudioState() } } - + /** * 释放音频资源 */ - fun releaseAudioResources() { + fun releaseAudioResources() { try { - isPaused= true + if (!isWriting.get()) return + + + isWriting.set(false) + isRunning.set(false) // 停止录音 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 - // } + // 中断并等待捕获线程结束 + writeThread?.interrupt() + writeThread?.join(300) // 最多等待300ms // 恢复音频模式 this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode @@ -975,64 +888,13 @@ class AzureAsrHelper(private val context: Context) { } catch (e: Exception) { Log.e(tag, "释放音频资源失败: ${e.message}") e.printStackTrace() + } finally { + writeQueue.clear() + writeThread = null + // 确保恢复原始音频状态 + // restoreOriginalAudioState() } } - - } - - /** - * 外部音频拉流 - * 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式 - */ - private inner class ExternalAudioPullStream : PullAudioInputStreamCallback() { - private val queue: BlockingQueue = LinkedBlockingQueue() - private var closed = false - - /** - * 外部调用:推送音频数据到队列 - * @param data 音频数据 - */ - fun pushAudio(data: ByteArray) { - // 将音频数据保存成wav格式的音频文件 - recordfile?.saveAudioDataToWav(data) - if (!closed) { - queue.offer(data) - } - } - - /** - * 修复:外部音频流恢复阻塞读取 - * @param buffer SDK提供的缓冲区 - * @return 读取的字节数,0表示流结束 - */ - override fun read(buffer: ByteArray): Int { - try { - val chunk = queue.take() - - // 检查是否是结束标志(空数组) - if (chunk.isEmpty()) { - return 0 - } - - val toCopy = minOf(chunk.size, buffer.size) - System.arraycopy(chunk, 0, buffer, 0, toCopy) - return toCopy - } catch (e: InterruptedException) { - Thread.currentThread().interrupt() - return 0 - } catch (e: Exception) { - Log.e(tag, "读取外部音频数据失败: ${e.message}") - return 0 - } - } - - /** - * SDK调用:关闭流 - */ - override fun close() { - closed = true - queue.clear() - } } /** 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 d885c92cd..c20214038 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 @@ -255,24 +255,15 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } - "setPause" -> { - azureAsrHelper.audioPause(); - return - } - - "setcontinue" -> { - azureAsrHelper.audioContinue(); - return - } "disableBluetoothAudio" -> { azureAsrHelper.disableBluetoothAudio(); - return + return } "restoreOriginalAudioState" -> { azureAsrHelper.restoreOriginalAudioState(); - return + return } "startContinuousRecognition" -> { @@ -296,62 +287,70 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { AzureAsrHelper.AudioSourceType.MICROPHONE } - val success = azureAsrHelper.startContinuousRecognition(object : - AzureAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String, detectedLanguage: String) { - sendAsrEvent( - mapOf( - "type" to "result", - "text" to text, - "detectedLanguage" to detectedLanguage - ) + val success = azureAsrHelper.startContinuousRecognition(audioSourceType) + result.success(true) + } catch (e: Exception) { + result.error("START_RECOGNITION_ERROR", e.message, null) + } + } + + "recognizeCallback" -> { + + val success = azureAsrHelper.setupEventListeners(object : + AzureAsrHelper.ContinuousRecognizeCallback { + override fun onResult(text: String, detectedLanguage: String) { + sendAsrEvent( + mapOf( + "type" to "result", + "text" to text, + "detectedLanguage" to detectedLanguage ) - } + ) + } - override fun onRecognizing( - recognizing: String, - detectedLanguage: String - ) { - sendAsrEvent( - mapOf( - "type" to "recognizing", - "text" to recognizing, - "detectedLanguage" to detectedLanguage - ) + override fun onRecognizing( + recognizing: String, + detectedLanguage: String + ) { + sendAsrEvent( + mapOf( + "type" to "recognizing", + "text" to recognizing, + "detectedLanguage" to detectedLanguage ) - } + ) + } - override fun onSessionStarted() { - sendAsrEvent(mapOf("type" to "sessionStarted")) - } + override fun onSessionStarted() { + sendAsrEvent(mapOf("type" to "sessionStarted")) + } - override fun onSessionStopped() { - sendAsrEvent(mapOf("type" to "sessionStopped")) - } + override fun onSessionStopped() { + sendAsrEvent(mapOf("type" to "sessionStopped")) + } - override fun onCanceled(reason: String, errorDetails: String) { - sendAsrEvent( - mapOf( - "type" to "canceled", - "reason" to reason, - "errorDetails" to errorDetails - ) + override fun onCanceled(reason: String, errorDetails: String) { + + sendAsrEvent( + mapOf( + "type" to "canceled", + "reason" to reason, + "errorDetails" to errorDetails ) - } + ) + } - override fun onError(error: String) { - sendAsrEvent( - mapOf( - "type" to "error", - "message" to error - ) + override fun onError(error: String) { + sendAsrEvent( + mapOf( + "type" to "error", + "message" to error ) - } - }, audioSourceType) - result.success(success) - } catch (e: Exception) { - result.error("START_RECOGNITION_ERROR", e.message, null) - } + ) + } + }) + FileLogger.d(tag, "recognizeCallback: ${success}") // + result.success(success) } "stopContinuousRecognition" -> { @@ -378,7 +377,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { "音频数据不能为空", null ) - azureAsrHelper.pushAudioData(data) + + azureAsrHelper.audioStream?.saveAudioDataTo(data) result.success(true) } @@ -525,16 +525,16 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { "setAudioOutputDevice" -> { val type = call.argument("type") - var success =false + var success = false if (type == 0) { // 默认(如果有耳机选耳机,否则使用系统扬声器) - success=azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) + success = azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) } else if (type == 1) { // 强制使用声器 - success=azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) + success = azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) } else if (type == 2) { // 强制使用耳机 - success=azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) + success = azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) } @@ -583,7 +583,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { override fun onAudioDataReceived(data: ByteArray) { if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { - azureAsrHelper.pushAudioData(data) + azureAsrHelper.audioStream?.saveAudioDataTo(data) } // 可选:处理音频数据 } 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 7a32f21da..c1e1b4602 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 @@ -64,7 +64,7 @@ object RecordFile { writeThread = Thread { try { while (isWriting.get() || writeQueue.isNotEmpty()) { - val data = writeQueue.poll() ?: continue + val data = writeQueue.poll() ?: continue fos?.write(data) } } catch (e: Exception) { @@ -248,6 +248,7 @@ object RecordFile { } finally { fos = null currentAudioFile = null + writeThread = null } return false } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 22a674269..3a39d7237 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 @@ -1,5 +1,7 @@ package com.yunqiinnovation.ble_service +import android.Manifest +import android.annotation.SuppressLint import android.bluetooth.* import android.bluetooth.le.* import android.content.Context @@ -22,6 +24,7 @@ import java.util.concurrent.LinkedBlockingQueue import android.os.Handler import java.util.concurrent.atomic.AtomicBoolean import android.util.Log +import androidx.annotation.RequiresPermission /** * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 @@ -61,7 +64,7 @@ object BleService { // 上下文 private lateinit var context: Context - +private val commandQueue: Queue = LinkedList() // 蓝牙相关组件 private lateinit var bluetoothManager: BluetoothManager private var bluetoothAdapter: BluetoothAdapter? = null @@ -83,6 +86,10 @@ object BleService { // 是否是ota模式 var isEnterOta = false + // 是否回复指令 + var isReply = true + // 是否回复指令码 + var cmdReplyType =0xff // 连接状态 val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED) @@ -93,9 +100,6 @@ object BleService { private var opusManager: OpusManager? = null private val mainHandler = Handler(Looper.getMainLooper()) - // ota解码器实例 - // var otaManager: OTAManager? = null - // 初始化状态 private var isInitialized = false @@ -663,6 +667,7 @@ object BleService { private fun processAudioData(data: ByteArray) { try { if (opusManager?.isDecodeStream == true) { + recordfile?.saveAudioDataToWav(data) opusManager?.writeAudioStream(data) } else { @@ -677,6 +682,8 @@ object BleService { /** * 处理设备响应数据 - 帧头为0xBB */ + @SuppressLint("StaticFieldLeak") + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun processDeviceResponse(data: ByteArray) { Log.d( TAG, @@ -741,7 +748,21 @@ object BleService { } }" ) - +isReply=true; +cmdReplyType=cmdType; +if(!commandQueue.isEmpty()) +{ + val nextCmd = commandQueue.poll() + writeChar!!.value=nextCmd + var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) + if(isSeccess==true) + { + Log.d( + TAG, + "收到设备响应:" + ) + } +} // 根据命令类型处理不同的响应 when (cmdType) { BleConst.CMD_GET_VERSION -> { @@ -1005,7 +1026,7 @@ object BleService { BleConst.CMD_WAKE_UP -> { Log.i(TAG, "收到唤醒指令") // 接收到唤醒信号,打开编码器 (设备侧) - openEncoder() + notifyWakeupSignalReceived() } @@ -1316,6 +1337,7 @@ object BleService { * @param cmdData 命令数据 * @return 是否成功发送 */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { if (!checkConn()) { Log.e(TAG, "发送命令失败: 设备未连接") @@ -1358,7 +1380,26 @@ object BleService { // 发送数据 writeChar!!.value = cmdPacket - return bluetoothGatt?.writeCharacteristic(writeChar) ?: false + // 加入队列并触发发送 + + if(isReply) + { + var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) + if(isSeccess == true) + { + isReply=false; + cmdType; + return true + } + else + return false + } + else + { + commandQueue.offer(cmdPacket) + } + return true + } catch (e: Exception) { Log.e(TAG, "发送命令异常: ${e.message}", e) return false From 74fc42fb649567c1b685bb6bc86ce6a0a6eab154 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 9 Jul 2025 16:08:12 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=A3=E7=BB=91?= =?UTF-8?q?=E9=80=BB=E8=BE=91bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/devices/controllers/devices_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart index 416918206..091c62b47 100644 --- a/lib/modules/devices/controllers/devices_controller.dart +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -48,7 +48,7 @@ class DevicesController extends GetxController { void unbindearphone() async { final response = await Api.unbindearphone({ - "code": earphoneCode.value, + "code": earphoneLicense.value, "earphonname": earphoneMac.value, }); if (response != null) {