diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 127288e55..ebb128fb5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -50,6 +50,8 @@ + + diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index c7c098a95..97a943a90 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -5,6 +5,8 @@ import '../../../data/services/user_portrait.dart'; import '../../../data/services/location_manager.dart'; import '../../../data/services/music_manager.dart'; import '../../../data/services/navigation_manager.dart'; +import '../../../data/services/ast_service.dart'; +import '../../../data/services/speech_impl/azure_ast_service.dart'; import 'package:get/get.dart'; import '../../data/services/meeting/meeting_task_service.dart'; import '../../data/services/meeting/meeting_upload_service.dart'; @@ -25,7 +27,6 @@ class InitialBinding extends Bindings { void dependencies() { Logger.warning('InitialBinding dependencies'); - // 语言管理器(需要最先初始化) // 语言管理器(需要最先初始化) Get.lazyPut(() => LanguageManager(), fenix: true); @@ -33,6 +34,9 @@ class InitialBinding extends Bindings { Get.lazyPut(() => SpeechFactory(), fenix: true); Get.find().initialize(initialType: SpeechServiceType.azure); + // 注册 AST 服务 + Get.lazyPut(() => AzureAstService(), fenix: true); + // 火山翻译服务 Get.lazyPut(() => VolcanoTranslationService(), fenix: true); diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 7dca392ef..b4c776ab6 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -487,15 +487,19 @@ const Map enUS = { // 权限管理页面 'getPermissionStatusFailed': 'Failed to get permission status', 'communicationPermissions': 'Communication Permissions', + 'alarmPermissions': 'Alarm Permissions', 'calendarPermissions': 'Calendar Permissions', 'locationPermissions': 'Location Permissions', 'phone': 'Phone', 'sms': 'SMS', + 'alarm': 'Alarm', 'calendarWrite': 'Calendar Write', 'calendarFullAccess': 'Calendar Full Access', 'locationInfo': 'Location Info', 'phonePermissionDesc': 'Used for phone call functionality', 'smsPermissionDesc': 'Used for SMS functionality', + 'alarmPermissionDesc': + 'Used to set exact alarms to ensure reminders trigger on time', 'calendarWritePermissionDesc': 'Used for creating calendar events and reminders', 'calendarFullAccessPermissionDesc': @@ -512,6 +516,8 @@ const Map enUS = { 'phonePermissionDetailDesc': 'Phone call functionality requires phone permission', 'smsPermissionDetailDesc': 'SMS functionality requires SMS permission', + 'alarmPermissionDetailDesc': + 'This permission is required to set exact alarms and ensure reminders trigger on time', 'calendarWritePermissionDetailDesc': 'Creating calendar events requires calendar write permission', 'calendarFullAccessPermissionDetailDesc': diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index d73546416..e0757379e 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -455,15 +455,18 @@ const Map zhCN = { // 权限管理页面 'getPermissionStatusFailed': '获取权限状态失败', 'communicationPermissions': '通讯权限', + 'alarmPermissions': '闹钟权限', 'calendarPermissions': '日历权限', 'locationPermissions': '位置权限', 'phone': '电话', 'sms': '短信', + 'alarm': '闹钟', 'calendarWrite': '日历写入', 'calendarFullAccess': '日历完整访问', 'locationInfo': '位置信息', 'phonePermissionDesc': '用于拨打电话功能', 'smsPermissionDesc': '用于发送短信功能', + 'alarmPermissionDesc': '用于设置精确闹钟以确保提醒按时触发', 'calendarWritePermissionDesc': '用于创建日历事件和提醒', 'calendarFullAccessPermissionDesc': '用于读取和管理日历事件及提醒', 'locationPermissionDesc': '用于获取当前位置信息', @@ -477,6 +480,7 @@ const Map zhCN = { 'permissionRequestError': '请求权限时出错', 'phonePermissionDetailDesc': '拨打电话功能需要电话权限', 'smsPermissionDetailDesc': '发送短信功能需要短信权限', + 'alarmPermissionDetailDesc': '设置精确闹钟需要此权限,以确保提醒按时触发', 'calendarWritePermissionDetailDesc': '创建日历事件需要日历写入权限', 'calendarFullAccessPermissionDetailDesc': '管理日历事件需要完整日历权限', 'locationPermissionDetailDesc': '获取位置信息需要位置权限', diff --git a/lib/core/widgets/intro_overlay_builder.dart b/lib/core/widgets/intro_overlay_builder.dart index bab8412b0..920737ae2 100644 --- a/lib/core/widgets/intro_overlay_builder.dart +++ b/lib/core/widgets/intro_overlay_builder.dart @@ -89,22 +89,9 @@ class IntroOverlayBuilder { } static Widget _button(String text, Function() onTap) { - return GestureDetector( + return _AnimatedBorderButton( + text: text, onTap: onTap, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20.r), - border: Border.all(color: Colors.white), - ), - child: Text( - text, - style: TextStyle( - fontSize: 14.sp, - color: Colors.white, - ), - ), - ), ); } } @@ -166,3 +153,142 @@ class _AnimatedLottieWidgetState extends State<_AnimatedLottieWidget> ); } } + +// 新增的带边框流动动画的按钮组件 +class _AnimatedBorderButton extends StatefulWidget { + final String text; + final Function() onTap; + + const _AnimatedBorderButton({ + required this.text, + required this.onTap, + }); + + @override + _AnimatedBorderButtonState createState() => _AnimatedBorderButtonState(); +} + +class _AnimatedBorderButtonState extends State<_AnimatedBorderButton> + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _animation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); + _animation = Tween( + begin: 0.0, + end: 1.0, + ).animate(_animationController); + + _animationController.repeat(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap, + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return CustomPaint( + painter: _FlowingBorderPainter( + progress: _animation.value, + borderRadius: 20.r, + ), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.r), + border: Border.all(color: Colors.transparent), + ), + child: Text( + widget.text, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + ), + ), + ), + ); + }, + ), + ); + } +} + +// 自定义绘制器,用于绘制流动的边框 +class _FlowingBorderPainter extends CustomPainter { + final double progress; + final double borderRadius; + + _FlowingBorderPainter({ + required this.progress, + required this.borderRadius, + }); + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final rrect = RRect.fromRectAndRadius(rect, Radius.circular(borderRadius)); + + // 计算路径总长度 + final path = Path()..addRRect(rrect); + final pathMetrics = path.computeMetrics().first; + final totalLength = pathMetrics.length; + + // 流动效果:创建一个渐变的虚线效果 + final dashLength = totalLength * 0.3; // 虚线长度 + final startOffset = (progress * totalLength) % totalLength; + + // 绘制流动的边框段 + for (int i = 0; i < 3; i++) { + final offset = (startOffset + i * totalLength / 3) % totalLength; + final endOffset = (offset + dashLength) % totalLength; + + Path extractedPath; + if (endOffset > offset) { + extractedPath = pathMetrics.extractPath(offset, endOffset); + } else { + // 处理跨越路径起点的情况 + final path1 = pathMetrics.extractPath(offset, totalLength); + final path2 = pathMetrics.extractPath(0, endOffset); + extractedPath = Path() + ..addPath(path1, Offset.zero) + ..addPath(path2, Offset.zero); + } + + // 创建渐变效果 + const gradient = LinearGradient( + colors: [ + Colors.green, + Colors.yellow, + Colors.red, + ], + stops: [0.0, 0.5, 1.0], + ); + + final gradientPaint = Paint() + ..shader = gradient.createShader(rect) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + canvas.drawPath(extractedPath, gradientPaint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index e6c67563c..4a8b34c2d 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -41,11 +41,26 @@ abstract class AsrService { bool isContinuousRecognitionActive(); /// 开始录音 - Future enableRecord(String filePath); + /// + /// [filePath] 录音文件路径 + /// [acceptAudioData] 是否接受音频数据回调,默认为 false + Future enableRecord(String filePath, {bool acceptAudioData = false}); + + /// 获取音频数据流(如果支持) + Stream? getAudioDataStream() => null; /// 暂停录音 Future pauseRecord(); + /// 继续录音 + Future resumeRecord(); + + /// 设置音频配置 + Future setAudioConfig({ + int sampleRate = 16000, + int channels = 1, + }); + // /// 移动文件到新路径 Future moveFile(String sourcePath, String destPath); @@ -64,12 +79,12 @@ enum RecognitionEventType { /// 最终识别结果 finalResult, + /// 麦识别结果 + finalResult1, + /// 中间识别结果(实时反馈) intermediateResult, - /// 音频 - onAudio, - /// 会话开始 sessionStarted, diff --git a/lib/data/services/ast_service.dart b/lib/data/services/ast_service.dart new file mode 100644 index 000000000..244a7fb37 --- /dev/null +++ b/lib/data/services/ast_service.dart @@ -0,0 +1,113 @@ +import 'dart:async'; +import 'dart:typed_data'; + +/// 语音识别服务接口 +abstract class AstService { + /// 支持的语言 + List get supportedLanguages; + + /// 初始化语音识别服务 + Future initialize({required List supportedLanguages}); + + /// 开始录音 + Future enableRecord(String filePath); + + /// + Future startContinuousTranslation(); + + /// + Future stopContinuousTranslation(); + + /// 返回一个包含识别事件的流 + Future> recognizeCallback(); + + /// 开始录音 + Future path(String filePath); + + /// 释放资源 + Future dispose(); +} + +// 识别事件类型 +enum RecognitionEventType1 { + /// 最终识别结果 + finalResult, + + /// 中间识别结果(实时反馈) + intermediateResult, + + /// 会话开始 + sessionStarted, + + /// 会话结束 + sessionStopped, + + /// 识别取消 + canceled, + + /// 识别错误 + error, +} + +// 识别事件 +class RecognitionEvent1 { + /// 事件类型 + final RecognitionEventType1 type; + + /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) + final String text; + + /// 检测到的语言 + final String detectedLanguage; + + /// 角色 + final String role; + + /// 原始音频 + final Uint8List? audio; + + /// 错误信息(仅在 error 和 canceled 类型中有效) + final String error; + + RecognitionEvent1({ + required this.type, + this.text = '', + this.detectedLanguage = '', + this.role = '', + this.audio, + this.error = '', + }); + + /// 创建最终结果事件的快捷构造函数 + factory RecognitionEvent1.finalResult({ + required String text, + String detectedLanguage = '', + }) { + return RecognitionEvent1( + type: RecognitionEventType1.finalResult, + text: text, + detectedLanguage: detectedLanguage, + ); + } + + /// 创建错误事件的快捷构造函数 + factory RecognitionEvent1.error(String errorMessage) { + return RecognitionEvent1( + type: RecognitionEventType1.error, + error: errorMessage, + ); + } + + /// 检查是否为最终结果 + bool get isFinalResult => type == RecognitionEventType1.finalResult; + + /// 检查是否为错误 + bool get isError => + type == RecognitionEventType1.error || + type == RecognitionEventType1.canceled; + + @override + String toString() { + return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; + } +} diff --git a/lib/data/services/audio_service.dart b/lib/data/services/audio_service.dart new file mode 100644 index 000000000..6dbdd781f --- /dev/null +++ b/lib/data/services/audio_service.dart @@ -0,0 +1,29 @@ +import 'dart:async'; +import 'dart:typed_data'; + +/// 语音识别服务接口 +abstract class AudioService { + /// 开始录音 + Future enableRecord(String filePath); + + /// 暂停录音 + Future pauseRecord(); + + /// 继续录音 + Future resumeRecord(); + + /// 设置音频配置 + Future setAudioConfig({ + int sampleRate = 16000, + int channels = 1, + }); + + // /// 移动文件到新路径 + Future moveFile(String sourcePath, String destPath); + + // /// 重命名指定路径的音频文件 + Future renameFile(String filePath, String newName); + + /// 结束录音 + Future stopRecord(bool isSave); +} diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 309dd2ae0..5137faee5 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -21,6 +21,10 @@ class AzureAsrService extends GetxService implements AsrService { static const MethodChannel _channel = MethodChannel('azure_speech/asr'); static const EventChannel _eventChannel = EventChannel('azure_speech/asr_events'); + // 音频数据事件通道 + static const EventChannel _audioDataEventChannel = + EventChannel('azure_speech/audio_data_events'); + // final GetStorage _storage = GetStorage(); bool _isInitialized = false; late final String _subscriptionKey; @@ -43,6 +47,11 @@ class AzureAsrService extends GetxService implements AsrService { String _latestDetectedLanguage = ''; String get latestDetectedLanguage => _latestDetectedLanguage; +// 音频数据事件订阅 + StreamSubscription? _audioDataEventSubscription; + // 音频数据流控制器 + StreamController? _audioDataStreamController; + // 当前音频源类型 AudioSourceType _audioSourceType = AudioSourceType.microphone; @@ -72,6 +81,19 @@ class AzureAsrService extends GetxService implements AsrService { }, onError: _handleRecognitionError); } + /// 设置音频数据事件通道 + void _setupAudioDataEventChannel() { + _audioDataEventSubscription?.cancel(); + _audioDataEventSubscription = + _audioDataEventChannel.receiveBroadcastStream().listen((event) { + if (event is Map) { + _handleAudioDataEvent(event); + } + }, onError: (error) { + Logger.error('音频数据事件流错误: ${error.toString()}'); + }); + } + @override Future initialize({ List? supportedLanguages, @@ -220,6 +242,36 @@ class AzureAsrService extends GetxService implements AsrService { return _isContinuousRecognitionActive; } + /// 处理音频数据事件 + void _handleAudioDataEvent(dynamic event) { + if (event is! Map) return; + + final Map eventMap = event; + final String eventType = eventMap['type'] as String? ?? ''; + + switch (eventType) { + case 'audioData': + final Uint8List data = eventMap['data'] as Uint8List? ?? Uint8List(0); + final double timestamp = + (eventMap['timestamp'] as num?)?.toDouble() ?? 0.0; + final int size = eventMap['size'] as int? ?? 0; + print('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}'); + Logger.debug('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}'); + + // 发送到音频数据流 + _audioDataStreamController?.add(data); + + break; + } + } + + /// 获取音频数据流 + Stream? getAudioDataStream() { + print("getAudioDataStream"); + _audioDataStreamController ??= StreamController.broadcast(); + return _audioDataStreamController?.stream; + } + /// 处理来自原生端的识别事件 void _handleRecognitionEvent(dynamic event) { if (event is! Map || _eventStreamController == null) return; @@ -242,22 +294,27 @@ class AzureAsrService extends GetxService implements AsrService { detectedLanguage: detectedLanguage, )); break; - - case 'recognizing': + case 'result1': final String text = eventMap['text'] as String? ?? ''; final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; + _latestRecognizedText = text; + _latestDetectedLanguage = detectedLanguage; _eventStreamController?.add(RecognitionEvent( - type: RecognitionEventType.intermediateResult, + type: RecognitionEventType.finalResult1, text: text, detectedLanguage: detectedLanguage, )); break; - case 'onAudio': - final Uint8List data = eventMap['data'] as Uint8List? ?? Uint8List(0); + + case 'recognizing': + final String text = eventMap['text'] as String? ?? ''; + final String detectedLanguage = + eventMap['detectedLanguage'] as String? ?? ''; _eventStreamController?.add(RecognitionEvent( - type: RecognitionEventType.onAudio, - audio: data, + type: RecognitionEventType.intermediateResult, + text: text, + detectedLanguage: detectedLanguage, )); break; case 'sessionStarted': @@ -324,6 +381,14 @@ class AzureAsrService extends GetxService implements AsrService { await _eventSubscription?.cancel(); _eventSubscription = null; + // 取消音频数据事件订阅 + await _audioDataEventSubscription?.cancel(); + _audioDataEventSubscription = null; + + // 关闭音频数据流 + await _audioDataStreamController?.close(); + _audioDataStreamController = null; + // 通知原生端释放资源 await _channel.invokeMethod('dispose'); _isInitialized = false; @@ -364,11 +429,16 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String filePath) async { + Future enableRecord(String filePath, + {bool acceptAudioData = false}) async { try { final bool result = await _channel.invokeMethod('enableRecord', { 'filePath': filePath, + 'acceptAudioData': acceptAudioData, // 新增参数 }); + if (acceptAudioData) { + _setupAudioDataEventChannel(); + } return result; } catch (e) { @@ -398,7 +468,30 @@ class AzureAsrService extends GetxService implements AsrService { return result; } catch (e) { - Logger.error('停止录音: ${e.toString()}'); + Logger.error('暂停录音: ${e.toString()}'); + rethrow; + } + } + + /// 设置音频配置 + /// + /// [sampleRate] 采样率,默认16000 + /// [channels] 声道数,默认1(单声道) + Future setAudioConfig({ + int sampleRate = 16000, + int channels = 1, + }) async { + try { + final bool result = await _channel.invokeMethod('setAudioConfig', { + 'sampleRate': sampleRate, + 'channels': channels, + }); + + Logger.info( + '音频配置设置${result ? '成功' : '失败'}: 采样率=$sampleRate, 声道数=$channels'); + return result; + } catch (e) { + Logger.error('设置音频配置失败: ${e.toString()}'); rethrow; } } @@ -459,4 +552,16 @@ class AzureAsrService extends GetxService implements AsrService { rethrow; } } + + @override + Future resumeRecord() async { + try { + final bool result = await _channel.invokeMethod('resumeRecord'); + + return result; + } catch (e) { + Logger.error('继续录音: ${e.toString()}'); + rethrow; + } + } } diff --git a/lib/data/services/speech_impl/azure_ast_service.dart b/lib/data/services/speech_impl/azure_ast_service.dart new file mode 100644 index 000000000..5b5dc35da --- /dev/null +++ b/lib/data/services/speech_impl/azure_ast_service.dart @@ -0,0 +1,305 @@ +import 'dart:async'; +import '../../../data/models/appconfig.dart'; +import 'package:flutter/services.dart'; +import '../../../core/utils/logger.dart'; +import 'package:get/get.dart'; +import '../ast_service.dart'; + +/// 音频源类型 +enum AudioSourceType { + microphone, // 使用设备麦克风 + external // 使用外部提供的音频数据 +} + +/// Azure 语音识别服务 +/// +/// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口 +class AzureAstService extends GetxService implements AstService { + static final AzureAstService to = Get.put(AzureAstService()); + static const MethodChannel _channel = MethodChannel('azure_speech/ast'); + static const EventChannel _eventChannel = + EventChannel('azure_speech/ast_events'); + // final GetStorage _storage = GetStorage(); + bool _isInitialized = false; + late final String _subscriptionKey; + late final String _serviceRegion; + late String _baseUrl; + final String _endpoint = '/'; // 修改为根路径 + late final String _accessKey; + late final String _secretKey; + late final String _region; + late final String _service; + + final List _defaultSupportedLanguages = ['zh-CN', 'en-US']; + @override + List get supportedLanguages => _defaultSupportedLanguages; + + //连续识别相关 + bool _isContinuousRecognitionActive = false; + StreamController? _eventStreamController; + StreamSubscription? _eventSubscription; + + // 最新的识别结果 + String _latestRecognizedText = ''; + String get latestRecognizedText => _latestRecognizedText; + + // 最新检测到的语言 + String _latestDetectedLanguage = ''; + String get latestDetectedLanguage => _latestDetectedLanguage; + + // 当前音频源类型 + AudioSourceType _audioSourceType = AudioSourceType.microphone; + + AzureAstService() { + _loadConfig(); + } + + /// 设置事件通道 + void _setupEventChannel() { + _eventSubscription?.cancel(); + _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) { + if (event is Map) { + _handleRecognitionEvent(event); + } + }, onError: _handleRecognitionError); + } + + /// 处理来自原生端的识别事件 + void _handleRecognitionEvent(dynamic event) { + if (event is! Map || _eventStreamController == null) return; + + final Map eventMap = event; + final String eventType = eventMap['type'] as String? ?? ''; + + // 添加日志帮助调试 + + switch (eventType) { + case 'result': + final String text = eventMap['text'] as String? ?? ''; + final String detectedLanguage = + eventMap['detectedLanguage'] as String? ?? ''; + _latestRecognizedText = text; + _latestDetectedLanguage = detectedLanguage; + _eventStreamController?.add(RecognitionEvent1( + type: RecognitionEventType1.finalResult, + text: text, + detectedLanguage: detectedLanguage, + )); + break; + + case 'recognizing': + final String text = eventMap['text'] as String? ?? ''; + final String detectedLanguage = + eventMap['detectedLanguage'] as String? ?? ''; + _eventStreamController?.add(RecognitionEvent1( + type: RecognitionEventType1.intermediateResult, + text: text, + detectedLanguage: detectedLanguage, + )); + break; + case 'sessionStarted': + _eventStreamController?.add(RecognitionEvent1( + type: RecognitionEventType1.sessionStarted, + )); + break; + + case 'sessionStopped': + _isContinuousRecognitionActive = false; + _eventStreamController?.add(RecognitionEvent1( + type: RecognitionEventType1.sessionStopped, + )); + break; + + case 'canceled': + _isContinuousRecognitionActive = false; + final String reason = eventMap['reason'] as String? ?? ''; + final String errorDetails = eventMap['errorDetails'] as String? ?? ''; + + if (reason.isNotEmpty || errorDetails.isNotEmpty) { + Logger.error('识别取消: $reason - ${errorDetails.toString()}'); + } + + _eventStreamController?.add(RecognitionEvent1( + type: RecognitionEventType1.canceled, + error: '$reason: $errorDetails', + )); + break; + + case 'error': + final String error = eventMap['message'] as String? ?? ''; + Logger.error('识别错误: ${error.toString()}'); + _eventStreamController?.add(RecognitionEvent1( + type: RecognitionEventType1.error, + error: error, + )); + break; + } + } + + /// 处理识别事件流错误 + void _handleRecognitionError(Object error) { + Logger.error('识别事件流错误: ${error.toString()}'); + _eventStreamController?.addError(error); + _cleanupEventStream(); + } + + /// 清理事件流资源 + void _cleanupEventStream() { + _eventStreamController?.close(); + _eventStreamController = null; + _isContinuousRecognitionActive = false; + } + + /// 从环境变量加载配置 + void _loadConfig() { + // final _env = _storage.read("ENV") as Map; + _subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; + _serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? ''; + _accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? ''; + _secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? ''; + _region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? 'cn-north-1'; + _service = 'translate'; + _baseUrl = 'https://translate.volcengineapi.com'; + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { + throw Exception( + '未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); + } + } + + @override + Future> 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; + } + } + + @override + Future enableRecord(String filePath) async { + try { + final bool result = await _channel.invokeMethod('enableRecord', { + 'filePath': filePath, + }); + + return result; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future path(String filePath) async { + try { + final bool result = await _channel.invokeMethod('path', { + 'filePath': filePath, + }); + + return result; + } catch (e) { + Logger.error('开始录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future startContinuousTranslation() async { + try { + final bool result = + await _channel.invokeMethod('startContinuousTranslation'); + + return result; + } catch (e) { + Logger.error('停止录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future stopContinuousTranslation() async { + try { + final bool result = + await _channel.invokeMethod('stopContinuousTranslation'); + + return result; + } catch (e) { + Logger.error('停止录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future dispose() async { + try { + final bool result = await _channel.invokeMethod('dispose'); + + return; + } catch (e) { + Logger.error('停止录音: ${e.toString()}'); + rethrow; + } + } + + @override + Future initialize({ + List? supportedLanguages, + bool useExternalAudio = false, + bool useEchoCancellation = false, + }) async { + try { + final List languages = + supportedLanguages ?? _defaultSupportedLanguages; +//底层会初始化前释放 + // // 检查是否需要重新初始化 + // if (_isInitialized) { + // await dispose(); + // } + + // 设置音频源类型 + _audioSourceType = useExternalAudio + ? AudioSourceType.external + : AudioSourceType.microphone; +// Future initialize({ +// required String subscriptionKey, +// required String region, +// required List supportedLanguages, +// required String audioSourceType, +// required String translationAccessKey, +// required String translationSecretKey, +// String translationRegion = 'cn-north-1', +// }); + + final bool result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'region': _serviceRegion, + 'supportedLanguages': languages, + 'audioSourceType': _audioSourceType.toString().split('.').last, + 'translationAccessKey': _accessKey, + 'translationSecretKey': _secretKey, + 'translationRegion': _region, + }); + + _isInitialized = result; + _setupEventChannel(); + Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); + return result; + } catch (e) { + Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); + _isInitialized = false; + rethrow; + } + } +} diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index dabf9e6e0..87e0863d8 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -813,7 +813,7 @@ class VolcanoAsrApiService implements AsrService { } @override - Future enableRecord(String filePath) { + Future enableRecord(String filePath, {bool acceptAudioData = false}) { // TODO: implement enableRecord throw UnimplementedError(); } @@ -865,4 +865,22 @@ class VolcanoAsrApiService implements AsrService { // TODO: implement startContinuousRecognition throw UnimplementedError(); } + + @override + Future setAudioConfig({int sampleRate = 16000, int channels = 1}) { + // TODO: implement setAudioConfig + throw UnimplementedError(); + } + + @override + Future resumeRecord() { + // TODO: implement resumeRecord + throw UnimplementedError(); + } + + @override + Stream? getAudioDataStream() { + // TODO: implement getAudioDataStream + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index b5e1493ec..6d090d642 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -348,7 +348,7 @@ class VolcanoAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String filePath) { + Future enableRecord(String filePath, {bool acceptAudioData = false}) { // TODO: implement enableRecord throw UnimplementedError(); } @@ -400,4 +400,22 @@ class VolcanoAsrService extends GetxService implements AsrService { // TODO: implement startContinuousRecognition throw UnimplementedError(); } + + @override + Future setAudioConfig({int sampleRate = 16000, int channels = 1}) { + // TODO: implement setAudioConfig + throw UnimplementedError(); + } + + @override + Future resumeRecord() { + // TODO: implement resumeRecord + throw UnimplementedError(); + } + + @override + Stream? getAudioDataStream() { + // TODO: implement getAudioDataStream + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index 959f98859..29daf7015 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -272,7 +272,7 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String filePath) { + Future enableRecord(String filePath, {bool acceptAudioData = false}) { // TODO: implement enableRecord throw UnimplementedError(); } @@ -324,4 +324,22 @@ class XunfeiAsrService extends GetxService implements AsrService { // TODO: implement startContinuousRecognition throw UnimplementedError(); } + + @override + Future setAudioConfig({int sampleRate = 16000, int channels = 1}) { + // TODO: implement setAudioConfig + throw UnimplementedError(); + } + + @override + Future resumeRecord() { + // TODO: implement resumeRecord + throw UnimplementedError(); + } + + @override + Stream? getAudioDataStream() { + // TODO: implement getAudioDataStream + throw UnimplementedError(); + } } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 5ffe68ffa..eda548237 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1070,9 +1070,9 @@ class AgentController extends GetxController { try { await AgentService.recognizeCallback(); if (_bluetoothManager.currentDeviceRx.value != null) { - await AgentService.restoreOriginalAudioState(); + // await AgentService.restoreOriginalAudioState(); } else { - await AgentService.disableBluetoothAudio(); + // await AgentService.disableBluetoothAudio(); } } catch (e) { Logger.e(TAG, '启动识别器失败: $e'); diff --git a/lib/modules/login/bindings/login_binding.dart b/lib/modules/login/bindings/login_binding.dart index 6729681f9..28907a22e 100644 --- a/lib/modules/login/bindings/login_binding.dart +++ b/lib/modules/login/bindings/login_binding.dart @@ -4,6 +4,7 @@ import '../controllers/login_controller.dart'; class LoginBinding extends Bindings { @override void dependencies() { - Get.lazyPut(() => LoginController()); + // 使用 put 而不是 lazyPut,确保控制器立即创建 + Get.put(LoginController(), permanent: false); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 2740a5b28..3b0c7f590 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -214,15 +214,30 @@ class LoginController extends GetxController { List get verificationCode => _verificationCode; final versionService = VersionUpdateService.to; - // 文本控制器 - TextEditingController contactController = TextEditingController(); - TextEditingController verificationCodeController = TextEditingController(); + // 文本控制器 - 延迟初始化 + TextEditingController? _contactController; + TextEditingController? _verificationCodeController; + + // Getter 方法,确保控制器存在 + TextEditingController get contactController { + _contactController ??= TextEditingController(); + return _contactController!; + } + + TextEditingController get verificationCodeController { + _verificationCodeController ??= TextEditingController(); + return _verificationCodeController!; + } @override void onInit() { super.onInit(); _viewState.value = LoginViewState.welcome; + // 初始化控制器 + _contactController = TextEditingController(); + _verificationCodeController = TextEditingController(); + isChinaMainland.value = Get.deviceLocale?.countryCode?.toUpperCase() == 'CN'; @@ -239,8 +254,21 @@ class LoginController extends GetxController { @override void onClose() { - // contactController.dispose(); - // verificationCodeController.dispose(); + // 安全地销毁控制器 + try { + _contactController?.dispose(); + _contactController = null; + } catch (e) { + Logger().e('销毁 contactController 失败: $e'); + } + + try { + _verificationCodeController?.dispose(); + _verificationCodeController = null; + } catch (e) { + Logger().e('销毁 verificationCodeController 失败: $e'); + } + super.onClose(); } diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 42318e96d..87165d0b0 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -371,27 +371,29 @@ class LoginView extends GetView { ), SizedBox(width: 12.w), Expanded( - child: TextField( - controller: controller.verificationCodeController, - keyboardType: TextInputType.number, - maxLength: 6, - decoration: InputDecoration( - hintText: 'verificationCode'.tr, // 验证码 - hintStyle: TextStyle( + child: GetBuilder( + builder: (controller) => TextField( + controller: controller.verificationCodeController, + keyboardType: TextInputType.number, + maxLength: 6, + decoration: InputDecoration( + hintText: 'verificationCode'.tr, // 验证码 + hintStyle: TextStyle( + fontSize: 14.sp, + color: isDarkMode + ? Colors.grey[500] + : Colors.black38, // 调整提示文字颜色 + ), + border: InputBorder.none, + counterText: '', + fillColor: Colors.transparent, + filled: true, + focusedBorder: InputBorder.none, + ), + style: TextStyle( fontSize: 14.sp, - color: isDarkMode - ? Colors.grey[500] - : Colors.black38, // 调整提示文字颜色 + color: isDarkMode ? Colors.white : Colors.black, ), - border: InputBorder.none, - counterText: '', - fillColor: Colors.transparent, - filled: true, - focusedBorder: InputBorder.none, - ), - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, ), ), ), diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 8cd767ee0..50736fe52 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -60,6 +60,7 @@ class MeetingRecordController extends GetxController bool _audioSourceType = false; late TabController tabController; + StreamSubscription? _audioDataSubscription; // 添加波形放大倍数变量 final double waveAmplifyFactor = 100.0; // 增加波动幅度 final RxDouble ursorPosition = 0.0.obs; // 当前播放位置 @@ -123,7 +124,7 @@ class MeetingRecordController extends GetxController /// Initializes UI controllers and focus nodes void _initializeComponents() { - tabController = TabController(length: 2, vsync: this); + tabController = TabController(length: 1, vsync: this); titleEditingController = TextEditingController(text: fileName.value); titleFocusNode = FocusNode(); titleFocusNode.addListener(_handleTitleFocusChange); @@ -255,7 +256,22 @@ class MeetingRecordController extends GetxController final fullFileName = "${fileName.value}_$formattedTime"; // Start audio recording - await _asrService.enableRecord("${dir.path}/$fullFileName.wav"); + await _asrService.enableRecord("${dir.path}/$fullFileName.wav", + acceptAudioData: true); + + // 监听音频数据流 + _audioDataSubscription = + _asrService.getAudioDataStream()?.listen((audioData) { + // 处理音频数据,例如: + // 1. 实时音频可视化 + // 2. 音频质量检测 + // 3. 发送到其他服务 + print('接收到音频数据: ${audioData.length} 字节'); + + // 处理音频数据的逻辑 + _processAudioData(audioData); + }); + isRecording.value = true; // Update state fileName.value = fullFileName; @@ -274,9 +290,11 @@ class MeetingRecordController extends GetxController // _bleManager.openEncoder(); break; case 1: + _asrService.setAudioConfig(sampleRate: 16000, channels: 1); _bleManager.openDecoder(); break; case 2: + _asrService.setAudioConfig(sampleRate: 16000, channels: 2); _bleManager.openA2DPDecoder(); break; } @@ -291,7 +309,7 @@ class MeetingRecordController extends GetxController /// Resumes paused recording session Future _resumeRecording() async { - await _asrService.startContinuousRecognition(_audioSourceType); + await _asrService.resumeRecord(); startTimer(); } @@ -302,10 +320,6 @@ class MeetingRecordController extends GetxController case RecognitionEventType.intermediateResult: intermediateContent.value = event.text; break; - case RecognitionEventType.onAudio: - // 添加新音频数据到波形 - _processAudioData(event.audio!); - break; case RecognitionEventType.finalResult: finalContent.value += _formatTranscript(event); intermediateContent.value = ''; diff --git a/lib/modules/opus_test/controllers/opus_test_controller.dart b/lib/modules/opus_test/controllers/opus_test_controller.dart index 6297dee3a..4fad692e9 100644 --- a/lib/modules/opus_test/controllers/opus_test_controller.dart +++ b/lib/modules/opus_test/controllers/opus_test_controller.dart @@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:just_audio/just_audio.dart'; import 'package:jl_opus/jl_opus.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:share_plus/share_plus.dart'; // 添加分享插件导入 class OpusTestController extends GetxController { // 选中的文件 @@ -231,7 +232,7 @@ class OpusTestController extends GetxController { isDecoding.value = false; return; } - + print("pcmPath: $pcmPath"); tempPcmPath = pcmPath; // 将PCM文件转换为WAV文件 @@ -346,6 +347,166 @@ class OpusTestController extends GetxController { // 数据大小 data.setUint32(40, pcmLength, Endian.little); } + + /// 删除外部文件 + Future deleteExternalFile(PlatformFile file) async { + try { + final fileToDelete = File(file.path!); + if (await fileToDelete.exists()) { + await fileToDelete.delete(); + externalFiles.removeWhere((f) => f.path == file.path); + + // 如果删除的是当前选中的文件,清空选择 + if (selectedFiles.isNotEmpty && selectedFiles.first.path == file.path) { + selectedFiles.clear(); + } + + statusMessage.value = '文件已删除: ${file.name}'; + } else { + statusMessage.value = '文件不存在: ${file.name}'; + } + } catch (e) { + statusMessage.value = '删除文件失败: $e'; + } + } + + /// 分享外部文件 + Future shareExternalFile(PlatformFile file) async { + try { + if (file.path != null) { + final fileToShare = File(file.path!); + if (await fileToShare.exists()) { + await Share.shareXFiles( + [XFile(file.path!)], + text: '分享Opus音频文件: ${file.name}', + ); + statusMessage.value = '正在分享文件: ${file.name}'; + } else { + statusMessage.value = '文件不存在: ${file.name}'; + } + } + } catch (e) { + statusMessage.value = '分享文件失败: $e'; + } + } + + /// 显示文件操作底部弹窗 + void showFileActionSheet(BuildContext context, PlatformFile file) { + showModalBottomSheet( + context: context, + backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) => Container( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 文件信息 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Icon( + Icons.audiotrack, + color: Get.isDarkMode ? Colors.white70 : Colors.black87, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + file.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Get.isDarkMode ? Colors.white : Colors.black, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + '${(file.size / 1024).toStringAsFixed(2)} KB', + style: TextStyle( + fontSize: 14, + color: Get.isDarkMode + ? Colors.white60 + : Colors.black54, + ), + ), + ], + ), + ), + ], + ), + ), + const Divider(), + // 分享选项 + ListTile( + leading: const Icon(Icons.share, color: Colors.blue), + title: const Text('分享文件'), + onTap: () { + Navigator.pop(context); + shareExternalFile(file); + }, + ), + // 删除选项 + ListTile( + leading: const Icon(Icons.delete, color: Colors.red), + title: const Text('删除文件', style: TextStyle(color: Colors.red)), + onTap: () { + Navigator.pop(context); + _showDeleteConfirmDialog(context, file); + }, + ), + // 取消选项 + ListTile( + leading: const Icon(Icons.cancel), + title: const Text('取消'), + onTap: () => Navigator.pop(context), + ), + ], + ), + ), + ); + } + + /// 显示删除确认对话框 + void _showDeleteConfirmDialog(BuildContext context, PlatformFile file) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + '确认删除', + style: TextStyle( + color: Get.isDarkMode ? Colors.white : Colors.black, + ), + ), + content: Text( + '确定要删除文件 "${file.name}" 吗?\n此操作不可撤销。', + style: TextStyle( + color: Get.isDarkMode ? Colors.white70 : Colors.black87, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + deleteExternalFile(file); + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('删除'), + ), + ], + ), + ); + } } // PCM音频源 diff --git a/lib/modules/opus_test/views/opus_test_view.dart b/lib/modules/opus_test/views/opus_test_view.dart index adecc15b6..de02e5138 100644 --- a/lib/modules/opus_test/views/opus_test_view.dart +++ b/lib/modules/opus_test/views/opus_test_view.dart @@ -170,6 +170,10 @@ class OpusTestView extends GetView { onTap: () { controller.selectExternalFile(file); }, + onLongPress: () { + // 长按显示操作菜单 + controller.showFileActionSheet(context, file); + }, trailing: Icon( Icons.arrow_forward, color: diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 4dcecf302..496e405e3 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -192,7 +192,18 @@ class SettingsController extends GetxController { }, {'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'} ] - } + }, + { + 'name': '闹钟权限', + 'icon': Icons.alarm, + 'permissions': [ + { + 'type': Permission.scheduleExactAlarm, + 'name': '闹钟', + 'description': '用于设置精确闹钟' + } + ] + }, ]; // 检查单个权限状态 @@ -453,7 +464,8 @@ class SettingsController extends GetxController { Logger().i('更新agent服务语音识别语言: ${supportedLanguages.join(', ')}'); // 调用agent服务更新语言设置 - final success = await AgentService.setSupportedLanguages(supportedLanguages); + final success = + await AgentService.setSupportedLanguages(supportedLanguages); if (success) { Logger().i('agent服务语言设置更新成功'); } else { diff --git a/lib/modules/settings/views/permissions_view.dart b/lib/modules/settings/views/permissions_view.dart index c6eebac8c..2b0ceaf80 100644 --- a/lib/modules/settings/views/permissions_view.dart +++ b/lib/modules/settings/views/permissions_view.dart @@ -4,6 +4,8 @@ import 'package:permission_handler/permission_handler.dart'; import '../controllers/settings_controller.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:ui'; +import 'dart:io' show Platform; + import '../../../core/utils/permission_util.dart'; // 引入PermissionUtil class PermissionsView extends GetView { @@ -37,7 +39,7 @@ class PermissionsView extends GetView { centerTitle: true, ), body: FutureBuilder>( - future: controller.checkAllPermissions(), + future: controller.checkAllPermissions(), //检查权限当前状态 builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -45,8 +47,8 @@ class PermissionsView extends GetView { if (snapshot.hasError) { return Center( - child: Text('getPermissionStatusFailed'.tr + - ': ${snapshot.error}')); // 获取权限状态失败 + child: Text( + '${'getPermissionStatusFailed'.tr}: ${snapshot.error}')); // 获取权限状态失败 } final permissionStatuses = snapshot.data ?? {}; @@ -54,15 +56,27 @@ class PermissionsView extends GetView { return ListView( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), children: [ - // 通讯权限组 - _buildPermissionSection( - 'communicationPermissions'.tr, // 通讯权限 - Icons.phone_android, - [Permission.phone, Permission.sms], + // 通讯权限组- iOS平台没有通讯权限 + if (Platform.isAndroid) ...[ + _buildPermissionSection( + 'communicationPermissions'.tr, // 通讯权限 + Icons.phone_android, + [Permission.phone, Permission.sms], + permissionStatuses, + isDarkMode), + SizedBox(height: 20.h), + ], + // 闹钟权限组 + if (Platform.isAndroid) ...[ + _buildPermissionSection( + 'alarmPermissions'.tr, // 闹钟权限 + Icons.alarm, + [Permission.scheduleExactAlarm], permissionStatuses, - isDarkMode), - - SizedBox(height: 20.h), + isDarkMode, + ), + SizedBox(height: 20.h), + ], // 日历权限组 _buildPermissionSection( @@ -162,6 +176,8 @@ class PermissionsView extends GetView { return 'calendarFullAccess'.tr; // 日历完整访问 case Permission.location: return 'locationInfo'.tr; // 位置信息 + case Permission.scheduleExactAlarm: + return 'alarm'.tr; // 闹钟 default: return permission.toString(); } @@ -180,6 +196,8 @@ class PermissionsView extends GetView { return 'calendarFullAccessPermissionDesc'.tr; // 用于读取和管理日历事件及提醒 case Permission.location: return 'locationPermissionDesc'.tr; // 用于获取当前位置信息 + case Permission.scheduleExactAlarm: + return 'alarmPermissionDesc'.tr; // 设置精确闹钟需要此权限 default: return 'appFunctionNeeds'.tr; // 应用功能需要 } @@ -355,6 +373,8 @@ class PermissionsView extends GetView { return 'calendarFullAccess'.tr; // 日历完整访问 case Permission.location: return 'locationInfo'.tr; // 位置信息 + case Permission.scheduleExactAlarm: + return 'alarm'.tr; // 闹钟 default: return permission.toString(); } @@ -373,6 +393,8 @@ class PermissionsView extends GetView { return 'calendarFullAccessPermissionDetailDesc'.tr; // 管理日历事件需要完整日历权限 case Permission.location: return 'locationPermissionDetailDesc'.tr; // 获取位置信息需要位置权限 + case Permission.scheduleExactAlarm: + return 'alarmPermissionDetailDesc'.tr; // 设置精确闹钟需要此权限 default: return 'appNeedsPermissionForFullFunction'.tr; // 应用需要此权限以提供完整功能 } diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 7e2c18d9a..24aff0513 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -514,23 +514,23 @@ 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, - // ), + //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 diff --git a/lib/modules/speech_test/controllers/speech_test_controller.dart b/lib/modules/speech_test/controllers/speech_test_controller.dart index ec1833c90..57d54bcad 100644 --- a/lib/modules/speech_test/controllers/speech_test_controller.dart +++ b/lib/modules/speech_test/controllers/speech_test_controller.dart @@ -164,7 +164,15 @@ class SpeechTestController extends GetxController { } recognitionStatus.value = '识别完成'; break; - + case RecognitionEventType.finalResult1: + // 更新最终结果,显示为正常文本 + finalResult.value = event.text; + intermediateResult.value = ''; // 清空中间结果 + if (event.detectedLanguage.isNotEmpty) { + detectedLanguage.value = event.detectedLanguage; + } + recognitionStatus.value = '识别完成'; + break; case RecognitionEventType.error: isListening.value = false; recognitionStatus.value = '识别错误: ${event.error}'; @@ -176,9 +184,6 @@ class SpeechTestController extends GetxController { // 清空中间结果 intermediateResult.value = ''; break; - case RecognitionEventType.onAudio: - // TODO: Handle this case. - break; } }, onError: (error) { isListening.value = false; diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index d3b3c1631..52a2ad2c2 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../../data/services/ast_service.dart'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/music_manager.dart'; import '../../../data/services/volcano_translation_service.dart'; @@ -37,6 +38,8 @@ class TranslationController extends GetxController { final VolcanoTranslationService _translationService = Get.find(); final TtsService _ttsService = Get.find(); + + final AstService _astService = Get.find(); final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); // 蓝牙服务 @@ -62,6 +65,8 @@ class TranslationController extends GetxController { Timer? _recordTimer; //秒 int _seconds = 0; + //暂停状态记录计时器是否暂停 + bool _isTimerPaused = false; // 存储相关配置 static const String _historyKey = 'translation_history'; static const String _sourceLanguageKey = 'translation_source_language'; @@ -89,6 +94,8 @@ class TranslationController extends GetxController { final isTranslating = false.obs; final isTtsEnabled = true.obs; final isRecording = false.obs; + final lasyIsRecording = false.obs; + final hasRecordPermission = false.obs; // 语言相关 final sourceLanguage = '中文(简体)'.obs; final targetLanguage = '英语'.obs; @@ -107,11 +114,15 @@ class TranslationController extends GetxController { var appDir; var dir; +// 最新的语音识别文本 + final latestRecognitionText = ''.obs; + +// 检测到的语言代码 + final detectedLanguageCode1 = ''.obs; //int num = 0; Future changeTranslationMode(String mode) async { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); - stopAll(); } @@ -129,12 +140,57 @@ class TranslationController extends GetxController { } } + /// 暂停计时器 + void pauseRecordTimer() { + if (_recordTimer != null && !_isTimerPaused) { + _recordTimer?.cancel(); + _recordTimer = null; + _isTimerPaused = true; + Logger.info('录音计时器已暂停'); + } + } + + /// 恢复计时器 + void resumeRecordTimer() { + if (_isTimerPaused) { + _recordTimer = Timer.periodic(Duration(seconds: 1), (_) { + _seconds++; + final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); + final seconds = (_seconds % 60).toString().padLeft(2, '0'); + recordDurationText.value = '$minutes:$seconds'; + }); + _isTimerPaused = false; + Logger.info('录音计时器已恢复'); + } + } + + /// 重置计时器 + void resetRecordTimer() { + _recordTimer?.cancel(); + _recordTimer = null; + _seconds = 0; + _isTimerPaused = false; + recordDurationText.value = '00:00'; + Logger.info('录音计时器已重置'); + } + Future setActiveSpeaker(int id) async { activeSpeaker.value = id; + await _asrService.startContinuousRecognition(_audioSourceType); // 监听识别结果 if (activeSpeaker.value != 0) { isPlayback = false; - await _asrService.startContinuousRecognition(_audioSourceType); + isRecognizing.value = true; + if (isRecording.value && isRecognizing.value) { + if (lasyIsRecording.value != isRecording.value) { + lasyIsRecording.value = isRecording.value; + await startRecording(); + } else { + await _asrService.resumeRecord(); + // 恢复计时器 + resumeRecordTimer(); + } + } // 开始ASR活跃时长跟踪(面对面模式) _startAsrActiveTracking(); @@ -143,7 +199,11 @@ class TranslationController extends GetxController { } else { // 停止ASR活跃时长跟踪(面对面模式) _stopAsrActiveTracking(); - + if (isRecording.value && isRecognizing.value) { + // 暂停计时器 + pauseRecordTimer(); + await _asrService.pauseRecord(); + } if (fTFTranslationResult != '' && !isPlayback) { isPlayback = true; await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); @@ -181,7 +241,6 @@ class TranslationController extends GetxController { StreamSubscription? _recognitionSubscription; StreamSubscription? _voiceInteractionSubscription; Timer? _translationDebounceTimer; - // 获取支持的语言列表 Map get supportedLanguages => _languageManager.getChineseNameToAsrCodeMap(); @@ -191,6 +250,9 @@ class TranslationController extends GetxController { super.onInit(); //_setupEventListeners(); + // 预先检查权限,避免在用户交互时阻塞 + _preCheckPermissions(); + // 初始化统计服务 _initUsageService(); @@ -215,11 +277,21 @@ class TranslationController extends GetxController { // 初始化服务 _initServices(); - + // 初始化语音翻译服务 + _initializeCallModeTranslationService(); // 🔑 reverse模式下不需要手动滚动,ListView会自然显示最新内容 // 参考Agent模块的实现模式 } + /// 预先检查权限 + Future _preCheckPermissions() async { + try { + await requestRecordPermission(); + } catch (e) { + Logger.e("Permission", "预检查权限失败: $e"); + } + } + // 初始化统计服务 void _initUsageService() { try { @@ -256,16 +328,17 @@ class TranslationController extends GetxController { _storage.write(_targetLanguageKey, targetLanguage.value); } - Future _requestRecordPermission() async { + Future requestRecordPermission() async { try { - final bool granted = await PermissionUtil.instance.requestPermission( + hasRecordPermission.value = + await PermissionUtil.instance.requestPermission( permissionType: Permission.microphone, permissionName: 'microphonePermission'.tr, explanationText: 'microphonePermissionExplanation'.tr, permanentDenialText: 'microphonePermissionPermanentDenial'.tr, ); - if (granted) { + if (hasRecordPermission.value) { Logger.d("Permission", "用户授予了录音权限"); return true; } else { @@ -349,6 +422,7 @@ class TranslationController extends GetxController { _recognitionSubscription?.cancel(); _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); + final bool storage = await _requestStoragePermission(); if (!storage) { Logger.d("Permission", "未授予存储权限,无法提供录音功能"); @@ -365,6 +439,144 @@ class TranslationController extends GetxController { } } + // 初始化通话模式的语音翻译服务 + Future _initializeCallModeTranslationService() async { + try { + Logger.info('开始初始化通话模式语音翻译服务'); + // 为通话模式配置特殊的ASR设置 + final List callModeLanguages = [ + sourceLanguageCode.value, + targetLanguageCode.value + ]; +// Future initialize({ +// required String subscriptionKey, +// required String region, +// required List supportedLanguages, +// required String audioSourceType, +// required String translationAccessKey, +// required String translationSecretKey, +// String translationRegion = 'cn-north-1', +// }); + // 重新初始化ASR服务以支持通话音频源 + await _astService.initialize(supportedLanguages: callModeLanguages); + + // 配置实时翻译参数 + await _configureCallModeTranslation(); + + Logger.info('通话模式语音翻译服务初始化完成'); + return; + } catch (e) { + Logger.error('通话模式语音翻译服务初始化失败: ${e.toString()}'); + } + } + + // 配置通话模式的翻译参数 + Future _configureCallModeTranslation() async { + try { + // 设置通话模式的特殊配置 + // 1. 更短的识别超时时间,适应通话场景 + // 2. 更高的识别敏感度 + // 3. 噪声抑制优化 + + // 这里可以调用ASR服务的特殊配置方法 + // await _asrService.configureForCallMode( + // endSilenceTimeout: 200, // 更短的静音超时 + // noiseReduction: true, // 启用噪声抑制 + // echoCancellation: true, // 启用回声消除 + // ); + + Logger.info('通话模式翻译参数配置完成'); + } catch (e) { + Logger.error('通话模式翻译参数配置失败: ${e.toString()}'); + } + } + + // // 处理通话模式的翻译结果 + // Future _handleCallModeTranslation(String sourceText) async { + // try { + // // 在通话模式下,翻译结果可能需要特殊处理 + // // 例如:发送到蓝牙设备、显示在特定UI等 + + // final translationResult = await _translationService.translateText( + // text: sourceText, + // sourceLanguageCode: sourceLanguageCode.value, + // targetLanguageCode: targetLanguageCode.value, + // ); + + // if (translationResult != null && translationResult.isNotEmpty) { + // // 通话模式下的特殊处理 + // await _processCallModeTranslationResult(sourceText, translationResult); + // } + // } catch (e) { + // Logger.error('通话模式翻译处理失败: ${e.toString()}'); + // } + // } + + // // 处理通话模式的翻译结果 + // Future _processCallModeTranslationResult(String sourceText, String translatedText) async { + // try { + // // 1. 更新UI显示 + // final newItem = TranslationItem( + // sourceText: sourceText, + // translatedText: translatedText, + // sourceLanguageCode: sourceLanguageCode.value, + // targetLanguageCode: targetLanguageCode.value, + // timestamp: DateTime.now(), + // sessionId: currentSessionId ?? DateTime.now().millisecondsSinceEpoch.toString(), + // isFirstInSession: translationHistory.isEmpty, + // isIntermediate: false, + // ); + + // translationHistory.add(newItem); + // translationHistory.refresh(); + // _scrollToBottom(); + // saveTranslationHistory(); + + // // 2. 通话模式下可能需要将翻译结果发送到蓝牙设备 + // // 或者通过其他方式传输给通话对方 + // await _sendTranslationToCallParty(translatedText); + + // // 3. 记录统计信息 + // _recordCallModeUsageStats(sourceText, translatedText); + + // Logger.info('通话模式翻译结果处理完成: $sourceText -> $translatedText'); + // } catch (e) { + // Logger.error('通话模式翻译结果处理失败: ${e.toString()}'); + // } + // } + + // // 将翻译结果发送给通话对方 + // Future _sendTranslationToCallParty(String translatedText) async { + // try { + // // 这里可以实现将翻译结果发送给通话对方的逻辑 + // // 例如:通过蓝牙、网络等方式 + + // // 示例:通过蓝牙发送 + // await bleManager.sendTranslationResult(translatedText); + + // Logger.info('翻译结果已发送给通话对方: $translatedText'); + // } catch (e) { + // Logger.error('发送翻译结果失败: ${e.toString()}'); + // } + // } + + // // 记录通话模式的使用统计 + // void _recordCallModeUsageStats(String sourceText, String translatedText) { + // try { + // _usageService.recordTranslationApiCall( + // sourceText: sourceText, + // targetText: translatedText, + // sourceLanguage: _languageManager.getChineseNameByAsrCode(sourceLanguageCode.value) ?? '未知', + // targetLanguage: _languageManager.getChineseNameByAsrCode(targetLanguageCode.value) ?? '未知', + // mode: 'call', // 明确标记为通话模式 + // ); + + // Logger.info('通话模式使用统计已记录'); + // } catch (e) { + // Logger.error('记录通话模式统计失败: ${e.toString()}'); + // } + // } + @override void onClose() { stopAll(); @@ -392,11 +604,6 @@ class TranslationController extends GetxController { } Future startRecording() async { - final bool storage = await _requestStoragePermission(); - if (!storage) { - Logger.d("Permission", "未授予存储权限,无法提供录音功能"); - return; - } //isRecording.value = true; _seconds = 0; recordDurationText.value = '00:00'; @@ -412,23 +619,29 @@ class TranslationController extends GetxController { final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); await _asrService.enableRecord( "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + // await _astService.enableRecord( + // "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); + return; } Future stopRecording() async { _recordTimer?.cancel(); _recordTimer = null; isRecording.value = false; + _isTimerPaused = false; recordDurationText.value = '00:00'; - await _asrService.stopRecord(true); - // 停止实际录音逻辑 - // ... + _seconds = 0; + + _asrService.stopRecord(true); + lasyIsRecording.value = false; + Logger.info('录音已停止,计时器已重置'); } // 开始语音识别 Future startRecognition() async { Logger.info('开始语音识别'); // 等待权限请求完成并获取结果 - final bool record = await _requestRecordPermission(); + final bool record = await requestRecordPermission(); // 如果权限被拒绝,直接返回 if (!record) { @@ -476,6 +689,8 @@ class TranslationController extends GetxController { bleManager.openA2DPDecoder(); _audioSourceType = true; isTtsEnabled.value = false; + await _astService.startContinuousTranslation(); + // await _astService.path("${dir.path}/8_mic.wav"); Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); } else { // 开始连续语音识别 @@ -483,18 +698,14 @@ class TranslationController extends GetxController { isTtsEnabled.value = true; } - if (currentMode.value == 'faceToFace') { - _asrService.stopContinuousRecognition(); + if (_bluetoothManager.currentDeviceRx.value != null) { + restoreOriginalAudioState(); } else { - if (_bluetoothManager.currentDeviceRx.value != null) { - restoreOriginalAudioState(); - } else { - disableBluetoothAudio(); - } - await _asrService.startContinuousRecognition(_audioSourceType); - // 开始ASR活跃时长计时 - _startAsrActiveTracking(); + disableBluetoothAudio(); } + await _asrService.startContinuousRecognition(_audioSourceType); + // 开始ASR活跃时长计时 + _startAsrActiveTracking(); isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { startRecording(); @@ -510,13 +721,22 @@ class TranslationController extends GetxController { if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) { detectedLanguageCode = event.detectedLanguage; + print("处理识别事件:${event.text}"); handleFinalResult(event.text); } else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) { if (event.detectedLanguage.isNotEmpty) { detectedLanguageCode = event.detectedLanguage; } + print("处理识别事件:${event.text}"); handleIntermediateResult(event.text); + } else if (event.type == RecognitionEventType.finalResult1 && + event.text.isNotEmpty) { + if (event.detectedLanguage.isNotEmpty) { + detectedLanguageCode1.value = event.detectedLanguage; + } + latestRecognitionText.value = event.text; + print("处理麦:${event.text}"); } else if (event.type == RecognitionEventType.error) { isRecognizing.value = false; Logger.error('语音识别错误: ${event.error}'); @@ -531,8 +751,9 @@ class TranslationController extends GetxController { if (!isRecognizing.value) return; try { + await stopRecording(); await _asrService.stopContinuousRecognition(); - + await _astService.stopContinuousTranslation(); // 停止ASR活跃时长计时 _stopAsrActiveTracking(); @@ -555,7 +776,7 @@ class TranslationController extends GetxController { saveTranslationHistory(); } - stopRecording(); + currentSessionId = null; } catch (e) { Logger.error('停止语音识别失败: ${e.toString()}'); @@ -833,7 +1054,6 @@ class TranslationController extends GetxController { } // 确保停止ASR活跃时长跟踪 _stopAsrActiveTracking(); - stopRecording(); _translationDebounceTimer?.cancel(); await _ttsService.stop(); } catch (e) { @@ -979,6 +1199,9 @@ class TranslationController extends GetxController { // 重新初始化ASR服务以支持新的语言 Future _reinitializeAsrService() async { try { + // 停止当前识别 + await stopRecognition(); + // 更新ASR支持的语言 final List asrSupportedLanguages = [ sourceLanguageCode.value, @@ -991,6 +1214,17 @@ class TranslationController extends GetxController { await _asrService.initialize( supportedLanguages: asrSupportedLanguages, ); +// Future initialize({ +// required String subscriptionKey, +// required String region, +// required List supportedLanguages, +// required String audioSourceType, +// required String translationAccessKey, +// required String translationSecretKey, +// String translationRegion = 'cn-north-1', +// }); + // 重新初始化ASR服务以支持通话音频源 + await _astService.initialize(supportedLanguages: asrSupportedLanguages); // 获取识别事件流 - 这启动了异步识别过程 var recognitionStream = await _asrService.recognizeCallback(); // 监听识别结果 diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index a7c21d007..2998bc6ab 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -459,67 +459,128 @@ class TranslationView extends GetView { ], ), child: Obx(() { - // 同时模式显示双按钮 - if (controller.currentMode.value == 'faceToFace') { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 耳机图标按钮 (说话者1) - _buildHoldToTalkButton( - isDarkMode: isDarkMode, - speakerId: 1, - activeSpeaker: controller.activeSpeaker.value, - icon: Icons.headset, // 耳机图标 - label: "headsetUser".tr, // 耳机用户 + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 原有的按钮区域 + if (controller.currentMode.value == 'faceToFace') + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 耳机图标按钮 (说话者1) + _buildHoldToTalkButton( + isDarkMode: isDarkMode, + speakerId: 1, + activeSpeaker: controller.activeSpeaker.value, + icon: Icons.headset, + label: "headsetUser".tr, + ), + SizedBox(width: 40.w), + // 手机图标按钮 (说话者2) + _buildHoldToTalkButton( + isDarkMode: isDarkMode, + speakerId: 2, + activeSpeaker: controller.activeSpeaker.value, + icon: Icons.phone_android, + label: "phoneUser".tr, + ), + ], + ) + else + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 主要控制按钮 - 开始/暂停语音识别 + Obx(() { + final isRecognizing = controller.isRecognizing.value; + return GestureDetector( + onTap: () { + if (isRecognizing) { + controller.stopAll(); + } else { + controller.startRecognition(); + } + }, + child: Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: isRecognizing + ? (isDarkMode ? Colors.red[700] : Colors.red) + : (isDarkMode ? AppColors.primary : Colors.blue), + shape: BoxShape.circle, + ), + child: Icon( + isRecognizing ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 30.sp, + ), + ), + ); + }), + ], ), - SizedBox(width: 40.w), - // 手机图标按钮 (说话者2) - _buildHoldToTalkButton( - isDarkMode: isDarkMode, - speakerId: 2, - activeSpeaker: controller.activeSpeaker.value, - icon: Icons.phone_android, // 手机图标 - label: "phoneUser".tr, // 手机用户 + + // 添加间距 + SizedBox(height: 16.h), + + // 语音识别结果显示框 + Container( + width: double.infinity, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: isDarkMode ? Colors.grey[600]! : Colors.grey[300]!, + width: 1, + ), ), - ], - ); - } - // 普通模式显示单按钮 - else { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 主要控制按钮 - 开始/暂停语音识别 - Obx(() { - final isRecognizing = controller.isRecognizing.value; - return GestureDetector( - onTap: () { - if (isRecognizing) { - controller.stopAll(); - } else { - controller.startRecognition(); - } - }, - child: Container( - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - color: isRecognizing - ? (isDarkMode ? Colors.red[700] : Colors.red) - : (isDarkMode ? AppColors.primary : Colors.blue), - shape: BoxShape.circle, + child: Obx(() { + // 显示最新的语音识别结果 + final recognitionText = controller.latestRecognitionText.value; + final detectedLanguage = controller.detectedLanguageCode1.value; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + Text( + '麦头语音识别结果', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontWeight: FontWeight.w500, + ), ), - child: Icon( - isRecognizing ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 30.sp, + SizedBox(height: 4.h), + // 识别文本 - 这里显示 finalResult1 的内容 + Text( + recognitionText.isEmpty ? '等待语音输入...' : recognitionText, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black87, + height: 1.3, + ), ), - ), + // 检测到的语言(如果有) + if (detectedLanguage.isNotEmpty) ...[ + SizedBox(height: 4.h), + Text( + '检测语言: $detectedLanguage', + style: TextStyle( + fontSize: 10.sp, + color: + isDarkMode ? Colors.grey[500] : Colors.grey[500], + ), + ), + ], + ], ); }), - ], - ); - } + ), + ], + ); }), ); } @@ -587,8 +648,13 @@ class TranslationView extends GetView { bool isDisabled = activeSpeaker != 0 && activeSpeaker != speakerId; Timer? _holdTimer; // 增加延迟触发的计时器 return Listener( - onPointerDown: (_) { - // 设置300ms延迟防止误触 + onPointerDown: (_) async { + // 同步检查权限状态,不使用 await + if (!controller.hasRecordPermission.value) { + Get.snackbar('权限提示', '需要麦克风权限才能使用录音功能'); + return; + } + _holdTimer = Timer(const Duration(milliseconds: 200), () { if (activeSpeaker == 0) { controller.setActiveSpeaker(speakerId); 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 cd0cbf423..87bca18a3 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 @@ -594,9 +594,7 @@ object AgentService : CoroutineScope { } - override fun onAudio(data: ByteArray) { - - } + override fun onSessionStopped() { sendEvent("recognition_stopped", mapOf("status" to "stopped")) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index a62e67ffd..dc8a689c4 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -135,7 +135,9 @@ Log.d(TAG, "手动启动语音识别: ") AgentService.pushAudioData(data) // 可选:处理音频数据 } - + override fun onAudioDataReceived1(data: ByteArray) { + + } /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别 diff --git a/local_plugins/agent_service/ios/agent_service/Package.swift b/local_plugins/agent_service/ios/agent_service/Package.swift index cc30a9ad6..83f572fff 100644 --- a/local_plugins/agent_service/ios/agent_service/Package.swift +++ b/local_plugins/agent_service/ios/agent_service/Package.swift @@ -14,6 +14,7 @@ let package = Package( .package(path: "../../../azure_speech/ios/azure_speech"), .package(path: "../../../chat_api/ios/chat_api"), .package(path: "../../../chat_storage/ios/chat_storage"), + .package(path: "../../../location_service/ios/location_service"), .package(path: "../../../music_service/ios/music_service"), .package(path: "../../../music_service/ios/user_portrait") @@ -28,6 +29,7 @@ let package = Package( .product(name: "azure-speech", package: "azure_speech"), .product(name: "chat-api", package: "chat_api"), .product(name: "chat-storage", package: "chat_storage"), + .product(name: "location-service", package: "location_service"), .product(name: "music-service", package: "music_service"), .product(name: "user-portrait", package: "user_portrait"), ], diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index e5ce57159..574689e3b 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -1,6 +1,7 @@ import Foundation import AVFoundation import ble_service +import location_service import speech import azure_speech import os @@ -65,7 +66,7 @@ class AgentServiceImpl: NSObject { private let maxIdleSeconds: TimeInterval = 10 internal var audioPlayer: AudioPlayer? - private var locationInfo:[String: String]? + // private var locationInfo:[String: String]? internal let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceImpl") @@ -182,7 +183,7 @@ class AgentServiceImpl: NSObject { } //启动定位服务 NotificationCenter.default.post(name: .locationstartEvent, object: nil) - addlocationmonitor() + // addlocationmonitor() if let mcpServer = config["mcpServer"] as? String { self.mcpServer = mcpServer } @@ -204,7 +205,9 @@ class AgentServiceImpl: NSObject { if isInitialized { loadChatHistory() } - + LocationService.shared.getCurrentLocation() { location in + // self.locationInfo = location + } return isInitialized } @@ -841,19 +844,19 @@ private func jsonToString(_ json: [String: Any]) -> String? { } // 添加定位监听 - private func addlocationmonitor(){ - // 通常放在启动后或模块初始化时 - NotificationCenter.default.addObserver( - forName: .updatelocationEvent, - object: nil, - queue: .main - ) { notification in - if let info = notification.userInfo as? [String: String] { - print("📤 地址信息:\(info)") - self.locationInfo = info - } - } - } + // private func addlocationmonitor(){ + // // 通常放在启动后或模块初始化时 + // NotificationCenter.default.addObserver( + // forName: .updatelocationEvent, + // object: nil, + // queue: .main + // ) { notification in + // if let info = notification.userInfo as? [String: String] { + // print("📤 地址信息:\(info)") + // self.locationInfo = info + // } + // } + // } /** * 填充模板字符串,使用正则表达式匹配 {key} 格式的占位符 * 对应 Kotlin 版本的 fillTemplate 方法 @@ -1379,6 +1382,10 @@ extension AgentServiceImpl: BleService.Callback { pushAudioData(data) } + func onAudioDataReceived1(data: Data) { + + } + func onWakeupSignalReceived() { stopTts() diff --git a/local_plugins/azure_speech/android/build.gradle.kts b/local_plugins/azure_speech/android/build.gradle.kts index 4d6a58288..c77148988 100644 --- a/local_plugins/azure_speech/android/build.gradle.kts +++ b/local_plugins/azure_speech/android/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { // 添加Microsoft语音SDK implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0") implementation(project(":speech")) + implementation("com.squareup.okhttp3:okhttp:4.12.0") add("compileOnly", project(":ble_service")) } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 0d00c776b..afc9e0409 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 @@ -11,7 +11,7 @@ import android.media.audiofx.AutomaticGainControl import android.util.Log import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.util.EventHandler - +import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver import android.media.AudioManager import android.media.AudioAttributes import android.media.AudioFocusRequest @@ -58,14 +58,11 @@ class AzureAsrHelper(private val context: Context) { var audioSourceType = AudioSourceType.MICROPHONE // 音频处理 - var audioStream: AudioStream? = null + var audioStream: SimpleAudioReceiver? = null + + - // 音频模式管理 - private var audioManager: AudioManager? = null - private var originalAudioMode = AudioManager.MODE_NORMAL - // 录音文件处理 - var recordfile: RecordFile? = null /** * 音频来源类型 @@ -103,10 +100,6 @@ class AzureAsrHelper(private val context: Context) { // 释放之前的资源 dispose() - // 初始化音频管理器 - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL - // 保存配置 this.subscriptionKey = subscriptionKey @@ -147,9 +140,7 @@ class AzureAsrHelper(private val context: Context) { // 设置分段策略为时间模式 setProperty("Speech_SegmentationStrategy", "Time") } - // 录音文件类 - recordfile = RecordFile; - + return true } catch (e: Exception) { @@ -158,6 +149,21 @@ class AzureAsrHelper(private val context: Context) { } } + // /** + // * 设置音频配置 + // * @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000) + // * @param channels 声道数 (1=单声道, 2=立体声) + // */ + // fun setAudioConfig(sampleRate: Int, channels: Int) { + // try { + // // 设置RecordFile的音频配置 + // recordfile?.setAudioConfig(sampleRate, channels) + // Log.d(tag, "音频配置已设置: 采样率=${sampleRate}Hz, 声道数=${channels}") + // } catch (e: Exception) { + // Log.e(tag, "设置音频配置失败: ${e.message}") + // } + // } + private fun isRecognizerValid(): Boolean { return recognizer != null } @@ -203,7 +209,7 @@ class AzureAsrHelper(private val context: Context) { if (audioStream == null) { // 创建外部音频拉流对象 - audioStream = AudioStream() + audioStream = SimpleAudioReceiver(context) audioStream!!.initAudioRecord() } @@ -237,11 +243,19 @@ class AzureAsrHelper(private val context: Context) { stopContinuousRecognition() } - - try { - // 启动音频处理 - audioStream!!.startAudioRecord() + // 启动音频处理(若已启动则跳过) + if (!audioStream!!.isWriting()) { + audioStream!!.startAudioRecord( + when (audioSourceType) { + AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE + AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL + }, + null + ) + } else { + Log.d(tag, "音频已启动,跳过重复启动") + } // 执行同步识别 val result = recognizer?.recognizeOnceAsync()?.get() @@ -264,13 +278,13 @@ class AzureAsrHelper(private val context: Context) { /** * 开始连续语音识别 - * - * @param callback 连续识别结果回调 - * @return 是否成功开始识别 + * @param audioSourceType 音频源类型 + * @param audioDataCallback 音频数据回调接口 + * @return 是否成功启动 */ fun startContinuousRecognition( - - audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE + audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE, + audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null ): Boolean { Log.d(tag, "startContinuousRecognition:$isContinuousRecognitionActive ") @@ -286,26 +300,29 @@ class AzureAsrHelper(private val context: Context) { } this.audioSourceType = audioSourceType - try { Log.d(tag, "startContinuousRecognition: ") - // 启动音频处理 - //startAudioProcessing() - // 开始连续识别 + // 启动音频处理(若已启动则跳过) + if (!audioStream!!.isWriting()) { + audioStream!!.startAudioRecord( + when (audioSourceType) { + AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE + AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL + }, + audioDataCallback + ) + } else { + Log.d(tag, "音频已启动,跳过重复启动") + } - // 启动音频处理 - audioStream!!.startAudioRecord() recognizer?.startContinuousRecognitionAsync() isContinuousRecognitionActive = true - - return true } catch (e: Exception) { audioStream!!.stopMicrophoneCapture() isContinuousRecognitionActive = false - return false } } @@ -468,7 +485,9 @@ class AzureAsrHelper(private val context: Context) { // 停止音频处理 stopAudioProcessing() // 停止录音 - recordfile!!.closeFile(true) + if (audioStream!!.recordfile != null) { + audioStream!!.recordfile!!.closeFile(true) + } // 释放recognizer recognizer?.close() recognizer = null @@ -523,53 +542,33 @@ class AzureAsrHelper(private val context: Context) { } - /** - * 禁用蓝牙音频功能,切换回正常音频模式 - */ - fun disableBluetoothAudio() { - try { - // 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由 - audioManager?.isBluetoothScoOn = false - // 停止蓝牙SCO连接 - audioManager?.stopBluetoothSco() - // 切换回正常音频模式(非通话模式) - audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION - - } catch (e: Exception) { - Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}") - } - } - - /** - * 恢复原始音频设备状态(通常是重新启用蓝牙) - */ - fun restoreOriginalAudioState() { - try { - // 恢复应用启动时的原始音频模式 - audioManager?.mode = AudioManager.MODE_NORMAL - - // 重新启用蓝牙SCO - audioManager?.isBluetoothScoOn = true - // 启动蓝牙SCO连接(通常在需要蓝牙通话时使用) - audioManager?.startBluetoothSco() - - } catch (e: Exception) { - Log.e("AudioConfig", "Failed to restore audio state: ${e.message}") - } - } + //录音音频文件 /** * 开启录音 */ - fun enableRecord(filePath: String) { + fun enableRecord(filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null) { Log.i(tag, "开启录音:") + if (audioStream == null) { + // 创建外部音频拉流对象 + audioStream = SimpleAudioReceiver(context) + audioStream!!.initAudioRecord() + } + if (audioStream!!.recordfile == null) { + audioStream!!.recordfile = RecordFile() + } - recordfile!!.closeFile(true) + // 启动音频处理 + audioStream!!.startAudioRecord( when(audioSourceType) { + AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE + AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL + },audioDataCallback) + audioStream!!.recordfile!!.closeFile(true) - recordfile!!.creatingFiles(filePath) + audioStream!!.recordfile!!.creatingFiles(filePath) } @@ -578,8 +577,8 @@ class AzureAsrHelper(private val context: Context) { */ fun moveFile(sourcePath: String, destPath: String): Boolean { Log.i(tag, "移动文件到新路径:") - - recordfile!!.moveFile(sourcePath, destPath) + if (audioStream!!.recordfile == null)return false + audioStream!!.recordfile!!.moveFile(sourcePath, destPath) return true } @@ -590,8 +589,8 @@ class AzureAsrHelper(private val context: Context) { fun renameFile(filePath: String, newName: String): Boolean { Log.i(tag, "重命名指定路径的音频文件:") - - recordfile!!.renameFile(filePath, newName) + if (audioStream!!.recordfile == null)return false + audioStream!!.recordfile!!.renameFile(filePath, newName) return true } @@ -600,307 +599,37 @@ class AzureAsrHelper(private val context: Context) { */ fun pauseRecord() { Log.i(tag, "停止连续录音:") + if (audioStream!!.recordfile == null)return + + audioStream!!.stopMicrophoneCapture() + - recordfile!!.isPause = true } - /** - * 关闭录音 + * 继续录音 */ - fun stopRecord(isSave: Boolean) { - Log.i(tag, "关闭录音:") - - recordfile!!.isPause = false - recordfile!!.closeFile(isSave) + fun resumeRecord() { + Log.i(tag, "继续录音:") + if (audioStream!!.recordfile == null)return + + audioStream!!.resumeRecord() } - /** - * 麦克风流 - 拉流模式 - * 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据 + * 关闭录音 */ - inner class AudioStream { - private val bufferSize = 4096 // 可根据需要调整 - var audioRecord: AudioRecord? = 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 - */ - 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) - } - - /** - * 初始化 - */ - fun initAudioRecord() { - - val format = getOptimalAudioFormat() - pushAudioStream = AudioInputStream.createPushStream(format) - isRunning.set(true) - startWriteThread() // 再启动数据读取线程 - - - } -/** - * 开启音频写入线程 - */ - 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 - } - - bytesRead == 0 -> continue // 无数据可读 - else -> bytesToWrite = bytesRead // 有效数据 - } - } - // 情况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 { - //continuousCallback?.onAudio(finalData) - pushAudioStream?.write(finalData) - recordfile?.saveAudioDataToWav(finalData) - } catch (e: Exception) { - Log.e("tag", "写入失败: ${e.message}") - } - } else { - Thread.yield() // 避免空转消耗CPU - } - } - } 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||!isContinuousRecognitionActive) return - // 放入队列,由写线程写入 - writeQueue.offer(buffer.copyOf()) - - } - - /** - * 开始音频输入 - */ - fun startAudioRecord() { - - isWriting.set(true) - - when (audioSourceType) { - - AudioSourceType.MICROPHONE -> runMicrophoneCapture() - AudioSourceType.EXTERNAL -> runExternalCapture() - } - - } - - private fun runMicrophoneCapture() { - try { - // 首先设置通话音频模式 - setupCommunicationAudioMode() - - 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) - - // 使用VOICE_COMMUNICATION音频源(专为VoIP优化) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - val format = AudioFormat.Builder() - .setSampleRate(sampleRate) - .setEncoding(audioFormat) - .setChannelMask(AudioFormat.CHANNEL_IN_MONO) - .build() - - audioRecord = AudioRecord.Builder() - .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) - .setAudioFormat(format) - .setBufferSizeInBytes(minBufferSize * 2) - .build() - } else { - audioRecord = AudioRecord( - MediaRecorder.AudioSource.VOICE_COMMUNICATION, - sampleRate, - channelConfig, - audioFormat, - minBufferSize * 2 - ) - } - - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { - throw IllegalStateException("AudioRecord初始化失败") - } - - audioRecord?.startRecording() - - Log.d("TAG", "通话模式录音开始,采样率: $sampleRate Hz") - - } catch (e: Exception) { - Log.e("TAG", "音频捕获异常: ${e.message}") - // 出错时恢复音频模式 - restoreCommunicationAudioMode() - } - } - - 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 { - - } - } - - /** - * 停止麦克风捕获并释放所有相关资源 - */ - fun stopMicrophoneCapture() { - try { - 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 stopRecord(isSave: Boolean) { + Log.i(tag, "关闭录音:") + if (audioStream == null) { + return } + if (audioStream!!.recordfile == null)return + audioStream!!.stopMicrophoneCapture() + audioStream!!.recordfile!!.closeFile(isSave) + } - /** - * 释放音频资源 - */ - fun releaseAudioResources() { - try { - if (!isWriting.get()) return - - - isWriting.set(false) - isRunning.set(false) - // 停止录音 - if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { - audioRecord?.stop() - } - - // 中断并等待捕获线程结束 - writeThread?.interrupt() - writeThread?.join(300) // 最多等待300ms - - // 恢复音频模式 - this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode - - // 释放录音实例 - audioRecord?.release() - audioRecord = null - } catch (e: Exception) { - Log.e(tag, "释放音频资源失败: ${e.message}") - e.printStackTrace() - } finally { - writeQueue.clear() - writeThread = null - // 确保恢复原始音频状态 - // restoreOriginalAudioState() - } - } - } /** * 一次性识别回调接口 @@ -952,13 +681,7 @@ class AzureAsrHelper(private val context: Context) { */ fun onSessionStopped() - /** - * 返回音频 - * - * @param data 识别的音频 - */ - fun onAudio(data: ByteArray) - + /** * 识别取消时调用 * @@ -975,55 +698,7 @@ class AzureAsrHelper(private val context: Context) { fun onError(error: String) } - /** - * 配置通话音频模式以优化回声消除 - */ - private fun setupCommunicationAudioMode() { - try { - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - // 保存原始音频模式 - originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL - - // 设置通话模式 - 这是关键! - audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION - - // 启用扬声器(如果需要外放) - audioManager?.isSpeakerphoneOn = true - - // 请求音频焦点 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) - .setAudioAttributes( - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - ) - .build() - - audioManager?.requestAudioFocus(focusRequest) - } - - Log.d("AudioMode", "通话音频模式已设置: MODE_IN_COMMUNICATION") - - } catch (e: Exception) { - Log.e("AudioMode", "设置通话音频模式失败: ${e.message}") - } - } - - /** - * 恢复原始音频模式 - */ - private fun restoreCommunicationAudioMode() { - try { - audioManager?.mode = originalAudioMode - audioManager?.isSpeakerphoneOn = false - // 释放音频焦点 - Log.d("AudioMode", "音频模式已恢复") - } catch (e: Exception) { - Log.e("AudioMode", "恢复音频模式失败: ${e.message}") - } - } + + } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt new file mode 100644 index 000000000..38c374238 --- /dev/null +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt @@ -0,0 +1,1311 @@ +package com.yunqiinnovation.azure_speech + +import android.content.Context +import android.media.AudioManager +import android.util.Log +import com.microsoft.cognitiveservices.speech.* +import com.microsoft.cognitiveservices.speech.audio.* +import kotlinx.coroutines.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import kotlin.coroutines.CoroutineContext +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import org.json.JSONObject +import org.json.JSONArray +import java.net.URLEncoder +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import com.yunqiinnovation.azure_speech.tools.RecordFile +import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer +/** + * 整合的语音翻译服务 + * 集成ASR语音识别、翻译服务和TTS语音合成 + * 实现音频输入 -> 语音识别 -> 翻译 -> 语音合成 -> 音频输出的完整流程 + */ +class IntegratedSpeechTranslationService( + private val context: Context +) : CoroutineScope { + + companion object { + private const val TAG = "IntegratedSpeechService" + + // 音频配置常量 + private const val SAMPLE_RATE = 16000 + private const val CHANNELS = 1 + private const val BITS_PER_SAMPLE = 16 + private const val BUFFER_SIZE = 4096 + + // 超时配置 + private const val END_SILENCE_TIMEOUT = "300" + private const val SEGMENTATION_SILENCE_TIMEOUT = "300" + private const val INITIAL_SILENCE_TIMEOUT = "200" + } + + // 协程上下文 + private val job = SupervisorJob() + override val coroutineContext: CoroutineContext = Dispatchers.Main + job + + // Azure服务组件 + private var speechConfig: SpeechConfig? = null + private var recognizer: SpeechRecognizer? = null + private var synthesizer: SpeechSynthesizer? = null + + // 翻译服务 + private var translationService: TranslationServiceInterface? = null + + // 音频处理 + private var audioProcessor: AudioProcessor? = null + private var audioConfig: AudioConfig? = null + // 录音文件处理 + var recordfile1: RecordFile? = null + var filePath: String? = null + // 配置管理 + private var serviceConfig = ServiceConfiguration() + + // 状态管理 + private val serviceState = ServiceState() + + // 事件回调 + private var eventCallback: ServiceEventCallback? = null + + // 语音映射缓存 + private val voiceCache = ConcurrentHashMap() + + /** + * 服务配置类 + */ + data class ServiceConfiguration( + var sourceLanguage: String = "zh-CN", + var targetLanguage: String = "en-US", + var currentVoice: String = "en-US-AriaNeural", + var speechRate: String = "0%", + var speechPitch: String = "0%", + var speechVolume: String = "100%", + var enableContinuousRecognition: Boolean = true, + var enableAutoLanguageDetection: Boolean = false, + var maxRetryAttempts: Int = 3, + var translationTimeout: Long = 10000L, + // 新增:控制是否播放合成的音频 + var enableAudioPlayback: Boolean = false + ) + + /** + * 服务状态类 + */ + data class ServiceState( + val isInitialized: AtomicBoolean = AtomicBoolean(false), + val isRecognizing: AtomicBoolean = AtomicBoolean(false), + val isSynthesizing: AtomicBoolean = AtomicBoolean(false), + val isTranslating: AtomicBoolean = AtomicBoolean(false) + ) + + /** + * 服务事件回调接口 + */ + interface ServiceEventCallback { + fun onServiceInitialized() + fun onRecognizing(text: String, language: String, confidence: Float) + fun onRecognized(text: String, language: String, confidence: Float) + fun onTranslated(originalText: String, translatedText: String, targetLanguage: String) + fun onTranslationStarted(text: String) + fun onTranslationFailed(text: String, error: String) + fun onSynthesisStarted(text: String) + fun onSynthesisCompleted(text: String) + fun onSynthesisFailed(text: String, error: String) + fun onSynthesisProgress(text: String, progress: Float) + fun onRecognitionStarted() + fun onRecognitionStopped() + fun onStateChanged(component: String, isActive: Boolean) + fun onError(component: String, error: String) + // 新增:返回合成的音频数据 + fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) + } + + /** + * 翻译服务接口 + */ + interface TranslationServiceInterface { + suspend fun initialize(config: Map): Boolean + suspend fun translateText( + text: String, + sourceLanguage: String, + targetLanguage: String + ): TranslationResult + + fun dispose() + } + + /** + * 翻译结果类 + */ + data class TranslationResult( + val success: Boolean, + val translatedText: String? = null, + val error: String? = null, + val confidence: Float = 0f + ) + + /** + * 初始化整合服务 + */ + suspend fun initialize( + azureConfig: AzureConfiguration, + translationConfig: TranslationConfiguration, + serviceConfig: ServiceConfiguration? = null, + callback: ServiceEventCallback + ): Boolean = withContext(Dispatchers.IO) { + try { + this@IntegratedSpeechTranslationService.eventCallback = callback + serviceConfig?.let { this@IntegratedSpeechTranslationService.serviceConfig = it } + + Log.d(TAG, "开始初始化整合服务,azureConfig:${azureConfig},translationConfig:${translationConfig}") + + // 初始化Azure语音服务 + if (!initializeAzureServices(azureConfig)) { + callback.onError("Initialization", "Azure服务初始化失败") + return@withContext false + } + + // 初始化翻译服务 + if (!initializeTranslationService(translationConfig)) { + callback.onError("Initialization", "翻译服务初始化失败") + return@withContext false + } + + // 初始化音频处理器 + initializeAudioProcessor() + + // 设置语音识别器 + setupSpeechRecognizer() + + // 设置语音合成器 + setupSpeechSynthesizer() + + serviceState.isInitialized.set(true) + Log.d(TAG, "整合服务初始化成功") + + withContext(Dispatchers.Main) { + callback.onServiceInitialized() + } + //startContinuousTranslation() + recordfile1 = RecordFile(); + return@withContext true + } catch (e: Exception) { + Log.e(TAG, "初始化失败", e) + withContext(Dispatchers.Main) { + callback.onError( + "Initialization", + "初始化失败: ${e.message}" + ) + } + return@withContext false + } + } + + /** + * 初始化Azure服务 + */ + private suspend fun initializeAzureServices(config: AzureConfiguration): Boolean { + return try { + Log.d(TAG, "开始初始化Azure服务${config.subscriptionKey},${config.region}") + speechConfig = + SpeechConfig.fromSubscription(config.subscriptionKey, config.region).apply { + speechRecognitionLanguage = serviceConfig.sourceLanguage + setSpeechSynthesisVoiceName(getVoiceForLanguage(serviceConfig.targetLanguage)) + setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm) + + // 优化配置 + setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", END_SILENCE_TIMEOUT) + setProperty("Speech_SegmentationSilenceTimeoutMs", SEGMENTATION_SILENCE_TIMEOUT) + setProperty( + "SpeechServiceConnection_InitialSilenceTimeoutMs", + INITIAL_SILENCE_TIMEOUT + ) + setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE") + + // 启用详细结果 + setProperty("SpeechServiceResponse_RequestDetailedResultTrueFalse", "true") + + // 自动语言检测 + if (serviceConfig.enableAutoLanguageDetection) { + setProperty("SpeechServiceConnection_LanguageIdMode", "Continuous") + } + } + + Log.d(TAG, "Azure语音服务配置完成") + true + } catch (e: Exception) { + Log.e(TAG, "Azure服务初始化失败", e) + false + } + } + + /** + * 初始化翻译服务 + */ + private suspend fun initializeTranslationService(config: TranslationConfiguration): Boolean { + return try { + translationService = VolcanoTranslationServiceImpl().apply { + initialize(config.toConfigMap()) + } + Log.d(TAG, "翻译服务初始化完成") + true + } catch (e: Exception) { + Log.e(TAG, "翻译服务初始化失败", e) + false + } + } + + /** + * 初始化音频处理器 + */ + private fun initializeAudioProcessor() { + audioProcessor = AudioProcessor().apply { + initialize() + } + audioConfig = AudioConfig.fromStreamInput(audioProcessor?.getAudioStream()) + Log.d(TAG, "音频处理器初始化完成") + } + + /** + * 设置语音识别器 + */ + private fun setupSpeechRecognizer() { + recognizer?.close() + recognizer = SpeechRecognizer(speechConfig, audioConfig).apply { + + // 识别中事件 + recognizing.addEventListener { _, event -> + if (event.result.text.isNotEmpty()) { + val confidence = extractConfidence(event.result) + Log.d(TAG, "识别中事件: ${event.result.text},serviceConfig.sourceLanguage:${serviceConfig.sourceLanguage}") + eventCallback?.onRecognizing( + event.result.text, + serviceConfig.sourceLanguage, + confidence + ) + } + } + + // 识别完成事件 + recognized.addEventListener { _, event -> + when (event.result.reason) { + ResultReason.RecognizedSpeech -> { + if (event.result.text.isNotEmpty()) { + val confidence = extractConfidence(event.result) + eventCallback?.onRecognized( + event.result.text, + serviceConfig.sourceLanguage, + confidence + ) + Log.d(TAG, "识别完成事件: ${event.result.text},serviceConfig.sourceLanguage:${serviceConfig.sourceLanguage}") + // 触发翻译流程 + launch { + processTranslationAndSynthesis(event.result.text) + } + } + } + + ResultReason.NoMatch -> { + Log.d(TAG, "未识别到语音") + } + + else -> { + Log.w(TAG, "识别结果: ${event.result.reason}") + } + } + } + + // 会话开始事件 + sessionStarted.addEventListener { _, _ -> + Log.d(TAG, "会话开始事件") + serviceState.isRecognizing.set(true) + eventCallback?.onRecognitionStarted() + eventCallback?.onStateChanged("Recognition", true) + } + + // 会话停止事件 + sessionStopped.addEventListener { _, _ -> + Log.d(TAG, "会话停止事件") + serviceState.isRecognizing.set(false) + eventCallback?.onRecognitionStopped() + eventCallback?.onStateChanged("Recognition", false) + } + + // 取消事件 + canceled.addEventListener { _, event -> + Log.d(TAG, "取消事件") + serviceState.isRecognizing.set(false) + val errorDetails = event.errorDetails ?: "未知错误" + eventCallback?.onError("Recognition", "识别被取消: $errorDetails") + eventCallback?.onStateChanged("Recognition", false) + } + } + + Log.d(TAG, "语音识别器设置完成") + } + + /** + * 设置语音合成器 + */ + private fun setupSpeechSynthesizer() { + synthesizer?.close() + + // 根据配置决定音频输出方式 + val audioConfig = if (serviceConfig.enableAudioPlayback) { + AudioConfig.fromDefaultSpeakerOutput() + } else { + // 不输出到扬声器,只生成音频数据 + null + } + + synthesizer = SpeechSynthesizer(speechConfig, audioConfig).apply { + + // 合成开始事件 + SynthesisStarted.addEventListener { _, _ -> + Log.d(TAG, "合成开始事件") + serviceState.isSynthesizing.set(true) + eventCallback?.onStateChanged("Synthesis", true) + } + + // 合成进行中事件 + Synthesizing.addEventListener { _, event -> + Log.d(TAG, "合成进行中事件") + // 计算进度(简化版) + val progress = 0.5f // 实际应用中可以根据音频数据计算 + eventCallback?.onSynthesisProgress("", progress) + } + + // 合成完成事件 + SynthesisCompleted.addEventListener { _, event -> + Log.d(TAG, "合成完成事件") + // eventCallback?.onSynthesisCompleted("") + eventCallback?.onStateChanged("Synthesis", false) + } + + // 合成取消事件 + SynthesisCanceled.addEventListener { _, event -> + Log.d(TAG, "合成取消事件") + serviceState.isSynthesizing.set(false) + val reason = event.result.reason + eventCallback?.onError("Synthesis", "语音合成取消: $reason") + eventCallback?.onStateChanged("Synthesis", false) + } + } + + Log.d(TAG, "语音合成器设置完成,播放模式: ${serviceConfig.enableAudioPlayback}") + } + + /** + * 开始连续语音翻译 + */ + fun startContinuousTranslation(): Boolean { + if (!serviceState.isInitialized.get()) { + eventCallback?.onError("Service", "服务未初始化") + return false + } + + if (serviceState.isRecognizing.get()) { + Log.w(TAG, "语音识别已在进行中") + return true + } + + return try { + + audioProcessor?.startRecording() + + if (serviceConfig.enableContinuousRecognition) { + Log.d(TAG, "startContinuousTranslation") + recognizer?.startContinuousRecognitionAsync() + } + + Log.d(TAG, "开始连续语音翻译") + true + } catch (e: Exception) { + Log.e(TAG, "启动连续翻译失败", e) + eventCallback?.onError("Service", "启动失败: ${e.message}") + false + } + } + fun enableRecord(filePath: String) { + Log.i(TAG, "开启录音:") + + recordfile1!!.closeFile(true) + + + recordfile1!!.creatingFiles(filePath) + + } + + /** + * 停止连续语音翻译 + */ + fun stopContinuousTranslation() { + try { + recognizer?.stopContinuousRecognitionAsync() + audioProcessor?.stopRecording() + recordfile1!!.closeFile(true) + // 等待当前处理完成 + while (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) { + Thread.sleep(100) + } + + Log.d(TAG, "停止连续语音翻译") + } catch (e: Exception) { + Log.e(TAG, "停止翻译失败", e) + eventCallback?.onError("Service", "停止失败: ${e.message}") + } + } + + /** + * 处理翻译和合成流程 + */ + private suspend fun processTranslationAndSynthesis(text: String) { + if (serviceState.isTranslating.get()) { + Log.w(TAG, "翻译正在进行中,跳过当前请求") + return + } + + serviceState.isTranslating.set(true) + eventCallback?.onTranslationStarted(text) + eventCallback?.onStateChanged("Translation", true) + + try { + val translationResult = withTimeout(serviceConfig.translationTimeout) { + translationService?.translateText( + text = text, + sourceLanguage = serviceConfig.sourceLanguage, + targetLanguage = serviceConfig.targetLanguage + ) + } + + serviceState.isTranslating.set(false) + eventCallback?.onStateChanged("Translation", false) + + when { + translationResult?.success == true && !translationResult.translatedText.isNullOrEmpty() -> { + eventCallback?.onTranslated( + text, + translationResult.translatedText, + serviceConfig.targetLanguage + ) + synthesizeText(translationResult.translatedText) + } + + translationResult?.error != null -> { + eventCallback?.onTranslationFailed(text, translationResult.error) + } + + else -> { + eventCallback?.onTranslationFailed(text, "翻译服务返回空结果") + } + } + + } catch (e: TimeoutCancellationException) { + serviceState.isTranslating.set(false) + eventCallback?.onStateChanged("Translation", false) + eventCallback?.onTranslationFailed(text, "翻译超时") + } catch (e: Exception) { + serviceState.isTranslating.set(false) + eventCallback?.onStateChanged("Translation", false) + eventCallback?.onTranslationFailed(text, "翻译异常: ${e.message}") + } + } + + /** + * 语音合成 + * @param text 要合成的文本 + * @return 返回合成的音频数据,如果合成失败则返回null + */ + private suspend fun synthesizeText(text: String): ByteArray? { + if (serviceState.isSynthesizing.get()) { + Log.w(TAG, "语音合成正在进行中") + return null + } + + try { + Log.d(TAG, "开始语音合成: $text") + serviceState.isSynthesizing.set(true) + eventCallback?.onSynthesisStarted(text) + + val ssml = generateOptimizedSsml(text) + + // 始终使用异步方法获取音频数据 + val result = synthesizer?.SpeakSsmlAsync(ssml)?.get() + + if (result?.reason == ResultReason.SynthesizingAudioCompleted) { + val playbackStatus = if (serviceConfig.enableAudioPlayback) "音频已播放" else "音频已合成但未播放" + Log.d(TAG, "语音合成成功,$playbackStatus") + + // 获取音频数据 + val audioData = result.audioData + if (audioData != null && audioData.isNotEmpty()) { + // 通过回调返回音频数据 + eventCallback?.onSynthesisAudioGenerated(text, audioData) + Log.d(TAG, "音频数据大小: ${audioData.size} 字节") + } + + return audioData + } else { + Log.e(TAG, "语音合成失败: ${result?.reason}") + eventCallback?.onSynthesisFailed(text, "合成失败: ${result?.reason}") + return null + } + + } catch (e: Exception) { + Log.e(TAG, "语音合成失败", e) + eventCallback?.onSynthesisFailed(text, "合成失败: ${e.message}") + return null + } finally { + serviceState.isSynthesizing.set(false) + } + } + + /** + * 生成优化的SSML + */ + private fun generateOptimizedSsml(text: String): String { + val cleanText = cleanTextForSynthesis(text) + val escapedText = escapeXmlText(cleanText) + + return """ + + + + $escapedText + + + + """.trimIndent() + } + + /** + * 清理文本用于合成 + */ + private fun cleanTextForSynthesis(text: String): String { + return text + .replace(Regex("https?://[^\\s]+"), "") // 移除URL + .replace(Regex("[\\p{So}\\p{Sk}]"), "") // 移除表情符号 + .replace(Regex("\\s+"), " ") // 合并多个空格 + .trim() + } + + /** + * XML文本转义 + */ + private fun escapeXmlText(text: String): String { + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + } + + /** + * 根据语言获取对应的语音 + */ + private fun getVoiceForLanguage(language: String): String { + return voiceCache.getOrPut(language) { + when (language) { + "zh-CN" -> "zh-CN-XiaoxiaoNeural" + "zh-TW" -> "zh-TW-HsiaoChenNeural" + "en-US" -> "en-US-AriaNeural" + "en-GB" -> "en-GB-SoniaNeural" + "ja-JP" -> "ja-JP-NanamiNeural" + "ko-KR" -> "ko-KR-SunHiNeural" + "fr-FR" -> "fr-FR-DeniseNeural" + "de-DE" -> "de-DE-KatjaNeural" + "es-ES" -> "es-ES-ElviraNeural" + "ru-RU" -> "ru-RU-SvetlanaNeural" + "ar-SA" -> "ar-SA-ZariyahNeural" + "pt-BR" -> "pt-BR-FranciscaNeural" + "it-IT" -> "it-IT-ElsaNeural" + else -> "en-US-AriaNeural" + }.also { + serviceConfig.currentVoice = it + } + } + } + + /** + * 提取识别置信度 + */ + private fun extractConfidence(result: SpeechRecognitionResult): Float { + return try { + // 从结果中提取置信度信息 + // 这里需要根据实际的Azure SDK API API来实现 + 0.8f // 默认值 + } catch (e: Exception) { + 0.5f + } + } + + /** + * 更新服务配置 + */ + fun updateConfiguration(newConfig: ServiceConfiguration) { + val needsRecognizerUpdate = newConfig.sourceLanguage != serviceConfig.sourceLanguage || + newConfig.enableAutoLanguageDetection != serviceConfig.enableAutoLanguageDetection + + val needsSynthesizerUpdate = newConfig.targetLanguage != serviceConfig.targetLanguage || + newConfig.speechRate != serviceConfig.speechRate || + newConfig.speechPitch != serviceConfig.speechPitch || + newConfig.speechVolume != serviceConfig.speechVolume + + serviceConfig = newConfig + + if (needsRecognizerUpdate) { + speechConfig?.speechRecognitionLanguage = serviceConfig.sourceLanguage + setupSpeechRecognizer() + } + + if (needsSynthesizerUpdate) { + speechConfig?.setSpeechSynthesisVoiceName(getVoiceForLanguage(serviceConfig.targetLanguage)) + setupSpeechSynthesizer() + } + + Log.d(TAG, "服务配置已更新") + } + + /** + * 设置音频输出设备 + */ +fun setAudioOutputDevice(device: com.deep_voice.speech.tts.AudioOutputDevice) { + try { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + audioManager?.let { manager -> + when (device) { + com.deep_voice.speech.tts.AudioOutputDevice.SPEAKER -> { + manager.mode = AudioManager.MODE_IN_COMMUNICATION + manager.isSpeakerphoneOn = true + } + + com.deep_voice.speech.tts.AudioOutputDevice.HEADPHONES -> { + manager.mode = AudioManager.MODE_NORMAL + manager.isSpeakerphoneOn = false + } + + com.deep_voice.speech.tts.AudioOutputDevice.DEFAULT -> { + manager.mode = AudioManager.MODE_NORMAL + manager.isSpeakerphoneOn = false + } + } + } + Log.d(TAG, "音频输出设备设置为: $device") + } catch (e: Exception) { + Log.e(TAG, "设置音频输出设备失败", e) + eventCallback?.onError("Audio", "设置音频输出设备失败: ${e.message}") + } +} + + /** + * 切换语言对 + */ + fun switchLanguages() { + val newConfig = serviceConfig.copy( + sourceLanguage = serviceConfig.targetLanguage, + targetLanguage = serviceConfig.sourceLanguage + ) + updateConfiguration(newConfig) + Log.d(TAG, "语言对已切换: ${newConfig.sourceLanguage} <-> ${newConfig.targetLanguage}") + } + + /** + * 设置是否启用音频播放 + * @param enabled true表示启用音频播放,false表示只合成不播放 + */ + fun setAudioPlaybackEnabled(enabled: Boolean) { + serviceConfig.enableAudioPlayback = enabled + Log.d(TAG, "音频播放设置更新: $enabled") + + // 重新设置语音合成器以应用新配置 + if (serviceState.isInitialized.get()) { + setupSpeechSynthesizer() + } + + eventCallback?.onStateChanged("AudioPlayback", enabled) + } + + /** + * 获取当前音频播放状态 + * @return true表示启用音频播放,false表示禁用 + */ + fun isAudioPlaybackEnabled(): Boolean { + return serviceConfig.enableAudioPlayback + } + + /** + * 单独的语音合成方法,只合成不播放,返回音频数据 + * @param text 要合成的文本 + * @return 合成的音频数据,失败时返回null + */ + suspend fun synthesizeTextToAudio(text: String): ByteArray? = withContext(Dispatchers.IO) { + try { + Log.d(TAG, "开始纯音频合成: $text") + + val ssml = generateOptimizedSsml(text) + + // 创建一个临时的合成器,输出到内存而不是扬声器 + val audioConfig = AudioConfig.fromStreamOutput(AudioOutputStream.createPullStream()) + val tempSynthesizer = SpeechSynthesizer(speechConfig, audioConfig) + + try { + val result = tempSynthesizer.SpeakSsmlAsync(ssml).get() + + if (result?.reason == ResultReason.SynthesizingAudioCompleted) { + val audioData = result.audioData + if (audioData != null && audioData.isNotEmpty()) { + Log.d(TAG, "纯音频合成成功,大小: ${audioData.size} 字节") + return@withContext audioData + } + } else { + Log.e(TAG, "纯音频合成失败: ${result?.reason}") + } + } finally { + tempSynthesizer.close() + } + + return@withContext null + } catch (e: Exception) { + Log.e(TAG, "纯音频合成异常", e) + return@withContext null + } + } + + /** + * 推送外部音频数据 + */ + fun pushAudioData(audioData: ByteArray) { + audioProcessor?.pushAudioData(audioData) + } + + /** + * 获取服务状态 + */ + fun getServiceStatus(): Map { + return mapOf( + "initialized" to serviceState.isInitialized.get(), + "recognizing" to serviceState.isRecognizing.get(), + "translating" to serviceState.isTranslating.get(), + "synthesizing" to serviceState.isSynthesizing.get() + ) + } + + /** + * 释放资源 + */ + fun dispose() { + try { + // 停止所有活动 + launch { + stopContinuousTranslation() + } + + // 释放Azure资源 + recognizer?.close() + synthesizer?.close() + speechConfig?.close() + + // 释放音频处理器 + audioProcessor?.dispose() + + // 释放翻译服务 + translationService?.dispose() + + // 取消协程 + job.cancel() + + // 清理缓存 + voiceCache.clear() + + serviceState.isInitialized.set(false) + + Log.d(TAG, "服务资源已释放") + } catch (e: Exception) { + Log.e(TAG, "释放资源失败", e) + } + } + + /** + * 音频处理器类 + */ + inner class AudioProcessor { + private var pushAudioStream: PushAudioInputStream? = null + private val audioQueue = LinkedBlockingQueue() + private val isRunning = AtomicBoolean(false) + private var processingThread: Thread? = null + + fun initialize() { + val format = AudioStreamFormat.getWaveFormatPCM( + SAMPLE_RATE.toLong(), + BITS_PER_SAMPLE.toShort(), + CHANNELS.toShort() + ) + pushAudioStream = AudioInputStream.createPushStream(format) + isRunning.set(true) + startProcessingThread() + } + + private fun startProcessingThread() { + processingThread = Thread { + while (isRunning.get()) { + try { + val audioData = + audioQueue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS) + audioData?.let { + //Log.d(TAG, "音频处理: ${it.size}") + pushAudioStream?.write(it) + recordfile1?.saveAudioDataToWav(it) + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + break + } catch (e: Exception) { + Log.e(TAG, "音频处理异常", e) + } + } + }.apply { + name = "AudioProcessingThread" + start() + } + } + + fun getAudioStream(): PushAudioInputStream? = pushAudioStream + + fun startRecording() { + Log.d(TAG, "开始音频录制") + } + + fun stopRecording() { + Log.d(TAG, "停止音频录制") + } + + fun pushAudioData(audioData: ByteArray) { + if (isRunning.get()) { + audioQueue.offer(audioData) + } + } + + fun dispose() { + isRunning.set(false) + processingThread?.interrupt() + pushAudioStream?.close() + audioQueue.clear() + } + } +} + +/** + * Azure配置类 + */ +data class AzureConfiguration( + val subscriptionKey: String, + val region: String +) + +/** + * 翻译配置类 + */ +data class TranslationConfiguration( + val accessKey: String, + val secretKey: String, + val region: String = "cn-north-1" +) { + fun toConfigMap(): Map { + return mapOf( + "accessKey" to accessKey, + "secretKey" to secretKey, + "region" to region + ) + } +} + + + +/** + * 火山翻译服务实现 + */ +class VolcanoTranslationServiceImpl : + IntegratedSpeechTranslationService.TranslationServiceInterface { + companion object { + private const val TAG = "VolcanoTranslationService" + private const val BASE_URL = "https://translate.volcengineapi.com" + private const val ENDPOINT = "/" + private const val SERVICE = "translate" + private const val VERSION = "2020-06-01" + private const val ACTION = "TranslateText" + private const val ALGORITHM = "HMAC-SHA256" + } + + private var isInitialized = false + private var accessKey: String = "" + private var secretKey: String = "" + private var region: String = "cn-north-1" + private var maxRetryAttempts: Int = 3 + private var timeout: Long = 10000L + + // HTTP客户端 + private val httpClient = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + // 语言代码映射 + private val languageCodeMap = mapOf( + "zh-CN" to "zh", + "zh-TW" to "zh-Hant", + "en-US" to "en", + "en-GB" to "en", + "ja-JP" to "ja", + "ko-KR" to "ko", + "fr-FR" to "fr", + "de-DE" to "de", + "es-ES" to "es", + "ru-RU" to "ru", + "ar-SA" to "ar", + "pt-BR" to "pt", + "it-IT" to "it", + "th-TH" to "th", + "vi-VN" to "vi", + "hi-IN" to "hi" + ) + + override suspend fun initialize(config: Map): Boolean { + return try { + accessKey = config["accessKey"] ?: "" + secretKey = config["secretKey"] ?: "" + region = config["region"] ?: "cn-north-1" + maxRetryAttempts = config["maxRetryAttempts"]?.toIntOrNull() ?: 3 + timeout = config["timeout"]?.toLongOrNull() ?: 10000L + + if (accessKey.isEmpty() || secretKey.isEmpty()) { + Log.e(TAG, "缺少必要的API密钥") + return false + } + + isInitialized = true + Log.d(TAG, "火山翻译服务初始化成功") + true + } catch (e: Exception) { + Log.e(TAG, "初始化失败", e) + false + } + } + + override suspend fun translateText( + text: String, + sourceLanguage: String, + targetLanguage: String + ): IntegratedSpeechTranslationService.TranslationResult { + if (!isInitialized) { + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "翻译服务未初始化" + ) + } + + return withContext(Dispatchers.IO) { + performTranslationWithRetry(text, sourceLanguage, targetLanguage) + } + } + + private suspend fun performTranslationWithRetry( + text: String, + sourceLanguage: String, + targetLanguage: String + ): IntegratedSpeechTranslationService.TranslationResult { + var lastException: Exception? = null + + repeat(maxRetryAttempts) { attempt -> + try { + val result = performTranslation(text, sourceLanguage, targetLanguage) + if (result.success) { + return result + } + lastException = Exception(result.error) + } catch (e: Exception) { + lastException = e + Log.w(TAG, "翻译请求失败,尝试 ${attempt + 1}/$maxRetryAttempts", e) + + // 重试延迟 + if (attempt < maxRetryAttempts - 1) { + delay(500L * (attempt + 1)) + } + } + } + + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "翻译失败: ${lastException?.message ?: "未知错误"}" + ) + } + + private suspend fun performTranslation( + text: String, + sourceLanguage: String, + targetLanguage: String + ): IntegratedSpeechTranslationService.TranslationResult { + try { + // 获取语言代码 + val sourceCode = getLanguageCode(sourceLanguage) + val targetCode = getLanguageCode(targetLanguage) + + if (sourceCode.isEmpty() || targetCode.isEmpty()) { + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "不支持的语言代码: $sourceLanguage -> $targetLanguage" + ) + } + + // 构建请求体 + val requestBody = JSONObject().apply { + put("SourceLanguage", sourceCode) + put("TargetLanguage", targetCode) + put("TextList", JSONArray().put(text)) + } + + // 查询参数 + val queryParams = mapOf( + "Action" to ACTION, + "Version" to VERSION, + "Region" to region, + "Service" to SERVICE + ) + + // 生成签名 + val headers = generateSignature("POST", requestBody, queryParams) + + // 构建URL + val urlBuilder = BASE_URL.toHttpUrl().newBuilder() + queryParams.forEach { (key, value) -> + urlBuilder.addQueryParameter(key, value) + } + val url = urlBuilder.build() + + // 构建请求 + val requestBodyObj = RequestBody.create( + "application/json; charset=utf-8".toMediaType(), + requestBody.toString() + ) + + val request = Request.Builder() + .url(url) + .post(requestBodyObj) + .apply { + headers.forEach { (key, value) -> + addHeader(key, value) + } + } + .build() + + // 发送请求 + val response = httpClient.newCall(request).execute() + + if (response.isSuccessful) { + val responseBody = response.body?.string() + if (responseBody != null) { + val jsonResponse = JSONObject(responseBody) + val translation = extractTranslation(jsonResponse) + + if (translation != null) { + return IntegratedSpeechTranslationService.TranslationResult( + success = true, + translatedText = translation, + confidence = 0.9f + ) + } else { + val error = extractError(jsonResponse) + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = error ?: "翻译结果为空" + ) + } + } + } + + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "HTTP错误: ${response.code}" + ) + + } catch (e: Exception) { + Log.e(TAG, "翻译请求异常", e) + return IntegratedSpeechTranslationService.TranslationResult( + success = false, + error = "请求异常: ${e.message}" + ) + } + } + + private fun getLanguageCode(languageCode: String): String { + return languageCodeMap[languageCode] ?: "" + } + + private fun extractTranslation(jsonResponse: JSONObject): String? { + try { + // 1. 标准响应结构 + if (jsonResponse.has("TranslationList")) { + val translationList = jsonResponse.getJSONArray("TranslationList") + if (translationList.length() > 0) { + val translationItem = translationList.getJSONObject(0) + if (translationItem.has("Translation")) { + return translationItem.getString("Translation") + } + } + } + + // 2. 其他可能的响应结构 + if (jsonResponse.has("Translation")) { + return jsonResponse.getString("Translation") + } + + if (jsonResponse.has("Result")) { + val result = jsonResponse.getJSONObject("Result") + if (result.has("Translation")) { + return result.getString("Translation") + } + } + + if (jsonResponse.has("Data")) { + val data = jsonResponse.getJSONObject("Data") + if (data.has("Translation")) { + return data.getString("Translation") + } + if (data.has("TranslationList")) { + val translationList = data.getJSONArray("TranslationList") + if (translationList.length() > 0) { + val translationItem = translationList.getJSONObject(0) + if (translationItem.has("Translation")) { + return translationItem.getString("Translation") + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "提取翻译结果失败", e) + } + + return null + } + + private fun extractError(jsonResponse: JSONObject): String? { + try { + if (jsonResponse.has("ResponseMetadata")) { + val metadata = jsonResponse.getJSONObject("ResponseMetadata") + if (metadata.has("Error")) { + val error = metadata.getJSONObject("Error") + return error.optString("Message", error.optString("Code", "未知错误")) + } + } + + if (jsonResponse.has("Error")) { + val error = jsonResponse.getJSONObject("Error") + return error.optString("Message", error.optString("Code", "未知错误")) + } + } catch (e: Exception) { + Log.e(TAG, "提取错误信息失败", e) + } + + return null + } + + private fun generateSignature( + method: String, + requestBody: JSONObject, + queryParams: Map + ): Map { + try { + // 1. 准备时间相关参数 + val now = Date() + val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + val timestampFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + + val date = dateFormat.format(now) + val timestamp = timestampFormat.format(now) + + // 2. 构建规范查询字符串 + val sortedParams = queryParams.toSortedMap() + val canonicalQueryString = sortedParams.map { (key, value) -> + "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" + }.joinToString("&") + + // 3. 创建规范请求 + val contentType = "application/json" + val payloadHash = sha256(requestBody.toString()) + val host = "translate.volcengineapi.com" + + val canonicalHeaders = "host:$host\nx-date:$timestamp\n" + val signedHeaders = "host;x-date" + + val canonicalRequest = + "$method\n$ENDPOINT\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash" + + // 4. 创建待签字符串 + val credentialScope = "$date/$region/$SERVICE/request" + val stringToSign = + "$ALGORITHM\n$timestamp\n$credentialScope\n${sha256(canonicalRequest)}" + + // 5. 计算签名 + val kSecret = secretKey.toByteArray(Charsets.UTF_8) + val kDate = hmacSha256(kSecret, date) + val kRegion = hmacSha256(kDate, region) + val kService = hmacSha256(kRegion, SERVICE) + val kSigning = hmacSha256(kService, "request") + val signature = + hmacSha256(kSigning, stringToSign).joinToString("") { "%02x".format(it) } + + // 6. 构建授权头 + val authorization = + "$ALGORITHM Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature" + + return mapOf( + "Content-Type" to contentType, + "X-Date" to timestamp, + "Authorization" to authorization, + "Host" to host + ) + + } catch (e: Exception) { + Log.e(TAG, "生成签名失败", e) + throw e + } + } + + private fun sha256(input: String): String { + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(input.toByteArray(Charsets.UTF_8)) + return hash.joinToString("") { "%02x".format(it) } + } + + private fun hmacSha256(key: ByteArray, data: String): ByteArray { + val mac = Mac.getInstance("HmacSHA256") + val secretKeySpec = SecretKeySpec(key, "HmacSHA256") + mac.init(secretKeySpec) + return mac.doFinal(data.toByteArray(Charsets.UTF_8)) + } + + override fun dispose() { + isInitialized = false + // 清理资源 + try { + httpClient.dispatcher.executorService.shutdown() + httpClient.connectionPool.evictAll() + } catch (e: Exception) { + Log.e(TAG, "清理资源失败", e) + } + } +} \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index b415262e8..cad5b8251 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 @@ -5,6 +5,8 @@ import android.os.Handler import android.os.Looper import androidx.annotation.NonNull import com.yunqiinnovation.azure_speech.utils.FileLogger +import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver +import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -16,6 +18,7 @@ import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType import com.yunqiinnovation.ble_service.BleService import com.deep_voice.speech.tts.AudioOutputDevice +import kotlinx.coroutines.* /** AzureSpeechPlugin */ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { @@ -35,6 +38,18 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { private var ttsEventSink: EventChannel.EventSink? = null private lateinit var azureTtsHelper: AzureTtsHelper + // AST相关 + private lateinit var astChannel: MethodChannel + private lateinit var astEventChannel: EventChannel + private var astEventSink: EventChannel.EventSink? = null + private lateinit var azureAstHelper: IntegratedSpeechTranslationService + + // 音频数据相关 + private lateinit var audioDataChannel: MethodChannel + private lateinit var audioDataEventChannel: EventChannel + private var audioDataEventSink: EventChannel.EventSink? = null + + // 是否已添加TTS事件监听器 private var isTtsListenerAdded = false @@ -74,6 +89,36 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } + // AST 事件发送方法 + private fun sendAstEvent(event: Map) { + if (astEventSink == null) { + FileLogger.w(tag, "无法发送AST事件:事件通道未准备好") + return + } + + mainHandler.post { + try { + astEventSink?.success(event) + } catch (e: Exception) { + FileLogger.e(tag, "发送AST事件失败: ${e.message}") + } + } + } + // 音频数据 事件发送方法 + private fun sendAudioDataEvent(event: Map) { + if (audioDataEventSink == null) { + FileLogger.w(tag, "无法发送音频数据事件:事件通道未准备好") + return + } + + mainHandler.post { + try { + audioDataEventSink?.success(event) + } catch (e: Exception) { + FileLogger.e(tag, "发送音频数据事件失败: ${e.message}") + } + } + } override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext @@ -84,7 +129,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { // 初始化TTS通道 ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts") ttsChannel.setMethodCallHandler(TtsMethodHandler()) - + // 初始化AST通道 + astChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast") + astChannel.setMethodCallHandler(AsTMethodHandler()) // 初始化ASR事件通道 asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events") @@ -111,11 +158,35 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { ttsEventSink = null } }) + // 初始化AST事件通道 + astEventChannel = + EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast_events") + astEventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + astEventSink = events + //setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器 + } + + override fun onCancel(arguments: Any?) { + astEventSink = null + } + }) + // 初始化音频数据事件通道 + audioDataEventChannel = + EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/audio_data_events") + audioDataEventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + audioDataEventSink = events + } + override fun onCancel(arguments: Any?) { + audioDataEventSink = null + } + }) // 初始化Azure语音服务 azureTtsHelper = AzureTtsHelper(context) azureAsrHelper = AzureAsrHelper(context) - + azureAstHelper = IntegratedSpeechTranslationService(context) // 2. 初始化BleService并注册回调 if (BleService.initialize(context)) { @@ -171,14 +242,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { sendAsrEvent(mapOf("type" to "sessionStopped")) } - override fun onAudio(data: ByteArray) { - sendAsrEvent( - mapOf( - "type" to "onAudio", - "data" to data - ) - ) - } override fun onCanceled(reason: String, errorDetails: String) { @@ -334,12 +397,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { "disableBluetoothAudio" -> { - azureAsrHelper.disableBluetoothAudio(); + azureAsrHelper.audioStream?.disableBluetoothAudio(); return } "restoreOriginalAudioState" -> { - azureAsrHelper.restoreOriginalAudioState(); + azureAsrHelper.audioStream?.restoreOriginalAudioState(); return } @@ -435,10 +498,30 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { "enableRecord" -> { val filePath = call.argument("filePath") ?: "" + // 是否接受音频数据 + val acceptAudioData = call.argument("acceptAudioData") ?: false try { - FileLogger.d(tag, "音频文件名称为: ${filePath}") // - - azureAsrHelper.enableRecord(filePath) + FileLogger.d(tag, "音频文件名称为: ${filePath}") + + // 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调 + azureAsrHelper.enableRecord(filePath, if(acceptAudioData) { + // 创建音频数据回调,将音频数据发送到 Flutter 层 + object : SimpleAudioReceiver.AudioDataCallback { + override fun onAudio(audioData: ByteArray) { + // 构建音频数据事件映射 + val audioEvent = mapOf( + "type" to "audioData", + "data" to audioData, + "timestamp" to System.currentTimeMillis(), + "size" to audioData.size + ) + // 发送音频数据事件到 Flutter 层 + sendAudioDataEvent(audioEvent) + } + } + } else { + null + }) result.success(true) } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) @@ -449,6 +532,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { azureAsrHelper.pauseRecord() result.success(true) } + "resumeRecord" -> { + azureAsrHelper.resumeRecord() + result.success(true) + } "stopRecord" -> { val isSave = call.argument("isSave") ?: false @@ -460,6 +547,17 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } + "setAudioConfig" -> { + val sampleRate = call.argument("sampleRate") ?: 16000 + val channels = call.argument("channels") ?: 1 + try { + azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels) + result.success(true) + } catch (e: Exception) { + result.error("SET_AUDIO_CONFIG_ERROR", e.message, null) + } + } + else -> { result.notImplemented() } @@ -495,6 +593,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val outputStream = customPlayer.getAudioOutputStream() if (outputStream != null) { azureTtsHelper.setCustomAudioOutputStream(outputStream) + } else { + FileLogger.w(tag, "无法获取自定义音频输出流") } } @@ -573,6 +673,326 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } + // ASt方法处理器 + inner class AsTMethodHandler : MethodCallHandler { + private suspend fun streamAudioFile(file: java.io.File) { + try { + val inputStream = file.inputStream() + val buffer = ByteArray(1024) // 每次读取1KB + + // WAV文件参数(假设16kHz, 16bit, 单声道) + val sampleRate = 16000 // 采样率 + val bytesPerSample = 2 // 16bit = 2字节 + val channels = 1 // 单声道 + + // 计算每秒需要的字节数 + val bytesPerSecond = sampleRate * bytesPerSample * channels + + // 计算每个缓冲区对应的播放时间(毫秒) + val bufferDurationMs = (buffer.size * 1000L) / bytesPerSecond + + FileLogger.d("TAG", "开始流式读取音频文件,缓冲区大小: ${buffer.size}, 播放间隔: ${bufferDurationMs}ms") + + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + // 只发送实际读取的字节数 + val audioChunk = if (bytesRead < buffer.size) { + buffer.copyOf(bytesRead) + } else { + buffer + } + + // 推送音频数据块 + withContext(Dispatchers.Main) { + azureAstHelper.pushAudioData(audioChunk) + FileLogger.d("TAG", "推送音频数据块: ${audioChunk.size} 字节") + } + } + + inputStream.close() + FileLogger.d("TAG", "音频文件流式读取完成") + + } catch (e: Exception) { + FileLogger.e("TAG", "流式读取音频文件失败: ${e.message}") + } +} + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "enableRecord" -> { + val filePath = call.argument("filePath") ?: "" + try { + FileLogger.d(tag, "音频文件名称为: ${filePath}") // + + azureAstHelper.enableRecord(filePath) + result.success(true) + } catch (e: Exception) { + result.error("ENABLERECORD_ERROR", e.message, null) + } + } + "startContinuousTranslation" -> { + try { + FileLogger.d(tag, "开启翻译") + azureAstHelper.startContinuousTranslation() + result.success(true) + } catch (e: Exception) { + result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null) + } + } + "stopContinuousTranslation" -> { + try { + FileLogger.d(tag, "停止翻译") + azureAstHelper.stopContinuousTranslation() + result.success(true) + } catch (e: Exception) { + result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null) + } + } + "dispose" -> { + try { + FileLogger.d(tag, "释放AST资源") + azureAstHelper.dispose() + result.success(true) + } catch (e: Exception) { + result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null) + } + } + + "recognizeCallback" -> { + + + FileLogger.d(tag, "recognizeCallback:") // + result.success(true) + } + + "path" -> { + val filePath = call.argument("filePath") ?: "" + try { + + // 读取这个wav音频文件,取里面的音频数据进行播放 + val file = java.io.File(filePath) + if (file.exists()) { + // 启动协程来按播放速度读取音频文件 + CoroutineScope(Dispatchers.IO).launch { + streamAudioFile(file) + } + result.success(true) + } else { + result.error("FILE_NOT_FOUND", "音频文件不存在: $filePath", null) + } + + } catch (e: Exception) { + result.error("READ_FILE_ERROR", "读取音频文件失败: ${e.message}", null) + } + } + + "initialize" -> { + val subscriptionKey = call.argument("subscriptionKey") ?: "" + val region = call.argument("region") ?: "" + val supportedLanguages = + call.argument>("supportedLanguages") ?: listOf("zh-CN") + val useExternalAudio = call.argument("useExternalAudio") ?: false + + // 获取翻译服务配置参数(需要从Flutter端传递) + val translationAccessKey = call.argument("translationAccessKey") ?: "" + val translationSecretKey = call.argument("translationSecretKey") ?: "" + val translationRegion = + call.argument("translationRegion") ?: "cn-north-1" + + FileLogger.d(tag, "初始化AST服务") + + // 创建Azure配置 + val azureConfig = AzureConfiguration( + subscriptionKey = subscriptionKey, + region = region + ) + + // 创建翻译配置 + val translationConfig = + TranslationConfiguration( + accessKey = translationAccessKey, + secretKey = translationSecretKey, + region = translationRegion + ) + + // 创建服务配置 + val serviceConfig = IntegratedSpeechTranslationService.ServiceConfiguration( + sourceLanguage = if (supportedLanguages.isNotEmpty()) supportedLanguages[0] else "zh-CN", + targetLanguage = if (supportedLanguages.size > 1) supportedLanguages[1] else "en-US" + ) + + // 创建事件回调 + val callback = object : IntegratedSpeechTranslationService.ServiceEventCallback { + override fun onServiceInitialized() { + // sendAstEvent( + // mapOf( + // "type" to "serviceInitialized" + // ) + // ) + } + + override fun onRecognizing(text: String, language: String, confidence: Float) { + // sendAstEvent( + // mapOf( + // "type" to "recognizing", + // "text" to text, + // "language" to language, + // "confidence" to confidence + // ) + // ) + } + + override fun onRecognized(text: String, language: String, confidence: Float) { + FileLogger.d(tag, "识别到文本: $text, 语言: $language, 置信度: $confidence") + sendAsrEvent( + mapOf( + "type" to "result1", + "text" to text, + "detectedLanguage" to language + ) + ) + // sendAstEvent( + // mapOf( + // "type" to "recognized", + // "text" to text, + // "language" to language, + // "confidence" to confidence + // ) + // ) + } + + override fun onTranslated( + originalText: String, + translatedText: String, + targetLanguage: String + ) { + // sendAstEvent( + // mapOf( + // "type" to "translated", + // "originalText" to originalText, + // "translatedText" to translatedText, + // "targetLanguage" to targetLanguage + // ) + // ) + } + + override fun onTranslationStarted(text: String) { + // sendAstEvent( + // mapOf( + // "type" to "translationStarted", + // "text" to text + // ) + // ) + } + + override fun onTranslationFailed(text: String, error: String) { + // sendAstEvent( + // mapOf( + // "type" to "translationFailed", + // "text" to text, + // "error" to error + // ) + // ) + } + + override fun onSynthesisStarted(text: String) { + // sendAstEvent( + // mapOf( + // "type" to "synthesisStarted", + // "text" to text + // ) + // ) + } + + override fun onSynthesisCompleted(text: String) { + FileLogger.d(tag, "语音合成完成,文本=${text}") + + + + // sendAstEvent( + // mapOf( + // "type" to "synthesisCompleted", + // "text" to text + // ) + // ) + } + override fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) { + FileLogger.d(tag, "语音合成音频生成,文本=${text},音频数据大小=${audioData.size}") + BleService.writeExternalAudioData(audioData) + } + override fun onSynthesisFailed(text: String, error: String) { + // sendAstEvent( + // mapOf( + // "type" to "synthesisFailed", + // "text" to text, + // "error" to error + // ) + // ) + } + + override fun onSynthesisProgress(text: String, progress: Float) { + // sendAstEvent( + // mapOf( + // "type" to "synthesisProgress", + // "text" to text, + // "progress" to progress + // ) + // ) + } + + override fun onRecognitionStarted() { + // sendAstEvent( + // mapOf( + // "type" to "recognitionStarted" + // ) + // ) + } + + override fun onRecognitionStopped() { + // sendAstEvent( + // mapOf( + // "type" to "recognitionStopped" + // ) + // ) + } + + override fun onStateChanged(component: String, isActive: Boolean) { + // sendAstEvent( + // mapOf( + // "type" to "stateChanged", + // "component" to component, + // "isActive" to isActive + // ) + // ) + } + + override fun onError(component: String, error: String) { + // sendAstEvent( + // mapOf( + // "type" to "error", + // "component" to component, + // "error" to error + // ) + // ) + } + } + + // 使用协程调用异步初始化方法 + GlobalScope.launch(Dispatchers.Main) { + azureAstHelper.initialize( + azureConfig = azureConfig, + translationConfig = translationConfig, + serviceConfig = serviceConfig, + callback = callback + ) + } + result.success(true) + } + } + } + } + + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { asrChannel.setMethodCallHandler(null) ttsChannel.setMethodCallHandler(null) @@ -613,6 +1033,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { // 可选:处理音频数据 } + override fun onAudioDataReceived1(data: ByteArray) { + + azureAstHelper.pushAudioData(data) + + // 可选:处理音频数据 + } /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别 @@ -624,4 +1050,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { override fun onDeviceInfoReceived(infoType: Int, infoData: Map) { // 不处理设备信息 } -} \ No newline at end of file +} + + + \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 709b99e66..0c0600aba 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -14,7 +14,7 @@ import com.deep_voice.speech.tts.TtsEventType import kotlinx.coroutines.* import java.io.ByteArrayInputStream import java.io.InputStream - +import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer /** * Azure TTS Helper * 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 c1e1b4602..66a24e058 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 @@ -9,11 +9,15 @@ import java.io.FileInputStream import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.atomic.AtomicBoolean -object RecordFile { +/** + * 音频录制文件管理类 + * 支持单声道和双声道录制,提供WAV文件格式输出 + */ +class RecordFile { private var currentAudioFile: File? = null private var fos: FileOutputStream? = null - // 新增:用于异步写入的队列和线程 + // 用于异步写入的队列和线程 private val writeQueue = LinkedBlockingQueue() private val isWriting = AtomicBoolean(false) private var writeThread: Thread? = null @@ -22,15 +26,73 @@ object RecordFile { private val dataBuffer = mutableListOf() private var totalBytesWritten = 0 - // 音频配置 - private val sampleRate = 16000 + // 音频配置 - 可配置的采样率和声道数 + private var sampleRate = 16000 + private var channels = 1 // 1=单声道, 2=立体声 - // 新增:用于存储最后成功保存的文件 + // 用于存储最后成功保存的文件 private var lastSavedFile: File? = null private var lastFileTimestamp: String = "" - var isPause = false + var fileName = "" + // 双声道分离相关变量 + private var leftChannelFile: File? = null + private var rightChannelFile: File? = null + private var leftChannelFos: FileOutputStream? = null + private var rightChannelFos: FileOutputStream? = null + private val leftChannelQueue = LinkedBlockingQueue() + private val rightChannelQueue = LinkedBlockingQueue() + private var leftChannelWriteThread: Thread? = null + private var rightChannelWriteThread: Thread? = null + private var leftChannelBytesWritten = 0 + private var rightChannelBytesWritten = 0 + + /** + * 设置音频配置 + * @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000) + * @param channels 声道数 (1=单声道, 2=立体声) + */ + fun setAudioConfig(sampleRate: Int, channels: Int) { + this.sampleRate = sampleRate + this.channels = channels + Log.d("RecordFile", "音频配置已更新: 采样率=${sampleRate}Hz, 声道数=${channels}") + } + + /** + * 获取左声道文件路径 + * @return 左声道文件的绝对路径,如果不存在则返回null + */ + internal fun getLeftChannelFilePath(): String? { + return leftChannelFile?.absolutePath + } + + /** + * 获取右声道文件路径 + * @return 右声道文件的绝对路径,如果不存在则返回null + */ + internal fun getRightChannelFilePath(): String? { + return rightChannelFile?.absolutePath + } + + /** + * 获取当前音频配置信息 + * @return Map包含采样率和声道数信息 + */ + internal fun getAudioConfig(): Map { + return mapOf( + "sampleRate" to sampleRate, + "channels" to channels + ) + } + + /** + * 检查是否为双声道模式 + * @return true表示双声道,false表示单声道 + */ + internal fun isStereoMode(): Boolean { + return channels == 2 + } // 修改:添加 filePath 参数 internal fun creatingFiles(filePath: String) { @@ -54,6 +116,105 @@ object RecordFile { fos = FileOutputStream(currentAudioFile, true) startWriteThread() + + // 如果是双声道,创建左右声道文件 + if (channels == 2) { + createStereoChannelFiles(filePath) + } + } + + /** + * 创建左右声道文件 + */ + private fun createStereoChannelFiles(originalFilePath: String) { + val originalFile = File(originalFilePath) + val parentDir = originalFile.parentFile + val nameWithoutExt = originalFile.nameWithoutExtension + + // 创建左声道文件 + leftChannelFile = File(parentDir, "${nameWithoutExt}_left.wav") + leftChannelFile?.createNewFile() + leftChannelFos = FileOutputStream(leftChannelFile).apply { + write(generateMonoWavHeader(0)) + close() + } + leftChannelFos = FileOutputStream(leftChannelFile, true) + + // 创建右声道文件 + rightChannelFile = File(parentDir, "${nameWithoutExt}_right.wav") + rightChannelFile?.createNewFile() + rightChannelFos = FileOutputStream(rightChannelFile).apply { + write(generateMonoWavHeader(0)) + close() + } + rightChannelFos = FileOutputStream(rightChannelFile, true) + + // 启动左右声道写入线程 + startStereoWriteThreads() + + Log.d("tag", "双声道文件创建完成: ${leftChannelFile?.name}, ${rightChannelFile?.name}") + } + + /** + * 生成单声道WAV文件头 + */ + private fun generateMonoWavHeader(dataLength: Int): ByteArray { + val totalLength = 36 + dataLength + val bytesPerSample = 2 + val monoChannels = 1 + val byteRate = sampleRate * monoChannels * bytesPerSample + val blockAlign = monoChannels * bytesPerSample + + return byteArrayOf( + 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), + (totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), + ((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), + 'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), + 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), + 16, 0, 0, 0, + 1, 0, + (monoChannels and 0xFF).toByte(), ((monoChannels shr 8) and 0xFF).toByte(), + (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), + ((sampleRate shr 16) and 0xFF).toByte(), ((sampleRate shr 24) and 0xFF).toByte(), + (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), + ((byteRate shr 16) and 0xFF).toByte(), ((byteRate shr 24) and 0xFF).toByte(), + (blockAlign and 0xFF).toByte(), ((blockAlign shr 8) and 0xFF).toByte(), + 16, 0, + 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), + (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), + ((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() + ) + } + + /** + * 启动左右声道写入线程 + */ + private fun startStereoWriteThreads() { + // 左声道写入线程 + leftChannelWriteThread = Thread { + try { + while (isWriting.get() || leftChannelQueue.isNotEmpty()) { + val data = leftChannelQueue.poll() ?: continue + leftChannelFos?.write(data) + } + } catch (e: Exception) { + Log.e("tag", "左声道异步写入失败: ${e.message}") + } + } + leftChannelWriteThread?.start() + + // 右声道写入线程 + rightChannelWriteThread = Thread { + try { + while (isWriting.get() || rightChannelQueue.isNotEmpty()) { + val data = rightChannelQueue.poll() ?: continue + rightChannelFos?.write(data) + } + } catch (e: Exception) { + Log.e("tag", "右声道异步写入失败: ${e.message}") + } + } + rightChannelWriteThread?.start() } @@ -76,19 +237,65 @@ object RecordFile { /** * 保存音频数据到 WAV 文件(异步) + * 如果是双声道,会自动拆分成左右声道分别保存 */ internal fun saveAudioDataToWav(buffer: ByteArray) { if (fos == null || currentAudioFile == null) return - // 放入队列,由写线程写入 - writeQueue.offer(buffer.copyOf()) - totalBytesWritten += buffer.size + + if (channels == 2) { + // 双声道:拆分左右声道 + splitStereoToMono(buffer) + } else { + // 单声道:直接保存 + writeQueue.offer(buffer.copyOf()) + totalBytesWritten += buffer.size + } } + /** + * 将双声道音频数据拆分成左右声道 + * @param stereoBuffer 双声道音频数据(16位PCM,交错格式:L1R1L2R2...) + */ + private fun splitStereoToMono(stereoBuffer: ByteArray) { + if (stereoBuffer.size % 4 != 0) { + Log.w("tag", "双声道音频数据长度不正确: ${stereoBuffer.size}") + return + } + + val sampleCount = stereoBuffer.size / 4 // 每个样本4字节(左右声道各2字节) + val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区 + val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区 + + // 拆分交错的左右声道数据 + for (i in 0 until sampleCount) { + val stereoIndex = i * 4 + val monoIndex = i * 2 + + // 左声道(低位字节在前,高位字节在后) + leftBuffer[monoIndex] = stereoBuffer[stereoIndex] + leftBuffer[monoIndex + 1] = stereoBuffer[stereoIndex + 1] + + // 右声道 + rightBuffer[monoIndex] = stereoBuffer[stereoIndex + 2] + rightBuffer[monoIndex + 1] = stereoBuffer[stereoIndex + 3] + } + + // 将左右声道数据分别放入队列 + leftChannelQueue.offer(leftBuffer) + rightChannelQueue.offer(rightBuffer) + leftChannelBytesWritten += leftBuffer.size + rightChannelBytesWritten += rightBuffer.size + + Log.d("tag", "双声道拆分完成: 左声道${leftBuffer.size}字节, 右声道${rightBuffer.size}字节") + } - // 更新文件头生成(修正RIFF长度计算) + + // 更新文件头生成(支持可配置的采样率和声道数) private fun generateWavHeader(dataLength: Int): ByteArray { val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 - val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 + val bytesPerSample = 2 // 16位PCM + val byteRate = sampleRate * channels * bytesPerSample // 采样率 * 声道数 * 字节/样本 + val blockAlign = channels * bytesPerSample // 声道数 * 字节/样本 return byteArrayOf( 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), @@ -98,10 +305,12 @@ object RecordFile { 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), 16, 0, 0, 0, // PCM头长度 1, 0, // PCM格式 - 1, 0, // 单声道 - (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 - (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 - 2, 0, // 块对齐 (通道数 * 样本位数/8) + (channels and 0xFF).toByte(), ((channels shr 8) and 0xFF).toByte(), // 声道数 + (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), + ((sampleRate shr 16) and 0xFF).toByte(), ((sampleRate shr 24) and 0xFF).toByte(), // 采样率 + (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), + ((byteRate shr 16) and 0xFF).toByte(), ((byteRate shr 24) and 0xFF).toByte(), // 字节率 + (blockAlign and 0xFF).toByte(), ((blockAlign shr 8) and 0xFF).toByte(), // 块对齐 16, 0, // 样本位数 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), @@ -225,31 +434,92 @@ object RecordFile { writeThread?.join(500) fos?.close() + var mainFileSuccess = false + var leftChannelSuccess = false + var rightChannelSuccess = false + + // 处理主文件 currentAudioFile?.let { file -> if (file.length() <= 44 || isSave == false) { file.delete() Log.d("tag", "音频文件过小已删除: ${file.absolutePath}") - return false } else { RandomAccessFile(file, "rw").use { raf -> raf.seek(0) raf.write(generateWavHeader(totalBytesWritten)) } Log.d("tag", "音频文件保存完成: ${file.absolutePath}") - - // 保存成功文件引用 lastSavedFile = file - return true + mainFileSuccess = true } } + + // 处理左右声道文件(如果是双声道) + if (channels == 2) { + leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave) + rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave) + + // 等待左右声道写入线程结束 + leftChannelWriteThread?.join(500) + rightChannelWriteThread?.join(500) + } + + return if (channels == 2) { + leftChannelSuccess && rightChannelSuccess + } else { + mainFileSuccess + } } catch (e: Exception) { Log.e("tag", "更新WAV文件头失败: ${e.message}") return false } finally { + // 清理资源 fos = null currentAudioFile = null writeThread = null + + // 清理双声道资源 + leftChannelFos = null + rightChannelFos = null + leftChannelFile = null + rightChannelFile = null + leftChannelWriteThread = null + rightChannelWriteThread = null + leftChannelBytesWritten = 0 + rightChannelBytesWritten = 0 + totalBytesWritten = 0 + } + } + + /** + * 关闭单个声道文件 + */ + private fun closeStereoChannelFile( + channelFile: File?, + channelFos: FileOutputStream?, + bytesWritten: Int, + channelName: String, + isSave: Boolean + ): Boolean { + return try { + channelFos?.close() + channelFile?.let { file -> + if (file.length() <= 44 || isSave == false) { + file.delete() + Log.d("tag", "${channelName}文件过小已删除: ${file.absolutePath}") + false + } else { + RandomAccessFile(file, "rw").use { raf -> + raf.seek(0) + raf.write(generateMonoWavHeader(bytesWritten)) + } + Log.d("tag", "${channelName}文件保存完成: ${file.absolutePath}") + true + } + } ?: false + } catch (e: Exception) { + Log.e("tag", "${channelName}文件关闭失败: ${e.message}") + false } - return false } } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt similarity index 99% rename from local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt rename to local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt index 84f846967..65727a25d 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt @@ -1,4 +1,4 @@ -package com.yunqiinnovation.azure_speech +package com.yunqiinnovation.azure_speech.tools import android.content.Context import android.media.AudioAttributes @@ -49,7 +49,7 @@ class SimpleAudioPlayer(private val context: Context? = null) { // 推送流 private var pushOutputStream: PushAudioOutputStream? = null - + // 自定义推送流回调 private inner class AudioOutputCallback : PushAudioOutputStreamCallback() { override fun write(dataBuffer: ByteArray): Int { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt new file mode 100644 index 000000000..c9d50f07e --- /dev/null +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt @@ -0,0 +1,426 @@ +package com.yunqiinnovation.azure_speech.tools + +import android.content.Context +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.util.Log +import com.microsoft.cognitiveservices.speech.* +import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat +import java.util.concurrent.LinkedBlockingQueue +import com.microsoft.cognitiveservices.speech.audio.AudioInputStream +import com.microsoft.cognitiveservices.speech.audio.PushAudioInputStream +import java.util.concurrent.atomic.AtomicBoolean +import android.media.AudioManager +// 添加缺失的导入 +import android.media.AudioFocusRequest +import android.media.AudioAttributes +import com.yunqiinnovation.azure_speech.tools.RecordFile + +/** + * 简单音频接收器类,用于处理音频录制和流传输 + * @param azureAsrHelper AzureAsrHelper实例的引用,用于访问共享状态 + */ +class SimpleAudioReceiver(private val context: Context) { + + companion object { + private const val TAG = "SimpleAudioReceiver" + } + + /** + * 音频来源类型(使用 AzureAsrHelper 中定义的枚举) + */ + enum class AudioSourceType { + /** 使用设备麦克风 */ + MICROPHONE, + /** 使用外部提供的音频数据 */ + EXTERNAL + } + + private val bufferSize = 4096 // 可根据需要调整 + private var audioDataCallback: AudioDataCallback? = null + var audioRecord: AudioRecord? = null + var pushAudioStream: PushAudioInputStream? = null + // 音频源配置 + var audioSourceType = AudioSourceType.MICROPHONE + // 新增:用于异步写入的队列和线程 + 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 + // 音频模式管理 + private var audioManager: AudioManager? = null + private var originalAudioMode = AudioManager.MODE_NORMAL + + // 录音文件处理 + var recordfile: RecordFile? = null + /** + * 获取最优音频格式 + */ + 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) + } + + /** + * 初始化音频录制 + */ + fun initAudioRecord() { + val format = getOptimalAudioFormat() + pushAudioStream = AudioInputStream.createPushStream(format) + isRunning.set(true) + Log.d(TAG, "initAudioRecord: ${isRunning}") + startWriteThread() // 再启动数据读取线程 + // 初始化音频管理器 + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL + } + + /** + * 启动写入线程 + */ + private fun startWriteThread() { + + writeThread = Thread { + try { + while (isRunning.get()) { + // Log.d(TAG, "startWriteThread:isWriting= ${isWriting}") + if (!isWriting.get()) { + Thread.sleep(10) // 短暂休眠避免空转 + continue + } + var data: ByteArray? = null + var bytesToWrite = 0 + //Log.d(TAG, "startWriteThread: ${audioRecord?.recordingState}") + // 情况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 + } + + bytesRead == 0 -> continue // 无数据可读 + else -> bytesToWrite = bytesRead // 有效数据 + } + } + // 情况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 { + Log.d(TAG, "写入数据大小: ${finalData.size}") + if(audioDataCallback!=null){ + audioDataCallback?.onAudio(finalData) + } + pushAudioStream?.write(finalData) + + + recordfile?.saveAudioDataToWav(finalData) + + } catch (e: Exception) { + Log.e(TAG, "写入失败: ${e.message}") + } + } else { + Thread.yield() // 避免空转消耗CPU + } + + } + } 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()) + } + + /** + * 开始音频输入 + * @param audioSourceType 音频源类型 + * @param callback 音频数据回调,可为空 + */ + fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) { + this.audioSourceType = audioSourceType + this.audioDataCallback = callback + writeQueue.clear() + isWriting.set(true) + + when (audioSourceType) { + AudioSourceType.MICROPHONE -> runMicrophoneCapture() + AudioSourceType.EXTERNAL -> runExternalCapture() + } + } +/** + * 配置通话音频模式以优化回声消除 + */ + private fun setupCommunicationAudioMode() { + try { + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + // 保存原始音频模式 + originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL + + // 设置通话模式 - 这是关键! + audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + + // 启用扬声器(如果需要外放) + audioManager?.isSpeakerphoneOn = true + + // 请求音频焦点 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build() + ) + .build() + + audioManager?.requestAudioFocus(focusRequest) + } + + Log.d("AudioMode", "通话音频模式已设置: MODE_IN_COMMUNICATION") + + } catch (e: Exception) { + Log.e("AudioMode", "设置通话音频模式失败: ${e.message}") + } + } + + /** + * 运行麦克风捕获 + */ + private fun runMicrophoneCapture() { + try { + // 首先设置通话音频模式 + setupCommunicationAudioMode() + + 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) + + // 使用VOICE_COMMUNICATION音频源(专为VoIP优化) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + val format = AudioFormat.Builder() + .setSampleRate(sampleRate) + .setEncoding(audioFormat) + .setChannelMask(AudioFormat.CHANNEL_IN_MONO) + .build() + + audioRecord = AudioRecord.Builder() + .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) + .setAudioFormat(format) + .setBufferSizeInBytes(minBufferSize * 2) + .build() + } else { + audioRecord = AudioRecord( + MediaRecorder.AudioSource.VOICE_COMMUNICATION, + sampleRate, + channelConfig, + audioFormat, + minBufferSize * 2 + ) + } + + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + throw IllegalStateException("AudioRecord初始化失败") + } + + audioRecord?.startRecording() + + Log.d("TAG", "通话模式录音开始,采样率: $sampleRate Hz") + + } catch (e: Exception) { + Log.e("TAG", "麦克风捕获失败: ${e.message}") + } + } + + /** + * 运行外部捕获 + */ + 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 { + + } + } +/** + * 继续麦克风捕获 + */ + fun resumeRecord() { + try { + isWriting.set(true) + // 首先设置通话音频模式 + setupCommunicationAudioMode() + // 停止录音 + audioRecord?.startRecording() + + + } catch (e: Exception) { + Log.e(TAG, "继续录音失败: ${e.message}") + } + } + + /** + * 停止麦克风捕获 + */ + fun stopMicrophoneCapture() { + try { + isWriting.set(false) + + // 停止录音 + audioRecord?.stop() + + // 恢复音频模式 + restoreCommunicationAudioMode() + + } catch (e: Exception) { + Log.e(TAG, "停止麦克风捕获失败: ${e.message}") + } + } + + /** + * 释放音频资源 + */ + fun releaseAudioResources() { + try { + isRunning.set(false) + isWriting.set(false) + + // 中断并等待捕获线程结束 + writeThread?.interrupt() + writeThread?.join(300) // 最多等待300ms + + // 释放录音实例 + audioRecord?.release() + audioRecord = null + } catch (e: Exception) { + Log.e(TAG, "释放音频资源失败: ${e.message}") + e.printStackTrace() + } finally { + writeQueue.clear() + writeThread = null + } + } + /** + * 禁用蓝牙音频功能,切换回正常音频模式 + */ + fun disableBluetoothAudio() { + try { + // 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由 + audioManager?.isBluetoothScoOn = false + // 停止蓝牙SCO连接 + audioManager?.stopBluetoothSco() + // 切换回正常音频模式(非通话模式) + audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + + } catch (e: Exception) { + Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}") + } + } + + /** + * 恢复原始音频设备状态(通常是重新启用蓝牙) + */ + fun restoreOriginalAudioState() { + try { + // 恢复应用启动时的原始音频模式 + audioManager?.mode = AudioManager.MODE_NORMAL + + // 重新启用蓝牙SCO + audioManager?.isBluetoothScoOn = true + // 启动蓝牙SCO连接(通常在需要蓝牙通话时使用) + audioManager?.startBluetoothSco() + + } catch (e: Exception) { + Log.e("AudioConfig", "Failed to restore audio state: ${e.message}") + } + } + /** + * 恢复原始音频模式 + */ + private fun restoreCommunicationAudioMode() { + try { + audioManager?.mode = originalAudioMode + audioManager?.isSpeakerphoneOn = false + // 释放音频焦点 + Log.d("AudioMode", "音频模式已恢复") + } catch (e: Exception) { + Log.e("AudioMode", "恢复音频模式失败: ${e.message}") + } + } + + fun isWriting(): Boolean { + return isWriting.get() + } + + /** + * 连续识别回调接口 + */ + interface AudioDataCallback { + /** + * 音频数据回调 + * @param data 音频数据字节数组 + */ + fun onAudio(data: ByteArray) + } +} + diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 40846707f..8e3e6a85e 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -285,6 +285,30 @@ public class AzureAsrHelper: NSObject { } } + /** + * 设置音频配置 + * @param sampleRate 采样率,默认16000 + * @param channels 声道数,默认1 + * @return 设置是否成功 + */ + public func setAudioConfig(sampleRate: Int, channels: Int) -> Bool { + do { + print("设置音频配置 - 采样率: \(sampleRate), 声道: \(channels)") + + // 设置音频流的配置 + audioStream?.setAudioConfig(sampleRate: sampleRate, channels: channels) + + // 设置录音文件的配置 + recordfile?.setAudioConfig(sampleRate: sampleRate, channels: channels) + + print("音频配置设置成功") + return true + } catch { + print("设置音频配置失败: \(error.localizedDescription)") + return false + } + } + /** * 设置识别器 @@ -678,6 +702,32 @@ public class AzureAsrHelper: NSObject { interleaved: true ) } + + /** + * 设置音频配置 + * @param sampleRate 采样率,默认16000 + * @param channels 声道数,默认1 + */ + public func setAudioConfig(sampleRate: Int, channels: Int) { + print("AudioStream设置音频配置 - 采样率: \(sampleRate), 声道: \(channels)") + + // 更新音频格式 + audioFormat = AVAudioFormat( + commonFormat: .pcmFormatInt16, + sampleRate: Double(sampleRate), + channels: AVAudioChannelCount(channels), + interleaved: true + ) + + // 如果已经有推流,重新创建 + if pushAudioStream != nil { + let audioStreamFormat = SPXAudioStreamFormat() + audioStreamFormat?.initUsingPCM(withSampleRate: UInt(sampleRate), bitsPerSample: 16, channels: UInt(channels)) + pushAudioStream = SPXPushAudioInputStream(audioFormat: audioStreamFormat) + } + + print("AudioStream音频配置设置完成") + } /** * 开始音频输入 */ diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index 5955ac7c8..4d18c9c71 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -262,7 +262,7 @@ import os.log return } do { - print("音频文件名称为: (filePath)") + print("音频文件名称为: \(filePath)") try azureAsrHelper.enableRecord(filePath: filePath) result(true) } catch { @@ -272,6 +272,9 @@ import os.log case "pauseRecord": azureAsrHelper.pauseRecord() result(true) + case "resumeRecord": + azureAsrHelper.resumeRecord() + result(true) case "stopRecord": guard let args = call.arguments as? [String: Any], @@ -299,6 +302,18 @@ import os.log azureTtsHelper.stop() result(true) + case "setAudioConfig": + guard let args = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数不能为空", details: nil)) + return + } + + let sampleRate = args["sampleRate"] as? Int ?? 16000 + let channels = args["channels"] as? Int ?? 1 + + let success = azureAsrHelper.setAudioConfig(sampleRate: sampleRate, channels: channels) + result(success) + default: result(FlutterMethodNotImplemented) } @@ -558,4 +573,4 @@ extension AzureSpeechPlugin: AudioDataListener { sendTtsEvent(eventMap) } -} +} diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index 9ae9dd608..dd3988a04 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -10,12 +10,17 @@ object BleConst { /** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000abc0-0000-1111-2222-123456789abc") + /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ + val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc") - /** 音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */ - val AUDIO_SERVICE_UUID1: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC") + /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ + val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc") + + /** 通话音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */ + val CALL_AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC") - /** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */ - val RECEIVE_AUDIO_CHAR_UUID1: UUID = UUID.fromString("0000ABC2-0001-1111-2222-123456789ABC") + /** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */ + val CALL_WRITE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC") /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb") @@ -23,15 +28,6 @@ object BleConst { /** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */ val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb") - /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc") - - /** 音频写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - val AUDIO_WRITE_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC") - - /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ - val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc") - /** 客户端特征配置描述符UUID */ val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 216f8a136..8cdcac089 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 @@ -12,6 +12,7 @@ import java.util.concurrent.CopyOnWriteArrayList // Import Jieli Opus SDK classes import com.jieli.jl_audio_decode.opus.model.OpusOption import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback +import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback import com.jieli.jl_audio_decode.opus.OpusManager import com.jieli.jl_audio_decode.exceptions.OpusException @@ -25,6 +26,7 @@ import android.os.Handler import java.util.concurrent.atomic.AtomicBoolean import android.util.Log import androidx.annotation.RequiresPermission +import java.util.concurrent.TimeUnit /** * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 @@ -52,6 +54,10 @@ object BleService { // 数据相关回调 fun onAudioDataReceived(data: ByteArray) + // 数据相关回调 + fun onAudioDataReceived1(data: ByteArray) + + // 唤醒信号相关回调 fun onWakeupSignalReceived() // 设备信息相关回调 - 统一回调接口 @@ -76,8 +82,9 @@ object BleService { private var notifyChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null private var audioChar: BluetoothGattCharacteristic? = null - private var audioChar1: BluetoothGattCharacteristic? = null + private var callWriteChar: BluetoothGattCharacteristic? = null var recordfile: RecordingFile? = null + var recordfile1: RecordingFile? = null // 扫描相关 private lateinit var scanHandler: Handler @@ -104,11 +111,58 @@ object BleService { // Opus解码器实例 private var opusManager: OpusManager? = null + private var option: OpusOption? = null private val mainHandler = Handler(Looper.getMainLooper()) + // 解码音频数据队列和处理线程 + private val audioDataQueue = LinkedBlockingQueue() + private var audioQueueProcessorThread: Thread? = null + + // 音频数据缓存 + private val audioDataBuffer = mutableListOf() + //private val AUDIO_BUFFER_SIZE = 1280// 1280字节缓存阈值 + + // 音频数据发送相关 + private val audioSendQueue = LinkedBlockingQueue() + private var audioSendThread: Thread? = null + private val audioSendHandler = Handler(Looper.getMainLooper()) + private val isAudioSending = AtomicBoolean(false) + + // 音频数据分块发送的常量 + private val AUDIO_CHUNK_SIZE = 120 // 每次发送80字节 + private val AUDIO_SEND_INTERVAL = 60L // 发送间隔20ms + + // 音频数据缓冲区,用于累积数据到80字节再发送 + private val audioBuffer = mutableListOf() + + // 重发机制相关常量 + private val MAX_RETRY_COUNT = 1 // 最大重试次数 + private val RETRY_DELAY = 30L // 重试延迟时间(毫秒) + // 初始化状态 private var isInitialized = false + // 音频数据统计相关变量 + private var bytesReceivedInCurrentSecond = 0 + // 添加发送数据统计变量 + private var bytesSentInCurrentSecond = 0 + private var lastStatisticsTime = System.currentTimeMillis() + private val statisticsHandler = Handler(Looper.getMainLooper()) + private val statisticsRunnable = object : Runnable { + override fun run() { + val currentTime = System.currentTimeMillis() + val timeDiff = currentTime - lastStatisticsTime + if (timeDiff >= 1000) { // 每秒统计一次 + Log.i(TAG, "每秒接收音频数据: $bytesReceivedInCurrentSecond 字节") + Log.i(TAG, "每秒发送音频数据: $bytesSentInCurrentSecond 字节") + bytesReceivedInCurrentSecond = 0 + bytesSentInCurrentSecond = 0 + lastStatisticsTime = currentTime + } + statisticsHandler.postDelayed(this, 1000) // 每秒执行一次 + } + } + private val replyTimeoutHandler = Handler(Looper.getMainLooper()) private val replyTimeoutRunnable = Runnable { if (!isReply) { @@ -128,20 +182,6 @@ object BleService { try { this.context = appContext.applicationContext - - // otaManager = OTAManager(this.context).apply { - // // 设置数据回调 - // setDataCallback(object : OTAManager.DataCallback { - // override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) { - // data?.let { - // Log.i("BleService", "收到数据:${it.size} 字节") - // Log.i("BleService", "认证交互数据${it.contentToString()}") - // // 移除次数限制,除非明确需要 - // otaManager?.onReceiveDeviceData(device, it) - // } - // } - // }) - // } // 初始化蓝牙管理器和适配器 bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager @@ -151,16 +191,23 @@ object BleService { // 初始化Handler scanHandler = Handler(Looper.getMainLooper()) - // 初始化OpusManager和OTAManager + // 启动队列处理 + startAudioQueueProcessing() + + // 初始化OpusManager和OpusOption try { opusManager = OpusManager() - startOpusStreamDecoding() + option = OpusOption() Log.d(TAG, "OpusManager初始化成功") } catch (e: OpusException) { Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) // 根据需要决定是否因为Opus初始化失败而返回false } recordfile = RecordingFile(this.context) + recordfile!!.fileName = "不拆分" + + recordfile1 = RecordingFile(this.context) + recordfile1!!.fileName = "重新压缩" isInitialized = true Log.d(TAG, "BLE服务初始化成功") return true @@ -205,6 +252,25 @@ object BleService { Log.d(TAG, "已清除所有BLE回调") } + fun writeExternalAudioData(data: ByteArray) { + // 检查OpusManager是否已初始化 + if (opusManager == null) { + Log.e(TAG, "opusManager 未初始化") + return + } + + // 如果已经在编码流中,直接写入数据 + if (opusManager?.isEncodeStream == true) { + Log.d(TAG, "正在进行Opus编码流,写入音频数据") + // 将外部音频数据写入编码流,每次处理1280字节 + opusManager?.writeEncodeStream(data) + } else { + Log.w(TAG, "Opus编码流未启动,无法写入音频数据") + // 可选:自动启动编码流 + // startOpusEncodeStream() + } + } + // ====================================================================================================== // 扫描功能 // ====================================================================================================== @@ -360,15 +426,16 @@ object BleService { } return null; } + fun getDeviceInfos(deviceName: String): List> { // 更新缓存 val matchedResults = scanResults.filter { it.device.name == deviceName } Log.d(TAG, "找到 ${matchedResults.size} 个名称为 $deviceName 的设备") - + val ret = mutableListOf>() - + // 如果找到匹配设备名的结果,返回匹配设备的信息 if (matchedResults.isNotEmpty()) { for (result in matchedResults) { @@ -386,9 +453,10 @@ object BleService { } } } - + return ret } + /** * 打印设备信息和服务UUID */ @@ -517,7 +585,7 @@ object BleService { notifyChar = null writeChar = null audioChar = null - audioChar1 = null + callWriteChar = null } } @@ -530,42 +598,56 @@ object BleService { notifyConnectionStateChanged(state) } + // 在连接成功时启动统计(可以在onConnectionStateChange的连接成功分支中添加) + private fun startBytesStatistics() { + bytesReceivedInCurrentSecond = 0 + lastStatisticsTime = System.currentTimeMillis() + statisticsHandler.post(statisticsRunnable) + Log.i(TAG, "开始统计每秒接收字节数") + } + + // 在断开连接时停止统计 + private fun stopBytesStatistics() { + statisticsHandler.removeCallbacks(statisticsRunnable) + Log.i(TAG, "停止统计每秒接收字节数") + } + /** * GATT回调 */ private val gattCallback = object : BluetoothGattCallback() { - - // override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { - // if (status == BluetoothGatt.GATT_SUCCESS) { - // Log.i(TAG, "MTU 更新成功: $mtu") - // configureOTA() - - // startOTA() - // } else { - // Log.e(TAG, "MTU 更新失败: status=$status") - // } - // } - override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { when { status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { updateConnectionState(BleConst.STATE_CONNECTED) + + // 设置PHY值为2M以提高传输速度 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + // 首先尝试读取当前PHY状态 + g.readPhy() + + // 然后请求设置2M PHY + g.setPreferredPhy( + BluetoothDevice.PHY_LE_2M_MASK, // TX PHY: 2M + BluetoothDevice.PHY_LE_2M_MASK, // RX PHY: 2M + BluetoothDevice.PHY_OPTION_NO_PREFERRED + ) + Log.d(TAG, "已请求设置PHY为2M") + } catch (e: Exception) { + Log.w(TAG, "设置PHY失败: ${e.message},将使用默认1M PHY") + } + } else { + Log.w(TAG, "当前Android版本不支持PHY设置(需要API 26+),使用默认1M PHY") + } + g.discoverServices() - // Log.i(TAG, "连接成功,开始 MTU 协商") - - // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK) - // g.requestMtu(512) // 触发 MTU 修改流程 - } newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) disconnectGatt() - - // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING) - // g.close() - // otaManager?.release(); } else -> { @@ -574,6 +656,45 @@ object BleService { } } } + + /** + * PHY读取回调 - 获取当前PHY状态 + */ + override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (status == BluetoothGatt.GATT_SUCCESS) { + val txPhyStr = getPhyString(txPhy) + val rxPhyStr = getPhyString(rxPhy) + Log.i(TAG, "当前PHY状态: TX=$txPhyStr, RX=$rxPhyStr") + } else { + Log.w(TAG, "读取PHY状态失败: status=$status") + } + } + } + + /** + * PHY更新回调 - 监控PHY设置结果 + */ + override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + val txPhyStr = getPhyString(txPhy) + val rxPhyStr = getPhyString(rxPhy) + Log.i(TAG, "PHY更新成功: TX=$txPhyStr, RX=$rxPhyStr") + } + 6 -> { // GATT_REQUEST_NOT_SUPPORTED + Log.w(TAG, "设备不支持PHY更新,将继续使用1M PHY进行通信") + } + 133 -> { // GATT_ERROR + Log.w(TAG, "PHY更新出现一般性错误,可能是设备兼容性问题") + } + else -> { + Log.w(TAG, "PHY更新失败: status=$status") + } + } + } + } override fun onServicesDiscovered(g: BluetoothGatt, status: Int) { if (status != BluetoothGatt.GATT_SUCCESS) { @@ -592,16 +713,18 @@ object BleService { audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) - // 获取音频服务1特征 - val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1) - audioChar1 = audioSvc1?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID1) - if (notifyChar == null || writeChar == null) { - Log.e(TAG, "未找到主服务所需特征") + // 获取通话音频服务特征 + val callAudioSvc = g.getService(BleConst.CALL_AUDIO_SERVICE_UUID) + callWriteChar = callAudioSvc?.getCharacteristic(BleConst.CALL_WRITE_AUDIO_CHAR_UUID) + if (callWriteChar == null) { + Log.e(TAG, "未找到通话音频主服务所需特征") updateConnectionState(BleConst.STATE_ERROR) return } - + Log.d(TAG, "找到通话音频服务所需特征") +startBytesStatistics() writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE + callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE //要先设置音频服务的通知,否则接收不到 // 设置音频服务的通知(如果存在) if (audioChar != null) { @@ -610,15 +733,9 @@ object BleService { } else { Log.w(TAG, "音频服务特征未找到") } - if (audioChar1 != null) { - setupNotifications(g, audioChar1) - Log.i(TAG, "音频服务特征找到并设置通知") - } else { - Log.w(TAG, "音频服务特征未找到") - } + // 设置主服务的通知 setupNotifications(g, notifyChar) -//开启解码 } @@ -628,15 +745,11 @@ object BleService { // 根据特征UUID区分处理 when (c.uuid) { - // 音频特征数据 - // BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> { - - // Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1") - // processAudioData(data) - // } // 音频特征数据 BleConst.RECEIVE_AUDIO_CHAR_UUID -> { - + + // 统计接收到的字节数 + bytesReceivedInCurrentSecond += data.size processAudioData(data) } // 通知特征数据(命令和控制) @@ -713,12 +826,30 @@ object BleService { */ private fun processAudioData(data: ByteArray) { try { + + if (opusManager?.isDecodeStream == true) { - recordfile?.saveAudioDataToWav(data) - opusManager?.writeAudioStream(data) + // 将完整的帧加入队列,由专门的线程处理 + audioDataQueue.offer(data) + // // 将接收到的数据添加到缓存中 + // synchronized(audioDataBuffer) { + // audioDataBuffer.addAll(data.toList()) + + // // 当缓存达到阈值时,提取完整的帧进行解码 + // while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) { + // // 提取一个完整的帧(1280字节) + // val frameData = ByteArray(AUDIO_BUFFER_SIZE) + // for (i in 0 until AUDIO_BUFFER_SIZE) { + // frameData[i] = audioDataBuffer.removeAt(0) + // } + + + // Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码") + // } + // } } else { - // Log.d(TAG, "Opus解码流未启动,忽略音频数据") + // Log.d(TAG, "Opus解码流未启动,忽略音频数据") } } catch (e: Exception) { Log.e(TAG, "处理音频数据异常: ${e.message}", e) @@ -922,11 +1053,13 @@ object BleService { codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON ) { - + recordfile1!!.closeFile() + recordfile1!!.creatingFiles() recordfile!!.closeFile() recordfile!!.creatingFiles() } else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) { recordfile!!.closeFile() + recordfile1!!.closeFile() } // 声道模式描述 val channelDesc = when (channelMode) { @@ -1102,11 +1235,16 @@ object BleService { Log.d(TAG, "已停止正在进行的Opus解码流") } - val option = OpusOption() - .setHasHead(hasHeader) - .setChannel(channel) - .setSampleRate(sampleRate) - .setPacketSize(packetSize) + // 清理音频数据缓存,确保开始时是干净的状态 + synchronized(audioDataBuffer) { + audioDataBuffer.clear() + Log.d(TAG, "开始解码前已清理音频数据缓存") + } + + option!!.setHasHead(hasHeader) + option!!.setChannel(channel) + option!!.setSampleRate(sampleRate) + option!!.setPacketSize(packetSize) Log.d(TAG, "准备开始Opus数据流解码, 参数: $option") @@ -1115,7 +1253,40 @@ object BleService { override fun onDecodeStream(data: ByteArray?) { if (data != null) { // Log.d(TAG, "Opus解码数据: ${data.size} bytes") - notifyAudioDataReceived(data) + if (option!!.getChannel() == 2) { + + //解码数据再重新编码回去 + + + val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节) + val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区 + val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区 + + // 拆分交错的左右声道数据 + for (i in 0 until sampleCount) { + val stereoIndex = i * 4 + val monoIndex = i * 2 + + // 左声道(低位字节在前,高位字节在后) + leftBuffer[monoIndex] = data[stereoIndex] + leftBuffer[monoIndex + 1] = data[stereoIndex + 1] + + // 右声道 + rightBuffer[monoIndex] = data[stereoIndex + 2] + rightBuffer[monoIndex + 1] = data[stereoIndex + 3] + } + + // //重新编码 + // opusManager?.writeEncodeStream(rightBuffer) + //notifyAudioDataReceived1(leftBuffer) + //回调 + notifyAudioDataReceived(rightBuffer)//对方的 + notifyAudioDataReceived1(leftBuffer)//自己的 + } else if (option!!.getChannel() == 1) { + notifyAudioDataReceived(data) + } else { + Log.e(TAG, "Opus解码数据错误: ${data.size} bytes") + } } } @@ -1143,6 +1314,13 @@ object BleService { private fun stopOpusStreamDecoding(): Boolean { if (opusManager?.isDecodeStream == true) { opusManager?.stopDecodeStream() + + // 清理音频数据缓存 + synchronized(audioDataBuffer) { + audioDataBuffer.clear() + Log.d(TAG, "已清理音频数据缓存") + } + Log.i(TAG, "已停止Opus数据流解码") return true } @@ -1150,7 +1328,268 @@ object BleService { return false } +// ====================================================================================================== + // Opus 编码相关 + // ====================================================================================================== + + private fun startOpusEncodeStream(): Boolean { + if (opusManager == null) { + Log.e(TAG, "OpusManager未初始化,无法开始编码") + return false + } + + // 如果已经在编码流,先停止 + if (opusManager?.isEncodeStream == true) { + Log.d(TAG, "Opus编码流已在运行") + return true + } + + // 启动音频发送线程 + startAudioSendThread() + var streamStartedSuccessfully = false + opusManager?.startEncodeStream(object : OnEncodeStreamCallback { + override fun onEncodeStream(data: ByteArray?) { + if (data != null) { + // 编码完成的数据处理: + // 1. 保存到WAV文件 + recordfile1?.saveAudioDataToWav(data) + // 2. 将编码后的数据加入发送队列进行分块发送 + addAudioDataToSendQueue(data) + } else { + Log.w(TAG, "编码回调收到空数据") + } + } + + override fun onStart() { + streamStartedSuccessfully = true + Log.i(TAG, "Opus数据流编码已开始") + } + + override fun onComplete(outPath: String?) { + Log.i(TAG, "Opus数据流编码完成: $outPath") + } + + override fun onError(code: Int, message: String?) { + Log.e(TAG, "Opus数据流编码错误: [$code] $message") + } + }) + + return true + } + + private fun stopOpusEncodeStream(): Boolean { + if (opusManager?.isEncodeStream == true) { + opusManager?.stopEncodeStream() + stopAudioSendThread() // 停止音频发送线程 + Log.i(TAG, "已停止Opus数据流编码") + return true + } + Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化") + return false + } + + /** + * 将音频数据添加到发送队列 + * 只有当缓冲区达到80字节时才发送数据 + * @param data 音频数据字节数组 + */ + private fun addAudioDataToSendQueue(data: ByteArray) { + try { + // 将新数据添加到缓冲区 + audioBuffer.addAll(data.toList()) + + // 当缓冲区达到80字节时,发送数据 + while (audioBuffer.size >= AUDIO_CHUNK_SIZE) { + // 取出80字节数据 + val chunk = ByteArray(AUDIO_CHUNK_SIZE) + for (i in 0 until AUDIO_CHUNK_SIZE) { + chunk[i] = audioBuffer.removeAt(0) + } + + // 将数据加入发送队列 + if (!audioSendQueue.offer(chunk)) { + Log.w(TAG, "音频发送队列已满,丢弃数据块") + break + } + } + + // Log.d(TAG, "音频数据已加入缓冲区,当前缓冲区大小: ${audioBuffer.size} 字节") + } catch (e: Exception) { + Log.e(TAG, "处理音频数据异常: ${e.message}", e) + } + } + + /** + * 启动音频数据发送线程 + */ + private fun startAudioSendThread() { + if (audioSendThread?.isAlive == true) { + Log.d(TAG, "音频发送线程已在运行") + return + } + + isAudioSending.set(true) + audioSendThread = Thread { + Log.i(TAG, "音频发送线程已启动") + + while (isAudioSending.get() && !Thread.currentThread().isInterrupted) { + try { + // 从队列中取出音频数据块 + val audioChunk = audioSendQueue.poll(100, TimeUnit.MILLISECONDS) + + if (audioChunk != null) { + // 发送音频数据块 + sendAudioChunk(audioChunk) + + // 控制发送频率,避免蓝牙缓冲区溢出 + Thread.sleep(AUDIO_SEND_INTERVAL) + } + } catch (e: InterruptedException) { + Log.d(TAG, "音频发送线程被中断") + break + } catch (e: Exception) { + Log.e(TAG, "音频发送线程异常: ${e.message}", e) + } + } + + Log.i(TAG, "音频发送线程已停止") + }.apply { + name = "AudioSendThread" + start() + } + } + + /** + * 停止音频数据发送线程 + */ + private fun stopAudioSendThread() { + isAudioSending.set(false) + audioSendThread?.interrupt() + audioSendQueue.clear() + audioBuffer.clear() // 清空音频缓冲区 + Log.i(TAG, "音频发送线程已停止,队列和缓冲区已清空") + } + + /** + * 发送音频数据块到设备(带重发机制) + * @param chunk 要发送的音频数据块 + * @param retryCount 当前重试次数 + */ + private fun sendAudioChunk(chunk: ByteArray, retryCount: Int = 0) { + try { + if (callWriteChar == null || bluetoothGatt == null) { + Log.e(TAG, "蓝牙连接或特征值未准备就绪") + return + } + + // 在主线程中执行蓝牙写入操作 + audioSendHandler.post { + try { + callWriteChar?.value = chunk + val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) + + if (isSuccess == true) { + // 累加发送成功的字节数 + bytesSentInCurrentSecond += chunk.size + Log.d(TAG, "成功发送音频数据块,大小: ${chunk.size} 字节") + } else { + Log.e(TAG, "发送音频数据块失败,大小: ${chunk.size} 字节") + // 详细的失败原因分析 + val failureReason = StringBuilder("发送音频数据块失败,大小: ${chunk.size} 字节") + + // 检查蓝牙连接状态 + if (bluetoothGatt == null) { + failureReason.append(" - 原因: BluetoothGatt为空") + } else { + // 检查连接状态 + val connectionState = bluetoothManager.getConnectionState( + bluetoothGatt!!.device, + BluetoothProfile.GATT + ) + when (connectionState) { + BluetoothProfile.STATE_DISCONNECTED -> { + failureReason.append(" - 原因: 设备已断开连接") + } + BluetoothProfile.STATE_CONNECTING -> { + failureReason.append(" - 原因: 设备正在连接中") + } + BluetoothProfile.STATE_DISCONNECTING -> { + failureReason.append(" - 原因: 设备正在断开连接") + } + BluetoothProfile.STATE_CONNECTED -> { + // 连接正常,检查其他原因 + if (callWriteChar == null) { + failureReason.append(" - 原因: 写入特征值为空") + } else { + // 检查特征值属性 + val properties = callWriteChar!!.properties + if ((properties and BluetoothGattCharacteristic.PROPERTY_WRITE) == 0 && + (properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) { + failureReason.append(" - 原因: 特征值不支持写入操作") + } else if (chunk.size > 512) { // BLE MTU通常限制 + failureReason.append(" - 原因: 数据块过大 (${chunk.size} > 512字节)") + } else { + failureReason.append(" - 原因: 未知错误,可能是设备忙碌或缓冲区满") + } + } + } + else -> { + failureReason.append(" - 原因: 未知连接状态($connectionState)") + } + } + } + + // 检查蓝牙适配器状态 + if (bluetoothAdapter?.isEnabled != true) { + failureReason.append(" - 蓝牙适配器未启用") + } + + Log.e(TAG, failureReason.toString()) + + // // 实现重发机制 + // if (retryCount < MAX_RETRY_COUNT) { + // Log.w(TAG, "准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT") + + // // 延迟后重发 + // audioSendHandler.postDelayed({ + // val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) + + // if (isSuccess == true) { + // Log.d(TAG, "重发音频数据块成功") + // } else { + // Log.e(TAG, "重发音频数据块失败") + // } + // }, RETRY_DELAY) + // } else { + // Log.e(TAG, "音频数据块发送失败,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块") + // // 可以选择将失败的数据块记录到日志或进行其他处理 + // } + } + } catch (e: Exception) { + Log.e(TAG, "发送音频数据块异常: ${e.message}", e) + + // // 异常情况下也进行重发 + // if (retryCount < MAX_RETRY_COUNT) { + // Log.w(TAG, "发送异常,准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT") + // audioSendHandler.postDelayed({ + // val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) + + // if (isSuccess == true) { + // Log.d(TAG, "重发音频数据块成功") + // } else { + // Log.e(TAG, "重发音频数据块失败") + // } + // }, RETRY_DELAY) + // } else { + // Log.e(TAG, "音频数据块发送异常,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块") + // } + } + } + } catch (e: Exception) { + Log.e(TAG, "发送音频数据块异常: ${e.message}", e) + } + } // ====================================================================================================== // 公开的命令接口 // ====================================================================================================== @@ -1217,7 +1656,7 @@ object BleService { // ) startOpusStreamDecoding(false, 1, 16000, 40) - Log.i(TAG, "打开解码0xA2") + Log.i(TAG, "打开解码0xA1") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CODEC_CONTROL_DECODE_ON.toByte(), @@ -1231,8 +1670,10 @@ object BleService { * 控制编解码 - 打开解码 */ fun openA2DPDecoder(): Boolean { - Log.i(TAG, "打开编码 0xA1") - startOpusStreamDecoding(false, 1, 16000, 80) + Log.i(TAG, "打开编码 0xA2") + startOpusEncodeStream() + //双声道 80字节 + startOpusStreamDecoding(false, 2, 16000, 80) // Log.i(TAG, "打开解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( @@ -1323,7 +1764,7 @@ object BleService { * 连接状态检查 */ private fun checkConn(): Boolean = - bluetoothGatt != null && writeChar != null && + bluetoothGatt != null && writeChar != null && callWriteChar != null && connectionState.value == BleConst.STATE_CONNECTED /** @@ -1351,14 +1792,28 @@ object BleService { stopScan() scanHandler.removeCallbacksAndMessages(null) disconnectGatt() - // 释放OpusManager - opusManager?.let { - if (it.isDecodeStream) { - it.stopDecodeStream() - } - it.release() + // 停止音频队列处理线程 + audioQueueProcessorThread?.interrupt() + audioQueueProcessorThread = null + audioDataQueue.clear() + + // 停止音频发送线程 + stopAudioSendThread() + + // 清理音频数据缓存 + synchronized(audioDataBuffer) { + audioDataBuffer.clear() + Log.d(TAG, "已清理音频数据缓存") } + + Log.d(TAG, "音频解码线程和队列处理线程已清理") + + // 释放OpusManager + stopOpusEncodeStream() + stopOpusStreamDecoding() + opusManager?.release() opusManager = null + option = null Log.d(TAG, "OpusManager已释放") isInitialized = false // 标记为未初始化 } @@ -1424,8 +1879,7 @@ object BleService { isReply = false replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 return true - } else - { + } else { commandQueue.poll() return false } @@ -1511,6 +1965,18 @@ object BleService { } } } + /** + * 向所有回调监听器分发音频数据 + */ + private fun notifyAudioDataReceived1(data: ByteArray) { + for (callback in callbacks) { + try { + callback.onAudioDataReceived1(data) + } catch (e: Exception) { + Log.e(TAG, "分发音频数据回调异常", e) + } + } + } /** * 向所有回调监听器分发唤醒信号 @@ -1544,6 +2010,46 @@ object BleService { } } + /** + * 获取PHY类型的字符串描述 + */ + private fun getPhyString(phy: Int): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + when (phy) { + BluetoothDevice.PHY_LE_1M -> "1M" + BluetoothDevice.PHY_LE_2M -> "2M" + BluetoothDevice.PHY_LE_CODED -> "Coded" + else -> "Unknown($phy)" + } + } else { + "Unknown($phy)" + } + } + + /** + * 启动音频队列处理 + */ + private fun startAudioQueueProcessing() { + audioQueueProcessorThread = Thread { + while (!Thread.currentThread().isInterrupted) { + try { + // 从队列中取出音频数据进行解码 + val audioData = audioDataQueue.take() // 阻塞等待数据 + + opusManager?.writeAudioStream(audioData) + } catch (e: InterruptedException) { + Log.d(TAG, "音频队列处理线程被中断") + break + } catch (e: Exception) { + Log.e(TAG, "音频队列处理异常: ${e.message}", e) + } + } + }.apply { + name = "AudioQueueProcessor" + start() + } + } + } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index d18f07de6..de20dd940 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -361,6 +361,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, // sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") } + override fun onAudioDataReceived1(data: ByteArray) { + // sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") + } + override fun onWakeupSignalReceived() { // 将唤醒事件发送到Flutter sendEvent(statusEventSink, mapOf("type" to "wakeup"), "发送唤醒信号异常") diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 7d7d50bbe..7d344240f 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -403,7 +403,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "openAI对象为null") throw IllegalStateException("OpenAI客户端未初始化") } -// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}") + Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}") val flow = openAI!!.chatCompletions(chatCompletionRequest) // Log.d(TAG, "liwei-------------------------开始AI 对话 6") flow diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt index aa469e62a..a08ed5472 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt @@ -22,7 +22,8 @@ import java.util.concurrent.atomic.AtomicBoolean */ class CustomSseClientTransport( private val client: HttpClient, - private val urlString: String?, + private val serviceidString: String?, + public val urlString: String?, private val reconnectionTime: Duration? = null, private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, private val onConnectionLost: (() -> Unit)? = null @@ -88,7 +89,7 @@ class CustomSseClientTransport( Triple(hostUrl, path, params) } catch (e: Exception) { - Log.e(TAG, "解析URL失败: $url, ${e.message}") + Log.e(TAG, "$serviceidString 解析URL失败: $url, ${e.message}") Triple(url, "", emptyMap()) } } @@ -102,9 +103,9 @@ class CustomSseClientTransport( session.incoming.collect { event -> when (event.event) { "error" -> { - Log.e(TAG, "SSE错误: ${event.data}") + Log.e(TAG, "$serviceidString SSE错误: ${event.data}") isConnected.set(false) - val exception = Exception("SSE Error: ${event.data}") + val exception = Exception("$serviceidString SSE Error: ${event.data}") _onError(exception) onConnectionLost?.invoke() throw exception @@ -112,7 +113,7 @@ class CustomSseClientTransport( "open" -> { // SSE连接已打开 - Log.d(TAG, "SSE连接已打开") + Log.d(TAG, "$serviceidString SSE连接已打开") isConnected.set(true) } "ping" -> { @@ -146,7 +147,7 @@ class CustomSseClientTransport( endpoint.complete(endpointWithParams) } catch (e: Exception) { - Log.e(TAG, "处理endpoint事件失败: ${e.message}", e) + Log.e(TAG, "$serviceidString 处理endpoint事件失败: ${e.message}", e) _onError(e) close() error(e) @@ -161,22 +162,22 @@ class CustomSseClientTransport( val message = json.decodeFromString(data) _onMessage(message) } catch (e: Exception) { - Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e) + Log.e(TAG, "$serviceidString 解析JSON-RPC消息失败: ${e.message}", e) _onError(e) } } } catch (e: Exception) { - Log.e(TAG, "处理事件失败: ${e.message}", e) + Log.e(TAG, "$serviceidString 处理事件失败: ${e.message}", e) _onError(e) } } } } } catch (e: CancellationException) { - Log.d(TAG, "SSE事件收集被取消") + Log.d(TAG, "$serviceidString SSE事件收集被取消") throw e } catch (e: Exception) { - Log.e(TAG, "SSE连接异常断开: ${e.message}", e) + Log.e(TAG, "$serviceidString SSE连接异常断开: ${e.message}", e) isConnected.set(false) _onError(e) onConnectionLost?.invoke() @@ -199,13 +200,13 @@ class CustomSseClientTransport( // 检查session是否仍然活跃 if (session.coroutineContext[Job]?.isCancelled == true) { - Log.w(TAG, "检测到SSE会话已取消") + Log.w(TAG, "$serviceidString 检测到SSE会话已取消") isConnected.set(false) onConnectionLost?.invoke() break } } catch (e: Exception) { - Log.e(TAG, "连接监控异常: ${e.message}", e) + Log.e(TAG, "$serviceidString 连接监控异常: ${e.message}", e) isConnected.set(false) onConnectionLost?.invoke() break @@ -219,7 +220,7 @@ class CustomSseClientTransport( */ override suspend fun start() { if (!initialized.compareAndSet(false, true)) { - Log.e(TAG, "传输层已经启动,不能重复启动") + Log.e(TAG, "$serviceidString 传输层已经启动,不能重复启动") error("CustomSseClientTransport already started!") } @@ -283,12 +284,30 @@ class CustomSseClientTransport( setBody(jsonString) } - if (!response.status.isSuccess()) { - val text = response.bodyAsText() - // Log.e(TAG, "发送消息失败:URL:${urlString} HTTP ${response.status}, $text") + // 优化处理百度API的202状态码 + when { + response.status.isSuccess() -> { + // 2xx状态码都视为成功 + Log.d(TAG, "$serviceidString 消息发送成功: HTTP ${response.status}") + } + response.status == HttpStatusCode.Accepted -> { + // 202 Accepted - 百度API异步处理中,这是正常状态 + Log.d(TAG, "$serviceidString 消息已被接受,正在异步处理: HTTP ${response.status}") + } + else -> { + // 其他非成功状态码才记录为错误 + val text = response.bodyAsText() + Log.w(TAG, "$serviceidString 发送消息收到非成功状态码: URL:${messageEndpoint} HTTP ${response.status}, $text") + + // 根据具体状态码决定是否抛出异常 + if (response.status.value >= 400) { + // 4xx和5xx错误才抛出异常 + throw Exception("HTTP ${response.status}: $text") + } + } } } catch (e: Exception) { - Log.e(TAG, "发送消息异常: ${e.message}", e) + Log.e(TAG, "$serviceidString 发送消息异常: ${e.message}", e) _onError(e) throw e } @@ -306,7 +325,7 @@ class CustomSseClientTransport( */ override suspend fun close() { if (!initialized.get()) { - Log.e(TAG, "关闭失败: 传输层未初始化") + Log.e(TAG, "$serviceidString 关闭失败: 传输层未初始化") error("CustomSseClientTransport is not initialized!") } @@ -317,6 +336,6 @@ class CustomSseClientTransport( job?.cancelAndJoin() connectionMonitorJob?.cancelAndJoin() - Log.d(TAG, "CustomSseClientTransport已关闭") + Log.d(TAG, "$serviceidString CustomSseClientTransport已关闭") } } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt index 20f6796df..022d5d159 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt @@ -68,55 +68,53 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { Log.w(TAG, "Failed to initialize system functions", e) } } - /** - * 初始化音乐函数 - */ - private fun initializeQQMusiceFunctions() { - try { - val handler = QQMusicSystemFunctionHandler(context) - handler.registerAllFunctions(this) - } catch (e: Exception) { - Log.w(TAG, "Failed to initialize system functions", e) - } - } /** * 连接到SSE服务器 * 直接接收完整的JSON配置字符串 - * + * * @param mcpConfigJson 包含mcpServers字段的JSON配置字符串 * @return 是否连接成功 */ suspend fun connectToSSE(mcpConfigJson: String): Boolean { // 清除现有连接 closeAllConnections() - + return try { val config = JSONObject(mcpConfigJson) val mcpServers = config.optJSONObject("mcpServers") ?: return false - + var connectedCount = 0 val serverIds = mcpServers.keys() - + while (serverIds.hasNext()) { val serverId = serverIds.next() val serverConfig = mcpServers.optJSONObject(serverId) ?: continue - + val url = serverConfig.optString("url", "") val tools = serverConfig.optString("tools", "") if (url.isEmpty()) continue - val subClient = MCPSubClient(serverId, url,tools, sharedHttpClient) - - if (subClient.connect()) { - subClients[serverId] = subClient - connectedCount++ - } else { - Log.w(TAG, "Failed to connect to MCP server: $serverId") + + Log.d(TAG, "开始连接MCP服务器: $serverId") + val subClient = MCPSubClient(serverId, url, tools, sharedHttpClient) + + // 使用协程并发连接,但每个服务器都会进行重试 + try { + if (subClient.connect()) { + subClients[serverId] = subClient + connectedCount++ + Log.d(TAG, "MCP服务器连接成功: $serverId") + } else { + Log.w(TAG, "MCP服务器连接失败: $serverId") + } + } catch (e: Exception) { + Log.e(TAG, "MCP服务器连接异常: $serverId, ${e.message}", e) } } - + isConnectedFlag = connectedCount > 0 + Log.d(TAG, "MCP连接完成,成功连接 $connectedCount 个服务器") connectedCount > 0 - + } catch (e: Exception) { Log.e(TAG, "Failed to connect to SSE", e) false diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index 320a208ff..f83f812e6 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt @@ -33,7 +33,7 @@ class MCPSubClient( private var heartbeatJob: Job? = null private val heartbeatInterval = 30000L // 30秒 // 重连配置 - private val maxRetryAttempts = 3 + private val maxRetryAttempts = 10 private val initialReconnectDelay = 1000L // 初始重连延迟1秒 private val maxReconnectDelay = 30000L // 最大重连延迟30秒 private var currentReconnectDelay = initialReconnectDelay @@ -45,95 +45,122 @@ class MCPSubClient( private var isConnected = false private var availableTools = mutableListOf() private var transport: CustomSseClientTransport? = null - + /** * 连接到MCP服务器 */ suspend fun connect(): Boolean = connectionMutex.withLock { - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 1") if (isConnected) return true Log.e(TAG, "[$serverId] 开始连接mcp服务器: $serverUrl") - return try { - // 创建MCP客户端实例 - val client = Client( - clientInfo = Implementation( - name = "deep-voice-chat-api", - version = "1.0.0" + + // 重试连接逻辑 + for (attempt in 0 until maxRetryAttempts) { + try { + Log.d(TAG, "[$serverId] 连接尝试 ${attempt + 1}/$maxRetryAttempts") + + // 创建MCP客户端实例 + val client = Client( + clientInfo = Implementation( + name = "deep-voice-chat-api", + version = "1.0.0" + ) ) - ) - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 2") - // 根据URL类型选择传输方式 - val newTransport = when { - serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> { - // SSE传输 - 使用自定义的CustomSseClientTransport - val mcpHttpClient = httpClient ?: createMcpHttpClient() - CustomSseClientTransport( - client = mcpHttpClient, - urlString = serverUrl, - onConnectionLost = { - // 连接断开回调 - Log.w(TAG, "[$serverId] 检测到连接断开") - scope.launch { - handleConnectionLost() + + // 根据URL类型选择传输方式 + val newTransport = when { + serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> { + // SSE传输 - 使用自定义的CustomSseClientTransport + val mcpHttpClient = httpClient ?: createMcpHttpClient() + CustomSseClientTransport( + client = mcpHttpClient, + serviceidString = serverId, + urlString = serverUrl, + onConnectionLost = { + // 连接断开回调 + Log.w(TAG, "[$serverId] 检测到连接断开") + scope.launch { + handleConnectionLost() + } } - } - ) - } - else -> { - Log.e(TAG, "[$serverId] 不支持的服务器URL格式: $serverUrl") - return false + ) + } + else -> { + Log.e(TAG, "[$serverId] 不支持的服务器URL格式: $serverUrl") + return false + } } - } - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 3") - transport = newTransport - try { - // 连接到服务器 - withTimeout(10000) { // 10秒超时 - client.connect(newTransport) + + transport = newTransport + + // 连接到服务器 - 增加超时时间 + try { + Log.d(TAG, "[$serverId] 尝试建立连接 ${transport?.urlString}") + withTimeout(30000) { // 30秒超时 + client.connect(newTransport) + } + Log.d(TAG, "[$serverId] 连接建立成功") + } catch (e: TimeoutCancellationException) { + Log.w(TAG, "[$serverId] 连接超时 (尝试 ${attempt + 1}/$maxRetryAttempts)") + if (attempt < maxRetryAttempts - 1) { + delay(currentReconnectDelay) + currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay) + continue // 继续下一次重试 + } else { + Log.e(TAG, "[$serverId] 所有连接尝试都超时,连接失败") + return false + } + } catch (e: Exception) { + Log.w(TAG, "[$serverId] 连接异常 (尝试 ${attempt + 1}/$maxRetryAttempts): ${e.message}") + if (attempt < maxRetryAttempts - 1) { + delay(currentReconnectDelay) + currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay) + continue // 继续下一次重试 + } else { + Log.e(TAG, "[$serverId] 所有连接尝试都失败: ${e.message}", e) + return false + } } - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 4") - } catch (e: TimeoutCancellationException) { - Log.e(TAG, "liwei---------Mcp [$serverId] 连接超时") - return false - } catch (e: Exception) { - Log.e(TAG, "liwei---------Mcp [$serverId] 连接异常: ${e.message}", e) - return false - } - - // 获取可用工具列表 - try { - val toolsResult = client.listTools() - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 5") - if (toolsResult != null) { - availableTools.clear() - val filtered = toolsResult.tools.filter { tool -> - filterTools.isEmpty() || filterTools.contains(tool.name) + + // 获取可用工具列表 + try { + val toolsResult = client.listTools() + if (toolsResult != null) { + availableTools.clear() + val allToolNames = toolsResult.tools.map { it.name } + Log.d(TAG, "$serverId:所有工具名称列表: $allToolNames") + val filtered = toolsResult.tools.filter { tool -> + filterTools.isEmpty() || filterTools.contains(tool.name) + } + val filteredToolNames = filtered.map { it.name } + Log.d(TAG, "$serverId: 过滤后的工具: $filteredToolNames") + availableTools.addAll(filtered) } - Log.w(TAG, "[$serverId] [${filterTools}] 获取工具列表: ${filtered} 原始列表:${toolsResult.tools}") - availableTools.addAll(filtered) - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 6") + } catch (e: Exception) { + Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}") + // 即使获取工具失败,连接也可能是成功的 } + + mcpClient = client + isConnected = true + retryCount = 0 + currentReconnectDelay = initialReconnectDelay + + Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl") + return true + } catch (e: Exception) { - Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}") - // 即使获取工具失败,连接也可能是成功的 + Log.w(TAG, "[$serverId] 连接尝试 ${attempt + 1} 失败: ${e.message}") + if (attempt < maxRetryAttempts - 1) { + delay(currentReconnectDelay) + currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay) + } } - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 7") - mcpClient = client - isConnected = true - - retryCount = 0 - currentReconnectDelay = initialReconnectDelay - // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 8") - // 启动心跳检测 -// startHeartbeat() - Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl") - true - } catch (e: Exception) { - Log.e(TAG, "[$serverId] MCP连接失败: ${e.message}", e) - false } + + Log.e(TAG, "[$serverId] 所有重试尝试都失败,MCP连接失败") + return false } - + /** * 检查是否包含指定工具 */ diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 462bf7401..00c95ad79 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -19,7 +19,6 @@ import android.telephony.SmsManager import android.util.Log import androidx.core.content.ContextCompat import com.yunqiinnovation.location_service.LocationService -import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import org.json.JSONObject import java.text.SimpleDateFormat import java.util.* @@ -1134,7 +1133,7 @@ private class GetCurrentTimeHandler : FunctionHandler { * 获取当前位置处理器 */ private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler { - + override suspend fun handle(arguments: Map): String { if (context == null) { return JSONObject().apply { @@ -1144,75 +1143,28 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio }) }.toString() } - - val handler = context.let { SystemFunctionHandler(it) } - - // 检查位置权限 - if (!handler.checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) { - return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "permission_denied") - put("message", "需要位置权限才能获取当前位置,请在权限管理中开启位置权限") - }) - }.toString() - } - - // 获取缓存的位置数据 - val cachedLocation = LocationService.getCachedLocationData() - - // 异步触发最新定位获取(不等待结果) - LocationService.requestLocationInBackground(object : LocationService.LocationCallback { - override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) { - Log.d(SystemFunctionHandler.TAG, "后台定位成功,缓存已更新: $latitude, $longitude, $address") - } - - override fun onLocationError(errorCode: Int, errorMessage: String) { - Log.w(SystemFunctionHandler.TAG, "后台定位失败: $errorCode - $errorMessage") - } - }) - - // 立即返回缓存数据(如果存在) - if (cachedLocation != null) { - val cacheAge = System.currentTimeMillis() - cachedLocation.timestamp - val cacheAgeMinutes = cacheAge / (60 * 1000) - - return JSONObject().apply { - put("result", JSONObject().apply { - put("latitude", cachedLocation.latitude) - put("longitude", cachedLocation.longitude) - put("address", cachedLocation.address) - put("source", "cache") - put("timestamp", cachedLocation.timestamp) - put("cacheAgeMinutes", cacheAgeMinutes) - put("note", "正在后台更新位置信息") - }) - }.toString() - } - // 如果没有缓存,则同步获取一次 return try { kotlinx.coroutines.suspendCancellableCoroutine { continuation -> LocationService.requestLocation(object : LocationService.LocationCallback { - override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) { - Log.d(SystemFunctionHandler.TAG, "首次定位成功: $latitude, $longitude, $address") - + override fun onLocationSuccess(latitude: Double, longitude: Double) { + Log.d(SystemFunctionHandler.TAG, "首次定位成功: $latitude, $longitude") + val result = JSONObject().apply { put("result", JSONObject().apply { - put("latitude", latitude) put("longitude", longitude) - put("address", address) - put("source", "gps") + put("latitude", latitude) put("timestamp", System.currentTimeMillis()) - put("note", "首次获取位置信息") + }) }.toString() - + continuation.resumeWith(Result.success(result)) } override fun onLocationError(errorCode: Int, errorMessage: String) { Log.e(SystemFunctionHandler.TAG, "首次定位失败: $errorCode - $errorMessage") - + val result = JSONObject().apply { put("error", JSONObject().apply { put("code", "location_error") @@ -1220,7 +1172,7 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio put("errorCode", errorCode) }) }.toString() - + continuation.resumeWith(Result.success(result)) } }) diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index c6a35e433..35d553698 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -461,6 +461,7 @@ private class GetCurrentLocationHandler: FunctionHandler { // 解析位置数据 guard let latStr = locationInfo["user_lat"] as? String, let lngStr = locationInfo["user_lng"] as? String, + let latitude = Double(latStr), let longitude = Double(lngStr) else { print("[GetCurrentLocationHandler] 位置数据格式错误") @@ -489,8 +490,9 @@ private class GetCurrentLocationHandler: FunctionHandler { let result: [String: Any] = [ "success": true, "data": [ - "latitude": latitude, "longitude": longitude, + "latitude": latitude, + "location": "\(longitude),\(latitude)", "timestamp": Date().timeIntervalSince1970 * 1000 ] ] diff --git a/local_plugins/location_service/android/src/main/AndroidManifest.xml b/local_plugins/location_service/android/src/main/AndroidManifest.xml index 3cbeb31e4..48fb949c7 100644 --- a/local_plugins/location_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/location_service/android/src/main/AndroidManifest.xml @@ -1,7 +1,12 @@ + + + - + + + diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt index 73788b194..bf217f258 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt @@ -16,30 +16,34 @@ import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.json.JSONObject + object LocationService { private const val TAG = "LocationService" private const val PERMISSION_REQUEST_CODE = 1001 + // 只接受高精度定位 + private const val HIGH_ACCURACY_THRESHOLD = 50.0 // 只接受50米以内的高精度定位 + private const val LOCATION_TIMEOUT_MS = 60000L // 增加到60秒超时,给更多时间获取高精度定位 + private const val CACHE_PREFS_NAME = "location_cache" + private var appcontext: Context? = null private var locationClient: AMapLocationClient? = null private var initialized = false private var permissionRetryHandler: Handler? = null private var permissionRetryRunnable: Runnable? = null - - // 存储当前的定位请求回调(Flutter用) private var currentLocationCallback: MethodChannel.Result? = null - // 存储权限请求回调(Flutter用) private var permissionRequestCallback: MethodChannel.Result? = null - - // 存储服务间调用的回调 private val serviceCallbacks = mutableListOf() + private var isHighAccuracyMode = true + private var cachedLocationData: LocationData? = null + private var locationTimeoutHandler: Handler? = null + private var locationStartTime: Long = 0 - /** - * 定位回调接口,供其他Android服务使用 - */ + // 修正接口,包含address参数 interface LocationCallback { - fun onLocationSuccess(latitude: Double, longitude: Double, address: String) + fun onLocationSuccess(latitude: Double, longitude: Double) fun onLocationError(errorCode: Int, errorMessage: String) } @@ -65,147 +69,165 @@ object LocationService { } fun initialize(context: Context) { - if (initialized) return - appcontext = context - - if (!checkPermissions(context)) { - Log.w(TAG, "没有定位权限,等待用户授权中...") - startPermissionMonitor() + if (initialized) { + Log.d(TAG, "定位服务已初始化") return } - + + appcontext = context.applicationContext + try { AMapLocationClient.updatePrivacyShow(context, true, true) AMapLocationClient.updatePrivacyAgree(context, true) - locationClient = AMapLocationClient(context.applicationContext) - val option = AMapLocationClientOption().apply { - locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy - isOnceLocation = true // 单次定位 - isNeedAddress = true // 需要地址信息 - httpTimeOut = 20000 // 设置超时时间 - } - + + // 优化后的定位参数配置 + val option = AMapLocationClientOption() + + // 高精度定位模式(融合GPS、网络、传感器) + option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy) + + // 设置定位场景 + option.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.SignIn) // 改为签到模式,更适合高精度定位 + + // 定位间隔(首次定位建议设置较短间隔) + option.setInterval(2000) // 改为2秒,更快响应 + + // 超时时间 + option.setHttpTimeOut(30000) // 30秒足够 + + // 启用所有辅助定位技术 + option.setGpsFirst(false) // 不强制GPS优先,让SDK智能选择 + option.setWifiScan(true) // 启用WiFi辅助定位 + option.setSensorEnable(true) // 启用传感器辅助 + option.setLocationCacheEnable(true) // 启用位置缓存 + + // 单次定位设置 + option.setOnceLocation(true) // 改为单次定位,避免持续定位的干扰 + option.setOnceLocationLatest(true) // 获取最新的定位结果 + + // 其他设置 + option.setMockEnable(false) // 禁用模拟定位 + option.setNeedAddress(true) // 启用地址解析,有助于定位精度 + locationClient?.setLocationOption(option) - - // 设置定位监听器 locationClient?.setLocationListener { location -> handleLocationResult(location) } - + initialized = true - Log.d(TAG, "定位服务初始化完成") + //首次请求一次定位 + requestLocation(object : LocationCallback { + override fun onLocationSuccess(latitude: Double, longitude: Double) { + } + + override fun onLocationError(errorCode: Int, errorMessage: String) { + } + }) + + Log.d(TAG, "定位服务初始化完成 - 优化高精度模式") } catch (e: Exception) { Log.e(TAG, "高德定位初始化失败: ${e.message}", e) } } - // 在LocationService object中添加缓存相关的属性和方法 - private var cachedLocationData: LocationData? = null - private const val CACHE_PREFS_NAME = "location_cache" - /** * 后台异步获取位置信息(不阻塞调用者) * @param callback 定位结果回调 */ - fun requestLocationInBackground(callback: LocationCallback) { + fun requestLocation(callback: LocationCallback) { val ctx = appcontext if (ctx == null) { callback.onLocationError(-1, "定位服务未初始化") return } - + if (!checkPermissions(ctx)) { callback.onLocationError(-2, "没有定位权限") return } - + if (!initialized) { callback.onLocationError(-3, "定位服务未初始化") return } - + + // 优先返回内存中的缓存 + if (cachedLocationData != null) { + callback.onLocationSuccess(cachedLocationData!!.latitude,cachedLocationData!!.longitude) + }else{ + // 添加到回调列表 + serviceCallbacks.add(callback) + } + // 记录定位开始时间 + locationStartTime = System.currentTimeMillis() + // 启动超时计时器 + startLocationTimeout() // 使用协程在后台执行 CoroutineScope(Dispatchers.IO).launch { try { - // 添加到回调列表 - serviceCallbacks.add(callback) - // 如果没有正在进行的定位请求,开始定位 - if (currentLocationCallback == null && serviceCallbacks.size == 1) { - locationClient?.startLocation() - } + locationClient?.startLocation() } catch (e: Exception) { + stopLocationTimeout() callback.onLocationError(-4, "启动后台定位失败: ${e.message}") } } } - - // 修改handleLocationResult方法,添加缓存逻辑 + + // 统一的handleLocationResult方法(整合优化逻辑) private fun handleLocationResult(location: AMapLocation?) { - // 处理Flutter回调 - val flutterCallback = currentLocationCallback - currentLocationCallback = null - - if (location != null && location.errorCode == 0) { - Log.d(TAG, "定位成功: ${location.latitude}, ${location.longitude}") - - // 缓存位置数据 - val locationData = LocationData( - latitude = location.latitude, - longitude = location.longitude, - address = location.address ?: "未知地址", - timestamp = System.currentTimeMillis() - ) - cachedLocationData = locationData - saveCachedLocationToPrefs(locationData) - - // 通知Flutter - if (flutterCallback != null) { - val result = mapOf( - "user_location" to (location.address ?: "未知地址"), - "user_lat" to location.latitude.toString(), - "user_lng" to location.longitude.toString() - ) - flutterCallback.success(result) - } + if (location == null) { + Log.e(TAG, "定位结果为空") + return + } + + Log.d(TAG, "定位结果: 来源=${location.provider}, 精度=${location.accuracy}米, 坐标=(${location.latitude}, ${location.longitude})") + if (location.errorCode == AMapLocation.LOCATION_SUCCESS) { + val accuracy = location.accuracy + // Log.d(TAG, "GPS定位成功: 精度=${accuracy}米") - // 通知其他Android服务 - serviceCallbacks.forEach { callback -> + // 只接受高精度GPS定位 + // if (accuracy <= HIGH_ACCURACY_THRESHOLD) { + Log.d(TAG, "接受高精度GPS定位: ${accuracy}米") + + // 停止超时计时器 + stopLocationTimeout() + + // 停止定位 try { - callback.onLocationSuccess( - location.latitude, - location.longitude, - location.address ?: "未知地址" - ) + locationClient?.stopLocation() } catch (e: Exception) { - Log.e(TAG, "服务回调失败: ${e.message}", e) + Log.e(TAG, "停止定位失败: ${e.message}", e) } - } - - } else { - Log.w(TAG, "定位失败: ${location?.errorCode} - ${location?.errorInfo}") - - // 通知Flutter - if (flutterCallback != null) { - flutterCallback.error("LOCATION_ERROR", "定位失败: ${location?.errorInfo ?: "未知错误"}", null) - } - - // 通知其他Android服务 - serviceCallbacks.forEach { callback -> - try { - callback.onLocationError( - location?.errorCode ?: -1, - location?.errorInfo ?: "未知错误" - ) - } catch (e: Exception) { - Log.e(TAG, "服务回调失败: ${e.message}", e) + + // 缓存位置数据 + val locationData = LocationData( + latitude = location.latitude, + longitude = location.longitude, + timestamp = System.currentTimeMillis() + ) + cachedLocationData = locationData + + // 处理成功的定位结果 + serviceCallbacks.forEach { callback -> + try { + callback.onLocationSuccess( + location.latitude, + location.longitude, + ) + } catch (e: Exception) { + Log.e(TAG, "服务回调失败: ${e.message}", e) + } } - } + // 清空服务回调列表(单次定位) + serviceCallbacks.clear() + // } else { + // Log.w(TAG, "GPS精度不足(${accuracy}米),继续等待更高精度定位") + // } + } else { + Log.e(TAG, "GPS定位失败: ${location.errorCode} - ${location.errorInfo}") } - - // 清空服务回调列表(单次定位) - serviceCallbacks.clear() } private fun startPermissionMonitor() { @@ -315,182 +337,56 @@ object LocationService { } } - /** - * 处理权限请求结果(Flutter用) - */ - fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ): Boolean { - if (requestCode != PERMISSION_REQUEST_CODE) { - return false - } - - val callback = permissionRequestCallback - permissionRequestCallback = null - - if (callback == null) { - return true - } - - val ctx = appcontext - if (ctx == null) { - callback.error("NO_CONTEXT", "上下文丢失", null) - return true - } - - val hasPermission = checkPermissions(ctx) - - if (hasPermission) { - // 权限获取成功,重新初始化定位服务 - if (!initialized) { - initialize(ctx) - } - callback.success(mapOf("granted" to true)) - } else { - callback.success(mapOf("granted" to false)) - } - - return true - } /** - * 按需获取当前位置(Flutter用) + * 位置数据类 */ - fun getCurrentLocation(result: MethodChannel.Result) { - val ctx = appcontext - if (ctx == null) { - result.error("NOT_INITIALIZED", "定位服务未初始化", null) - return - } - - if (!checkPermissions(ctx)) { - result.error("NO_PERMISSION", "需要定位权限", null) - return - } - - if (!initialized) { - result.error("NOT_INITIALIZED", "定位服务未初始化", null) - return - } - - if (currentLocationCallback != null) { - result.error("LOCATION_IN_PROGRESS", "正在定位中,请稍后再试", null) - return - } - - // 保存回调,开始定位 - currentLocationCallback = result - locationClient?.startLocation() - } + data class LocationData( + val latitude: Double, + val longitude: Double, + val timestamp: Long + ) - // ========== 以下是供其他Android服务使用的接口 ========== - - /** - * 检查是否有定位权限(供其他服务使用) - */ - fun hasLocationPermission(): Boolean { - val ctx = appcontext ?: return false - return checkPermissions(ctx) - } - - /** - * 获取当前位置(供其他Android服务使用) - * @param callback 定位结果回调 - */ - fun requestLocation(callback: LocationCallback) { - val ctx = appcontext - if (ctx == null) { - callback.onLocationError(-1, "定位服务未初始化") - return - } - - if (!checkPermissions(ctx)) { - callback.onLocationError(-2, "没有定位权限") - return - } - - if (!initialized) { - callback.onLocationError(-3, "定位服务未初始化") - return - } - - // 添加到回调列表 - serviceCallbacks.add(callback) - - // 如果没有正在进行的定位请求,开始定位 - if (currentLocationCallback == null && serviceCallbacks.size == 1) { - locationClient?.startLocation() - } - } - /** - * 同步获取缓存的位置信息(供其他服务使用) - * 注意:这个方法返回的可能是过期的位置信息 + * 启动定位超时计时器 */ - fun getCachedLocationData(): LocationData? { - // 优先返回内存中的缓存 - if (cachedLocationData != null) { - return cachedLocationData - } + private fun startLocationTimeout() { + // 如果已经有超时计时器在运行,先停止它 + stopLocationTimeout() - // 从SharedPreferences中读取缓存 - val ctx = appcontext ?: return null - return loadCachedLocationFromPrefs(ctx) - } - - private fun saveCachedLocationToPrefs(locationData: LocationData) { - val ctx = appcontext ?: return - try { - val prefs = ctx.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE) - prefs.edit().apply { - putString("latitude", locationData.latitude.toString()) - putString("longitude", locationData.longitude.toString()) - putString("address", locationData.address) - putLong("timestamp", locationData.timestamp) - apply() + locationTimeoutHandler = Handler(Looper.getMainLooper()) + locationTimeoutHandler?.postDelayed({ + Log.w(TAG, "定位超时,停止定位请求") + + // 停止定位 + try { + locationClient?.stopLocation() + } catch (e: Exception) { + Log.e(TAG, "停止定位失败: ${e.message}", e) } - } catch (e: Exception) { - Log.e(TAG, "保存位置缓存失败: ${e.message}", e) - } - } - - private fun loadCachedLocationFromPrefs(context: Context): LocationData? { - return try { - val prefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE) - val latitude = prefs.getString("latitude", null)?.toDoubleOrNull() ?: return null - val longitude = prefs.getString("longitude", null)?.toDoubleOrNull() ?: return null - val address = prefs.getString("address", null) ?: return null - val timestamp = prefs.getLong("timestamp", 0L) - if (timestamp == 0L) return null + // 通知所有等待的回调超时错误 + serviceCallbacks.forEach { callback -> + try { + callback.onLocationError(-5, "定位超时,未能在${LOCATION_TIMEOUT_MS/1000}秒内获取到高精度位置") + } catch (e: Exception) { + Log.e(TAG, "超时回调失败: ${e.message}", e) + } + } - LocationData(latitude, longitude, address, timestamp) - } catch (e: Exception) { - Log.e(TAG, "读取位置缓存失败: ${e.message}", e) - null - } + // 清空回调列表 + serviceCallbacks.clear() + + // 清理超时处理器 + locationTimeoutHandler = null + }, LOCATION_TIMEOUT_MS) } /** - * 位置数据类 + * 停止定位超时计时器 */ - data class LocationData( - val latitude: Double, - val longitude: Double, - val address: String, - val timestamp: Long - ) - - fun stop() { - locationClient?.stopLocation() - locationClient?.onDestroy() - locationClient = null - currentLocationCallback = null - permissionRequestCallback = null - serviceCallbacks.clear() - initialized = false - stopPermissionMonitor() + private fun stopLocationTimeout() { + locationTimeoutHandler?.removeCallbacksAndMessages(null) + locationTimeoutHandler = null } } diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt index 3c90cfabc..21bd7184f 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt @@ -12,11 +12,10 @@ import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry /** LocationServicePlugin */ -class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.RequestPermissionsResultListener { +class LocationServicePlugin: FlutterPlugin, MethodCallHandler { private val TAG = "LocationServicePlugin" private lateinit var context: Context private lateinit var channel : MethodChannel - private var activityBinding: ActivityPluginBinding? = null override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext @@ -25,43 +24,12 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Pl } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - // Log.d(TAG, "Method call: ${call.method}") - when (call.method) { - "location" -> LocationService.getCurrentLocation(result) - "requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result) - "checkPermission" -> LocationService.checkLocationPermission(context, result) - else -> result.notImplemented() - } + Log.d(TAG, "Method call: ${call.method}") + } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addRequestPermissionsResultListener(this) - LocationService.initialize(context) - } - - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } - - override fun onDetachedFromActivity() { - activityBinding?.removeRequestPermissionsResultListener(this) - activityBinding = null - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ): Boolean { - return LocationService.onRequestPermissionsResult(requestCode, permissions, grantResults) - } } diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/NativeLocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/NativeLocationService.kt new file mode 100644 index 000000000..af4064134 --- /dev/null +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/NativeLocationService.kt @@ -0,0 +1,499 @@ +package com.yunqiinnovation.location_service + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.location.* +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.* + +object NativeLocationService { + private const val TAG = "NativeLocationService" + private const val PERMISSION_REQUEST_CODE = 1002 + + // 高精度定位阈值 + private const val HIGH_ACCURACY_THRESHOLD = 50.0f // 只接受50米以内的高精度定位 + private const val LOCATION_TIMEOUT_MS = 60000L // 60秒超时 + private const val MIN_TIME_BETWEEN_UPDATES = 5000L // 5秒最小更新间隔 + private const val MIN_DISTANCE_CHANGE_FOR_UPDATES = 10f // 10米最小距离变化 + + private var appContext: Context? = null + private var locationManager: LocationManager? = null + private var initialized = false + private val serviceCallbacks = mutableListOf() + private var cachedLocationData: LocationData? = null + private var locationTimeoutHandler: Handler? = null + private var locationStartTime: Long = 0 + private var gpsLocationListener: LocationListener? = null + private var networkLocationListener: LocationListener? = null + + // 定位回调接口 + interface LocationCallback { + fun onLocationSuccess(latitude: Double, longitude: Double) + fun onLocationError(errorCode: Int, errorMessage: String) + } + + /** + * 初始化原生定位服务 + */ + fun initialize(context: Context) { + if (initialized) { + Log.d(TAG, "原生定位服务已初始化") + return + } + + appContext = context.applicationContext + locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + if (locationManager == null) { + Log.e(TAG, "无法获取LocationManager") + return + } + + initialized = true + Log.d(TAG, "原生定位服务初始化完成") + } + + /** + * 检查GPS是否可用 + */ + private fun isGpsEnabled(): Boolean { + return locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) ?: false + } + + /** + * 检查网络定位是否可用 + */ + private fun isNetworkEnabled(): Boolean { + return locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ?: false + } + + /** + * 获取位置信息(异步) + */ + fun requestLocation(callback: LocationCallback) { + val ctx = appContext + if (ctx == null) { + callback.onLocationError(-1, "定位服务未初始化") + return + } + + if (!checkPermissions(ctx)) { + callback.onLocationError(-2, "没有定位权限") + return + } + + if (!initialized) { + callback.onLocationError(-3, "定位服务未初始化") + return + } + + // 检查定位服务是否可用 + if (!isGpsEnabled() && !isNetworkEnabled()) { + callback.onLocationError(-4, "GPS和网络定位都不可用,请检查设置") + return + } + + // 优先返回缓存的位置 + cachedLocationData?.let { cached -> + val currentTime = System.currentTimeMillis() + // 如果缓存时间在5分钟内,直接返回 + if (currentTime - cached.timestamp < 5 * 60 * 1000) { + callback.onLocationSuccess(cached.latitude, cached.longitude) + return + } + } + + // 添加到回调列表 + serviceCallbacks.add(callback) + + // 记录定位开始时间 + locationStartTime = System.currentTimeMillis() + + // 启动超时计时器 + startLocationTimeout() + + // 使用协程在后台执行定位 + CoroutineScope(Dispatchers.IO).launch { + try { + startLocationUpdates() + } catch (e: Exception) { + stopLocationTimeout() + callback.onLocationError(-5, "启动定位失败: ${e.message}") + } + } + } + + /** + * 开始位置更新 + */ + private fun startLocationUpdates() { + val ctx = appContext ?: return + val locManager = locationManager ?: return + + // 检查权限 + if (ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + ctx, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.e(TAG, "没有定位权限") + return + } + + Log.d(TAG, "=== 启动混合定位模式 ===") + Log.d(TAG, "网络定位可用: ${isNetworkEnabled()}") + Log.d(TAG, "GPS定位可用: ${isGpsEnabled()}") + + // 优先启动网络定位 + if (isNetworkEnabled()) { + Log.d(TAG, "🌐 启动网络定位(优先)") + + networkLocationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + Log.d(TAG, "🌐 网络定位更新: 精度=${location.accuracy}米") + handleLocationResult(location, "NETWORK") + } + + override fun onProviderEnabled(provider: String) { + Log.d(TAG, "网络提供者已启用: $provider") + } + + override fun onProviderDisabled(provider: String) { + Log.d(TAG, "网络提供者已禁用: $provider") + } + + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { + Log.d(TAG, "网络状态变化: $provider, status: $status") + } + } + + locManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + MIN_TIME_BETWEEN_UPDATES, + MIN_DISTANCE_CHANGE_FOR_UPDATES, + networkLocationListener!!, + Looper.getMainLooper() + ) + Log.d(TAG, "✅ 网络定位已启动") + } + + // GPS作为补充定位 + if (isGpsEnabled()) { + Log.d(TAG, "🛰️ 启动GPS定位(补充)") + + gpsLocationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + Log.d(TAG, "🛰️ GPS定位更新: 精度=${location.accuracy}米") + handleLocationResult(location, "GPS") + } + + override fun onProviderEnabled(provider: String) { + Log.d(TAG, "GPS提供者已启用: $provider") + } + + override fun onProviderDisabled(provider: String) { + Log.d(TAG, "GPS提供者已禁用: $provider") + } + + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { + when (status) { + LocationProvider.AVAILABLE -> Log.d(TAG, "GPS状态: 可用") + LocationProvider.OUT_OF_SERVICE -> Log.d(TAG, "GPS状态: 服务中断") + LocationProvider.TEMPORARILY_UNAVAILABLE -> Log.d(TAG, "GPS状态: 暂时不可用") + } + } + } + + locManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + MIN_TIME_BETWEEN_UPDATES, + MIN_DISTANCE_CHANGE_FOR_UPDATES, + gpsLocationListener!!, + Looper.getMainLooper() + ) + Log.d(TAG, "✅ GPS定位已启动") + } else { + Log.w(TAG, "GPS未启用,仅使用网络定位") + } + + if (!isNetworkEnabled() && !isGpsEnabled()) { + Log.e(TAG, "❌ 所有定位服务都不可用") + serviceCallbacks.forEach { callback -> + callback.onLocationError(-6, "定位服务不可用") + } + } + } + + /** + * 处理定位结果 + */ + // 修改handleLocationResult方法,对网络定位进行更细致的处理 + private fun handleLocationResult(location: Location, provider: String) { + Log.d(TAG, "定位结果: 来源=$provider, 精度=${location.accuracy}米, 坐标=(${location.latitude}, ${location.longitude})") + + // 混合定位策略:优先接受可用的定位结果 + when (provider) { + "NETWORK" -> { + // 网络定位优先策略 + when { + location.accuracy <= 200f -> { + Log.d(TAG, "✅ 接受网络定位: ${location.accuracy}米") + processLocationSuccess(location) + } + location.accuracy <= 1000f -> { + Log.d(TAG, "⚠️ 接受中等精度网络定位: ${location.accuracy}米") + // 短暂等待,看是否有更好的定位 + val elapsedTime = System.currentTimeMillis() - locationStartTime + if (elapsedTime > 5000) { // 5秒后接受 + processLocationSuccess(location) + } else { + Log.d(TAG, "等待更高精度定位中...") + } + } + location.accuracy <= 3000f -> { + Log.w(TAG, "⚠️ 网络定位精度较低: ${location.accuracy}米") + // 10秒后接受低精度定位 + val elapsedTime = System.currentTimeMillis() - locationStartTime + if (elapsedTime > 10000) { + Log.d(TAG, "接受低精度网络定位") + processLocationSuccess(location) + } + } + else -> { + Log.w(TAG, "网络定位精度过低(${location.accuracy}米),继续等待") + } + } + } + "GPS" -> { + // GPS作为高精度补充 + if (location.accuracy <= HIGH_ACCURACY_THRESHOLD) { + Log.d(TAG, "✅ 接受高精度GPS定位: ${location.accuracy}米") + processLocationSuccess(location) + } else if (location.accuracy <= HIGH_ACCURACY_THRESHOLD * 2) { + Log.d(TAG, "✅ 接受GPS定位: ${location.accuracy}米") + processLocationSuccess(location) + } else { + Log.w(TAG, "GPS定位精度不足(${location.accuracy}米),继续等待") + } + } + } + } + + /** + * 处理定位成功 + */ + private fun processLocationSuccess(location: Location) { + // 停止超时计时器 + stopLocationTimeout() + + // 停止定位更新 + stopLocationUpdates() + + // 获取地址信息(可选) + val address = getAddressFromLocation(location) + + // 缓存位置数据 + val locationData = LocationData( + latitude = location.latitude, + longitude = location.longitude, + timestamp = System.currentTimeMillis() + ) + cachedLocationData = locationData + + // 处理成功的定位结果 + serviceCallbacks.forEach { callback -> + try { + callback.onLocationSuccess( + location.latitude, + location.longitude + ) + } catch (e: Exception) { + Log.e(TAG, "服务回调失败: ${e.message}", e) + } + } + + // 清空服务回调列表 + serviceCallbacks.clear() + } + + /** + * 停止定位更新 + */ + private fun stopLocationUpdates() { + val locManager = locationManager ?: return + + gpsLocationListener?.let { + locManager.removeUpdates(it) + gpsLocationListener = null + } + + networkLocationListener?.let { + locManager.removeUpdates(it) + networkLocationListener = null + } + + Log.d(TAG, "已停止定位更新") + } + + /** + * 从位置获取地址信息 + */ + private fun getAddressFromLocation(location: Location): String { + return try { + val ctx = appContext ?: return "" + val geocoder = Geocoder(ctx, Locale.getDefault()) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // Android 13+ 使用新的异步API + var addressResult = "" + geocoder.getFromLocation(location.latitude, location.longitude, 1) { addresses -> + if (addresses.isNotEmpty()) { + addressResult = addresses[0].getAddressLine(0) ?: "" + } + } + addressResult + } else { + // 旧版本使用同步API + @Suppress("DEPRECATION") + val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1) + if (!addresses.isNullOrEmpty()) { + addresses[0].getAddressLine(0) ?: "" + } else { + "" + } + } + } catch (e: Exception) { + Log.w(TAG, "获取地址失败: ${e.message}") + "" + } + } + + /** + * 检查定位权限 + */ + private fun checkPermissions(context: Context): Boolean { + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } + + /** + * 检查精确定位权限 + */ + private fun hasFineLocationPermission(context: Context): Boolean { + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } + + /** + * 检查粗略定位权限 + */ + private fun hasCoarseLocationPermission(context: Context): Boolean { + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } + + /** + * 检查后台定位权限 + */ + private fun hasBackgroundLocationPermission(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_BACKGROUND_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } else { + true // Android 9 及以下没有这个权限 + } + } + + /** + * 位置数据类 + */ + data class LocationData( + val latitude: Double, + val longitude: Double, + val timestamp: Long + ) + + /** + * 启动定位超时计时器 + */ + private fun startLocationTimeout() { + // 如果已经有超时计时器在运行,先停止它 + stopLocationTimeout() + + locationTimeoutHandler = Handler(Looper.getMainLooper()) + locationTimeoutHandler?.postDelayed({ + Log.w(TAG, "定位超时,停止定位请求") + + // 停止定位 + stopLocationUpdates() + + // 通知所有等待的回调超时错误 + serviceCallbacks.forEach { callback -> + try { + callback.onLocationError(-6, "定位超时,未能在${LOCATION_TIMEOUT_MS/1000}秒内获取到位置") + } catch (e: Exception) { + Log.e(TAG, "超时回调失败: ${e.message}", e) + } + } + + // 清空回调列表 + serviceCallbacks.clear() + + // 清理超时处理器 + locationTimeoutHandler = null + }, LOCATION_TIMEOUT_MS) + } + + /** + * 停止定位超时计时器 + */ + private fun stopLocationTimeout() { + locationTimeoutHandler?.removeCallbacksAndMessages(null) + locationTimeoutHandler = null + } + + /** + * 清理资源 + */ + fun cleanup() { + stopLocationUpdates() + stopLocationTimeout() + serviceCallbacks.clear() + cachedLocationData = null + initialized = false + Log.d(TAG, "原生定位服务已清理") + } +} + +// 调整精度阈值,更适合混合定位 +private const val HIGH_ACCURACY_THRESHOLD = 100.0f // 放宽到100米 +private const val LOCATION_TIMEOUT_MS = 30000L // 缩短到30秒 +private const val MIN_TIME_BETWEEN_UPDATES = 2000L // 2秒更新 +private const val MIN_DISTANCE_CHANGE_FOR_UPDATES = 5f // 5米距离变化 \ No newline at end of file diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift index 5facd02e0..221a1ed56 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift @@ -2,15 +2,15 @@ import Foundation /// 位置数据结构 public struct LocationData: Codable { - public let latitude: Double public let longitude: Double + public let latitude: Double public let timestamp: TimeInterval - public let latitudeandlongitude: String + public let location: String - public init(latitude: Double, longitude: Double,timestamp: TimeInterval) { - self.latitude = latitude + public init( longitude: Double,latitude: Double,timestamp: TimeInterval) { self.longitude = longitude + self.latitude = latitude self.timestamp = timestamp - self.latitudeandlongitude = "\(latitude),\(longitude)" + self.location = "\(longitude),\(latitude)" } } diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift index 906789434..faccc1834 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift @@ -23,37 +23,33 @@ public class LocationService: NSObject { private var authorizationStatus: CLAuthorizationStatus = .notDetermined private var isLocationServicesEnabled: Bool = false + // 高精度定位属性 + private var locationAttempts: Int = 0 + private let maxLocationAttempts: Int = 3 + private let requiredAccuracy: CLLocationAccuracy = 10.0 // 要求精度在10米以内 + private var bestLocation: CLLocation? + private var locationTimer: Timer? + + // MARK: - 权限回调 + private var permissionCallback: ((Result) -> Void)? + private override init() { super.init() setupLocationManager() } + private func setupLocationManager() { DispatchQueue.main.async { [weak self] in self?.locationManager = CLLocationManager() self?.locationManager?.delegate = self - self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest - self?.locationManager?.distanceFilter = 10 - // 不需要手动获取状态,delegate 方法会自动被调用 + // 高精度定位设置 + self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBestForNavigation // 最高精度 + self?.locationManager?.distanceFilter = 1 // 降低距离过滤器,更敏感 + print("[LocationService] LocationManager 初始化完成,等待权限状态回调") } } - // private func setupLocationManager() { - // DispatchQueue.main.async { [weak self] in - // self?.locationManager = CLLocationManager() - // self?.locationManager?.delegate = self - // self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest - // self?.locationManager?.distanceFilter = 10 - - // // 初始化时获取当前状态 - // self?.authorizationStatus = CLLocationManager.authorizationStatus() - // self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled() - - // print("[LocationService] LocationManager 初始化完成") - // print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)") - // print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)") - // } - // } /// 获取当前位置 - 主要接口 public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { @@ -98,20 +94,14 @@ public class LocationService: NSObject { /// 异步更新位置缓存 private func asyncUpdateLocation() { // 检查权限 - guard checkLocationPermission() else { - print("[LocationService] 异步更新失败:无位置权限") - return - } - - // 异步请求位置更新 - DispatchQueue.global(qos: .background).async { [weak self] in - self?.requestLocationUpdate { result in + if checkLocationPermission() { + requestHighAccuracyLocation { [weak self] result in switch result { case .success(let locationData): - print("[LocationService] 异步更新位置缓存成功") self?.cachedLocationData = locationData + print("[LocationService] 异步更新缓存成功") case .failure(let error): - print("[LocationService] 异步更新位置缓存失败: \(error.localizedDescription)") + print("[LocationService] 异步更新缓存失败: \(error.localizedDescription)") } } } @@ -119,14 +109,22 @@ public class LocationService: NSObject { /// 同步获取位置 private func syncGetLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { - // 检查权限 - guard checkLocationPermission() else { - print("[LocationService] 无位置权限,尝试请求权限") + if checkLocationPermission() { + requestHighAccuracyLocation { [weak self] result in + switch result { + case .success(let locationData): + self?.cachedLocationData = locationData + let locationInfo = self?.formatLocationData(locationData) ?? [:] + completion(.success(locationInfo)) + case .failure(let error): + completion(.failure(error)) + } + } + } else { requestLocationPermission { [weak self] result in switch result { case .success(let granted): if granted { - print("[LocationService] 权限获取成功,重新获取位置") self?.syncGetLocation(completion: completion) } else { completion(.failure(NSError(domain: "LocationService", code: -1, userInfo: [NSLocalizedDescriptionKey: "位置权限被拒绝"]))) @@ -135,164 +133,152 @@ public class LocationService: NSObject { completion(.failure(error)) } } - return - } - - // 有权限,直接获取位置 - requestLocationUpdate { [weak self] result in - switch result { - case .success(let locationData): - print("[LocationService] 同步获取位置成功") - self?.cachedLocationData = locationData - let locationInfo = self?.formatLocationData(locationData) ?? [:] - completion(.success(locationInfo)) - case .failure(let error): - print("[LocationService] 同步获取位置失败: \(error.localizedDescription)") - completion(.failure(error)) - } } } - /// 检查位置权限 - 使用缓存的状态 + /// 检查位置权限 private func checkLocationPermission() -> Bool { return authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways } - /// 请求位置权限 - 异步处理 + /// 请求位置权限 private func requestLocationPermission(completion: @escaping (Result) -> Void) { - print("[LocationService] 当前缓存的权限状态: \(authorizationStatus.rawValue)") - - switch authorizationStatus { - case .notDetermined: - print("[LocationService] 请求位置权限") - // 保存回调,等待权限结果 - permissionCallback = completion - DispatchQueue.main.async { [weak self] in - self?.locationManager?.requestWhenInUseAuthorization() + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + guard let locationManager = self.locationManager else { + completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "LocationManager 未初始化"]))) + return + } + + // 设置权限回调 + self.permissionCallback = completion + + // 请求权限 + switch self.authorizationStatus { + case .notDetermined: + locationManager.requestWhenInUseAuthorization() + case .denied, .restricted: + completion(.success(false)) + case .authorizedWhenInUse, .authorizedAlways: + completion(.success(true)) + @unknown default: + completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"]))) } - case .denied, .restricted: - print("[LocationService] 位置权限被拒绝或受限") - completion(.success(false)) - case .authorizedWhenInUse, .authorizedAlways: - print("[LocationService] 已有位置权限") - completion(.success(true)) - @unknown default: - completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"]))) } } - /// 请求位置更新 - 优化版本 - private func requestLocationUpdate(completion: @escaping (Result) -> Void) { + /// 高精度定位方法 + private func requestHighAccuracyLocation(completion: @escaping (Result) -> Void) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } guard let locationManager = self.locationManager else { - print("[LocationService] 错误:LocationManager 未初始化") completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "LocationManager 未初始化"]))) return } // 检查是否已有请求在进行 if self.currentLocationCallback != nil { - print("[LocationService] 错误:位置请求正在进行中,拒绝新请求") completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "位置请求正在进行中"]))) return } - // 使用缓存的状态信息 - // print("[LocationService] 开始位置请求") - // print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)") - // print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)") - // print("[LocationService] 当前线程: \(Thread.current)") - // print("[LocationService] 是否主线程: \(Thread.isMainThread)") - - // 验证权限状态 + // 检查权限 if !self.checkLocationPermission() { - print("[LocationService] 错误:无位置权限") completion(.failure(NSError(domain: "LocationService", code: -5, userInfo: [NSLocalizedDescriptionKey: "无位置权限"]))) return } - if !self.isLocationServicesEnabled { - print("[LocationService] 错误:位置服务未启用") - completion(.failure(NSError(domain: "LocationService", code: -6, userInfo: [NSLocalizedDescriptionKey: "位置服务未启用"]))) - return - } - - // 强制重新设置 delegate - if locationManager.delegate == nil { - print("[LocationService] 警告:Delegate 为空,重新设置") - locationManager.delegate = self - } - - // 验证 delegate - if locationManager.delegate === self { - print("[LocationService] Delegate 验证成功") - } else { - print("[LocationService] 错误:Delegate 设置失败") - completion(.failure(NSError(domain: "LocationService", code: -8, userInfo: [NSLocalizedDescriptionKey: "Delegate 设置失败"]))) - return - } + // 重置状态 + self.locationAttempts = 0 + self.bestLocation = nil // 设置回调 self.currentLocationCallback = { result in switch result { case .success(let locationInfo): - print("[LocationService] 位置回调成功,开始解析数据") if let latStr = locationInfo["user_lat"] as? String, let lngStr = locationInfo["user_lng"] as? String, let latitude = Double(latStr), let longitude = Double(lngStr) { - print("[LocationService] 位置数据解析成功: lat=\(latitude), lng=\(longitude)") let locationData = LocationData( - latitude: latitude, longitude: longitude, + latitude: latitude, timestamp: Date().timeIntervalSince1970 * 1000 ) completion(.success(locationData)) } else { - print("[LocationService] 错误:位置数据格式错误,原始数据: \(locationInfo)") completion(.failure(NSError(domain: "LocationService", code: -7, userInfo: [NSLocalizedDescriptionKey: "位置数据格式错误"]))) } case .failure(let error): - print("[LocationService] 位置回调失败: \(error.localizedDescription)") completion(.failure(error)) } } - print("[LocationService] 调用 locationManager.requestLocation()") - locationManager.requestLocation() - print("[LocationService] requestLocation() 调用完成,等待回调") + print("[LocationService] 开始高精度连续定位") + // 开始连续定位 + locationManager.startUpdatingLocation() - // 30秒超时机制 - DispatchQueue.main.asyncAfter(deadline: .now() + 30) { - if self.currentLocationCallback != nil { - print("[LocationService] 位置请求超时") + // 设置超时定时器(15秒) + self.locationTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { _ in + self.stopLocationUpdates() + if let bestLocation = self.bestLocation { + self.processLocation(bestLocation) + } else { let callback = self.currentLocationCallback self.currentLocationCallback = nil - callback?(.failure(NSError(domain: "LocationService", code: -9, userInfo: [NSLocalizedDescriptionKey: "位置请求超时"]))) + callback?(.failure(NSError(domain: "LocationService", code: -9, userInfo: [NSLocalizedDescriptionKey: "无法获取满足精度要求的位置"]))) } } } } + /// 停止位置更新 + private func stopLocationUpdates() { + locationManager?.stopUpdatingLocation() + locationTimer?.invalidate() + locationTimer = nil + } + + /// 处理位置数据 + private func processLocation(_ location: CLLocation) { + let locationInfo: [String: Any] = [ + "user_lng": String(location.coordinate.longitude), + "user_lat": String(location.coordinate.latitude), + "timestamp": Date().timeIntervalSince1970 * 1000, + "accuracy": location.horizontalAccuracy // 添加精度信息 + ] + + print("[LocationService] 处理最终位置,精度: \(location.horizontalAccuracy)m") + + // 更新缓存 + cachedLocationData = LocationData( + longitude: location.coordinate.longitude, + latitude: location.coordinate.latitude, + timestamp: Date().timeIntervalSince1970 * 1000 + ) + + let callback = currentLocationCallback + currentLocationCallback = nil + callback?(.success(locationInfo)) + } + /// 格式化位置数据 private func formatLocationData(_ locationData: LocationData) -> [String: Any] { return [ - "user_lat": String(locationData.latitude), "user_lng": String(locationData.longitude), + "user_lat": String(locationData.latitude), + "location": "\(locationData.longitude),\(locationData.latitude)", "timestamp": locationData.timestamp ] } - - // MARK: - 权限回调 - private var permissionCallback: ((Result) -> Void)? } // MARK: - CLLocationManagerDelegate extension LocationService: CLLocationManagerDelegate { - // 替换 didUpdateLocations 方法中的逆地理编码部分 + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { print("[LocationService] didUpdateLocations 被调用,位置数量: \(locations.count)") @@ -301,22 +287,47 @@ extension LocationService: CLLocationManagerDelegate { return } - print("[LocationService] 位置更新成功: lat=\(location.coordinate.latitude), lng=\(location.coordinate.longitude)") - print("[LocationService] 位置精度: \(location.horizontalAccuracy)m") + print("[LocationService] 位置更新: lat=\(location.coordinate.latitude), lng=\(location.coordinate.longitude)") + print("[LocationService] 水平精度: \(location.horizontalAccuracy)m, 垂直精度: \(location.verticalAccuracy)m") + print("[LocationService] 位置时间: \(location.timestamp)") - // 直接使用经纬度,不进行逆地理编码 - let locationInfo: [String: Any] = [ - "user_lat": String(location.coordinate.latitude), - "user_lng": String(location.coordinate.longitude), - "timestamp": Date().timeIntervalSince1970 * 1000 - ] + // 过滤无效位置 + guard location.horizontalAccuracy >= 0 else { + print("[LocationService] 位置精度无效,忽略此次更新") + return + } - print("[LocationService] 准备调用位置回调") - // 调用回调 - let callback = currentLocationCallback - currentLocationCallback = nil - callback?(.success(locationInfo)) - print("[LocationService] 位置回调已调用") + // 过滤过旧的位置(超过5秒) + let locationAge = -location.timestamp.timeIntervalSinceNow + guard locationAge < 5.0 else { + print("[LocationService] 位置数据过旧(\(locationAge)秒),忽略") + return + } + + locationAttempts += 1 + + // 检查是否满足精度要求 + if location.horizontalAccuracy <= requiredAccuracy { + print("[LocationService] 获得高精度位置,精度: \(location.horizontalAccuracy)m") + stopLocationUpdates() + processLocation(location) + return + } + + // 保存最佳位置 + if bestLocation == nil || location.horizontalAccuracy < bestLocation!.horizontalAccuracy { + bestLocation = location + print("[LocationService] 更新最佳位置,精度: \(location.horizontalAccuracy)m") + } + + // 达到最大尝试次数,使用最佳位置 + if locationAttempts >= maxLocationAttempts { + print("[LocationService] 达到最大尝试次数,使用最佳位置") + stopLocationUpdates() + if let bestLocation = bestLocation { + processLocation(bestLocation) + } + } } public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { @@ -325,11 +336,10 @@ extension LocationService: CLLocationManagerDelegate { print("[LocationService] 错误代码: \((error as NSError).code)") print("[LocationService] 错误域: \((error as NSError).domain)") + stopLocationUpdates() let callback = currentLocationCallback currentLocationCallback = nil - print("[LocationService] 准备调用错误回调") callback?(.failure(error)) - print("[LocationService] 错误回调已调用") } public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index c030cb725..1830237b1 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -383,6 +383,9 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { // 可选:处理音频数据 } + override fun onAudioDataReceived1(data: ByteArray) { + + } /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别 diff --git a/pubspec.yaml b/pubspec.yaml index c6aaae6db..ace7af29b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.6+20 +version: 1.0.7+20 environment: sdk: ">=3.3.0 <4.0.0"