Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
liwei1dao 1 year ago
parent
commit
e8cd5cc819
  1. 6
      lib/core/bindings/initial_binding.dart
  2. 156
      lib/core/widgets/intro_overlay_builder.dart
  3. 23
      lib/data/services/asr_service.dart
  4. 113
      lib/data/services/ast_service.dart
  5. 29
      lib/data/services/audio_service.dart
  6. 123
      lib/data/services/speech_impl/azure_asr_service.dart
  7. 305
      lib/data/services/speech_impl/azure_ast_service.dart
  8. 20
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  9. 20
      lib/data/services/speech_impl/volcano_asr_service.dart
  10. 20
      lib/data/services/speech_impl/xunfei_asr_service.dart
  11. 5
      lib/modules/login/bindings/login_binding.dart
  12. 38
      lib/modules/login/controllers/login_controller.dart
  13. 40
      lib/modules/login/views/login_view.dart
  14. 28
      lib/modules/meeting/controllers/meeting_record_controller.dart
  15. 163
      lib/modules/opus_test/controllers/opus_test_controller.dart
  16. 4
      lib/modules/opus_test/views/opus_test_view.dart
  17. 34
      lib/modules/settings/views/settings_view.dart
  18. 13
      lib/modules/speech_test/controllers/speech_test_controller.dart
  19. 294
      lib/modules/translation/controllers/translation_controller.dart
  20. 180
      lib/modules/translation/views/translation_view.dart
  21. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  22. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  23. 4
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  24. 1
      local_plugins/azure_speech/android/build.gradle.kts
  25. 517
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  26. 1311
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt
  27. 461
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  28. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  29. 310
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt
  30. 4
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt
  31. 426
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  32. 50
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  33. 19
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  34. 22
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  35. 674
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  36. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  37. 3
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

6
lib/core/bindings/initial_binding.dart

@ -3,6 +3,8 @@ import '../../../data/services/user_portrait.dart';
import '../../../data/services/location_manager.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/navigation_manager.dart';
import '../../../data/services/ast_service.dart';
import '../../../data/services/speech_impl/azure_ast_service.dart';
import 'package:get/get.dart';
import '../../data/services/meeting/meeting_task_service.dart';
import '../../data/services/meeting/meeting_upload_service.dart';
@ -23,7 +25,6 @@ class InitialBinding extends Bindings {
void dependencies() {
Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化)
// 语言管理器(需要最先初始化)
Get.lazyPut<LanguageManager>(() => LanguageManager(), fenix: true);
@ -31,6 +32,9 @@ class InitialBinding extends Bindings {
Get.lazyPut<SpeechFactory>(() => SpeechFactory(), fenix: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
// 注册 AST 服务
Get.lazyPut<AstService>(() => AzureAstService(), fenix: true);
// 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);

156
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<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(
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;
}
}

23
lib/data/services/asr_service.dart

@ -41,11 +41,26 @@ abstract class AsrService {
bool isContinuousRecognitionActive();
/// 开始录音
Future<bool> enableRecord(String filePath);
///
/// [filePath] 录音文件路径
/// [acceptAudioData] 是否接受音频数据回调,默认为 false
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false});
/// 获取音频数据流(如果支持)
Stream<Uint8List>? getAudioDataStream() => null;
/// 暂停录音
Future<bool> pauseRecord();
/// 继续录音
Future<bool> resumeRecord();
/// 设置音频配置
Future<bool> setAudioConfig({
int sampleRate = 16000,
int channels = 1,
});
// /// 移动文件到新路径
Future<bool> moveFile(String sourcePath, String destPath);
@ -64,12 +79,12 @@ enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 麦识别结果
finalResult1,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 音频
onAudio,
/// 会话开始
sessionStarted,

113
lib/data/services/ast_service.dart

@ -0,0 +1,113 @@
import 'dart:async';
import 'dart:typed_data';
/// 语音识别服务接口
abstract class AstService {
/// 支持的语言
List<String> get supportedLanguages;
/// 初始化语音识别服务
Future<bool> initialize({required List<String> supportedLanguages});
/// 开始录音
Future<bool> enableRecord(String filePath);
///
Future<bool> startContinuousTranslation();
///
Future<bool> stopContinuousTranslation();
/// 返回一个包含识别事件的流
Future<Stream<RecognitionEvent1>> recognizeCallback();
/// 开始录音
Future<bool> path(String filePath);
/// 释放资源
Future<void> 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}';
}
}

29
lib/data/services/audio_service.dart

@ -0,0 +1,29 @@
import 'dart:async';
import 'dart:typed_data';
/// 语音识别服务接口
abstract class AudioService {
/// 开始录音
Future<bool> enableRecord(String filePath);
/// 暂停录音
Future<bool> pauseRecord();
/// 继续录音
Future<bool> resumeRecord();
/// 设置音频配置
Future<bool> setAudioConfig({
int sampleRate = 16000,
int channels = 1,
});
// /// 移动文件到新路径
Future<bool> moveFile(String sourcePath, String destPath);
// /// 重命名指定路径的音频文件
Future<bool> renameFile(String filePath, String newName);
/// 结束录音
Future<bool> stopRecord(bool isSave);
}

123
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<Uint8List>? _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<bool> initialize({
List<String>? supportedLanguages,
@ -220,6 +242,36 @@ class AzureAsrService extends GetxService implements AsrService {
return _isContinuousRecognitionActive;
}
/// 处理音频数据事件
void _handleAudioDataEvent(dynamic event) {
if (event is! Map) return;
final Map<dynamic, dynamic> 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<Uint8List>? getAudioDataStream() {
print("getAudioDataStream");
_audioDataStreamController ??= StreamController<Uint8List>.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<bool> enableRecord(String filePath) async {
Future<bool> 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<bool> 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<bool> resumeRecord() async {
try {
final bool result = await _channel.invokeMethod('resumeRecord');
return result;
} catch (e) {
Logger.error('继续录音: ${e.toString()}');
rethrow;
}
}
}

305
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<String> _defaultSupportedLanguages = ['zh-CN', 'en-US'];
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
//连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent1>? _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<dynamic, dynamic> 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<String, String>;
_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<Stream<RecognitionEvent1>> recognizeCallback() async {
if (!_isInitialized) {
await initialize();
}
try {
_eventStreamController = StreamController<RecognitionEvent1>.broadcast();
// 开始连续识别
final bool result = await _channel.invokeMethod('recognizeCallback');
if (!result) {
_cleanupEventStream();
}
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> 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<bool> 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<bool> startContinuousTranslation() async {
try {
final bool result =
await _channel.invokeMethod('startContinuousTranslation');
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> stopContinuousTranslation() async {
try {
final bool result =
await _channel.invokeMethod('stopContinuousTranslation');
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> dispose() async {
try {
final bool result = await _channel.invokeMethod('dispose');
return;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
bool useExternalAudio = false,
bool useEchoCancellation = false,
}) async {
try {
final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
//底层会初始化前释放
// // 检查是否需要重新初始化
// if (_isInitialized) {
// await dispose();
// }
// 设置音频源类型
_audioSourceType = useExternalAudio
? AudioSourceType.external
: AudioSourceType.microphone;
// Future<bool> initialize({
// required String subscriptionKey,
// required String region,
// required List<String> 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;
}
}
}

20
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -813,7 +813,7 @@ class VolcanoAsrApiService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath) {
Future<bool> 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<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
@override
Stream<Uint8List>? getAudioDataStream() {
// TODO: implement getAudioDataStream
throw UnimplementedError();
}
}

20
lib/data/services/speech_impl/volcano_asr_service.dart

@ -348,7 +348,7 @@ class VolcanoAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath) {
Future<bool> 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<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
@override
Stream<Uint8List>? getAudioDataStream() {
// TODO: implement getAudioDataStream
throw UnimplementedError();
}
}

20
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -272,7 +272,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath) {
Future<bool> 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<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
@override
Stream<Uint8List>? getAudioDataStream() {
// TODO: implement getAudioDataStream
throw UnimplementedError();
}
}

5
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>(() => LoginController());
// 使用 put 而不是 lazyPut,确保控制器立即创建
Get.put<LoginController>(LoginController(), permanent: false);
}
}
}

38
lib/modules/login/controllers/login_controller.dart

@ -214,15 +214,30 @@ class LoginController extends GetxController {
List<String> 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();
}

40
lib/modules/login/views/login_view.dart

@ -371,27 +371,29 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(width: 12.w),
Expanded(
child: TextField(
controller: controller.verificationCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: InputDecoration(
hintText: 'verificationCode'.tr, // 验证码
hintStyle: TextStyle(
child: GetBuilder<LoginController>(
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,
),
),
),

28
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -60,6 +60,7 @@ class MeetingRecordController extends GetxController
bool _audioSourceType = false;
late TabController tabController;
StreamSubscription<Uint8List>? _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<void> _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 = '';

163
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<void> 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<void> 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音频源

4
lib/modules/opus_test/views/opus_test_view.dart

@ -170,6 +170,10 @@ class OpusTestView extends GetView<OpusTestController> {
onTap: () {
controller.selectExternalFile(file);
},
onLongPress: () {
// 长按显示操作菜单
controller.showFileActionSheet(context, file);
},
trailing: Icon(
Icons.arrow_forward,
color:

34
lib/modules/settings/views/settings_view.dart

@ -514,23 +514,23 @@ class SettingsView extends GetView<SettingsController> {
// 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

13
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;

294
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<VolcanoTranslationService>();
final TtsService _ttsService = Get.find<TtsService>();
final AstService _astService = Get.find<AstService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
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<void> 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<void> 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<String, String> 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<void> _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<bool> _requestRecordPermission() async {
Future<bool> 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<void> _initializeCallModeTranslationService() async {
try {
Logger.info('开始初始化通话模式语音翻译服务');
// 为通话模式配置特殊的ASR设置
final List<String> callModeLanguages = [
sourceLanguageCode.value,
targetLanguageCode.value
];
// Future<bool> initialize({
// required String subscriptionKey,
// required String region,
// required List<String> 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<void> _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<void> _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<void> _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<void> _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<void> 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<void> 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<void> 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<void> _reinitializeAsrService() async {
try {
// 停止当前识别
await stopRecognition();
// 更新ASR支持的语言
final List<String> asrSupportedLanguages = [
sourceLanguageCode.value,
@ -991,6 +1214,17 @@ class TranslationController extends GetxController {
await _asrService.initialize(
supportedLanguages: asrSupportedLanguages,
);
// Future<bool> initialize({
// required String subscriptionKey,
// required String region,
// required List<String> 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();
// 监听识别结果

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

@ -459,67 +459,128 @@ class TranslationView extends GetView<TranslationController> {
],
),
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<TranslationController> {
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);

4
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -591,9 +591,7 @@ object AgentService : CoroutineScope {
}
override fun onAudio(data: ByteArray) {
}
override fun onSessionStopped() {
sendEvent("recognition_stopped", mapOf("status" to "stopped"))

4
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) {
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

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

@ -1382,6 +1382,10 @@ extension AgentServiceImpl: BleService.Callback {
pushAudioData(data)
}
func onAudioDataReceived1(data: Data) {
}
func onWakeupSignalReceived() {
stopTts()

1
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"))
}

517
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<ByteArray>()
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}")
}
}
}

1311
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt

File diff suppressed because it is too large

461
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<String, Any>) {
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<String, Any>) {
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<String>("filePath") ?: ""
// 是否接受音频数据
val acceptAudioData = call.argument<Boolean>("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<Boolean>("isSave") ?: false
@ -460,6 +547,17 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
}
"setAudioConfig" -> {
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val channels = call.argument<Int>("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<String>("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<String>("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<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val supportedLanguages =
call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
// 获取翻译服务配置参数(需要从Flutter端传递)
val translationAccessKey = call.argument<String>("translationAccessKey") ?: ""
val translationSecretKey = call.argument<String>("translationSecretKey") ?: ""
val translationRegion =
call.argument<String>("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<String, Any>) {
// 不处理设备信息
}
}
}

2
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
*

310
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<ByteArray>()
private val isWriting = AtomicBoolean(false)
private var writeThread: Thread? = null
@ -22,15 +26,73 @@ object RecordFile {
private val dataBuffer = mutableListOf<ByteArray>()
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<ByteArray>()
private val rightChannelQueue = LinkedBlockingQueue<ByteArray>()
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<String, Int> {
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
}
}

4
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt → 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 {

426
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<ByteArray>()
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)
}
}

50
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音频配置设置完成")
}
/**
* 开始音频输入
*/

19
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)
}
}
}

22
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")

674
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<ByteArray>()
private var audioQueueProcessorThread: Thread? = null
// 音频数据缓存
private val audioDataBuffer = mutableListOf<Byte>()
//private val AUDIO_BUFFER_SIZE = 1280// 1280字节缓存阈值
// 音频数据发送相关
private val audioSendQueue = LinkedBlockingQueue<ByteArray>()
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<Byte>()
// 重发机制相关常量
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<Map<String, Any>> {
// 更新缓存
val matchedResults = scanResults.filter {
it.device.name == deviceName
}
Log.d(TAG, "找到 ${matchedResults.size} 个名称为 $deviceName 的设备")
val ret = mutableListOf<Map<String, Any>>()
// 如果找到匹配设备名的结果,返回匹配设备的信息
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()
}
}
}

4
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"), "发送唤醒信号异常")

3
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) {
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

Loading…
Cancel
Save