Browse Source

可以正常端到端语音合成

newdev_shunjiawei
fdp 1 year ago
parent
commit
45e3a73914
  1. 6
      lib/core/bindings/initial_binding.dart
  2. 6
      lib/data/services/asr_service.dart
  3. 107
      lib/data/services/ast_service.dart
  4. 164
      lib/data/services/speech_impl/azure_ast_service.dart
  5. 6
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  6. 6
      lib/data/services/speech_impl/volcano_asr_service.dart
  7. 6
      lib/data/services/speech_impl/xunfei_asr_service.dart
  8. 38
      lib/modules/login/views/login_view.dart
  9. 5
      lib/modules/meeting/controllers/meeting_record_controller.dart
  10. 148
      lib/modules/translation/controllers/translation_controller.dart
  11. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  12. 4
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  13. 1
      local_plugins/azure_speech/android/build.gradle.kts
  14. 1223
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt
  15. 338
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  16. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt
  17. 22
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  18. 454
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  19. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  20. 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';
@ -22,7 +24,6 @@ class InitialBinding extends Bindings {
void dependencies() {
Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化)
// 语言管理器(需要最先初始化)
Get.lazyPut<LanguageManager>(() => LanguageManager(), fenix: true);
@ -30,6 +31,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);

6
lib/data/services/asr_service.dart

@ -46,6 +46,12 @@ abstract class AsrService {
/// 暂停录音
Future<bool> pauseRecord();
/// 设置音频配置
Future<bool> setAudioConfig({
int sampleRate = 16000,
int channels = 1,
});
// /// 移动文件到新路径
Future<bool> moveFile(String sourcePath, String destPath);

107
lib/data/services/ast_service.dart

@ -0,0 +1,107 @@
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> stopContinuousTranslation(bool isSave);
/// 开始录音
Future<bool> path(String filePath);
}
/// 识别事件类型
// enum RecognitionEventType {
// /// 最终识别结果
// finalResult,
// /// 中间识别结果(实时反馈)
// intermediateResult,
// /// 音频
// onAudio,
// /// 会话开始
// sessionStarted,
// /// 会话结束
// sessionStopped,
// /// 识别取消
// canceled,
// /// 识别错误
// error,
// }
/// 识别事件
// class RecognitionEvent {
// /// 事件类型
// final RecognitionEventType type;
// /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
// final String text;
// /// 检测到的语言
// final String detectedLanguage;
// /// 角色
// final String role;
// /// 原始音频
// final Uint8List? audio;
// /// 错误信息(仅在 error 和 canceled 类型中有效)
// final String error;
// RecognitionEvent({
// required this.type,
// this.text = '',
// this.detectedLanguage = '',
// this.role = '',
// this.audio,
// this.error = '',
// });
// /// 创建最终结果事件的快捷构造函数
// factory RecognitionEvent.finalResult({
// required String text,
// String detectedLanguage = '',
// }) {
// return RecognitionEvent(
// type: RecognitionEventType.finalResult,
// text: text,
// detectedLanguage: detectedLanguage,
// );
// }
// /// 创建错误事件的快捷构造函数
// factory RecognitionEvent.error(String errorMessage) {
// return RecognitionEvent(
// type: RecognitionEventType.error,
// error: errorMessage,
// );
// }
// /// 检查是否为最终结果
// bool get isFinalResult => type == RecognitionEventType.finalResult;
// /// 检查是否为错误
// bool get isError =>
// type == RecognitionEventType.error ||
// type == RecognitionEventType.canceled;
// @override
// String toString() {
// return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}';
// }
// }

164
lib/data/services/speech_impl/azure_ast_service.dart

@ -0,0 +1,164 @@
import 'dart:async';
import '../../../data/models/appconfig.dart';
import 'package:flutter/services.dart';
import '../../../core/utils/logger.dart';
import 'package:get/get.dart';
import '../ast_service.dart';
/// 音频源类型
enum AudioSourceType {
microphone, // 使用设备麦克风
external // 使用外部提供的音频数据
}
/// Azure 语音识别服务
///
/// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口
class AzureAstService extends GetxService implements AstService {
static final AzureAstService to = Get.put(AzureAstService());
static const MethodChannel _channel = MethodChannel('azure_speech/ast');
static const EventChannel _eventChannel =
EventChannel('azure_speech/ast_events');
// final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
late String _baseUrl;
final String _endpoint = '/'; // 修改为根路径
late final String _accessKey;
late final String _secretKey;
late final String _region;
late final String _service;
final List<String> _defaultSupportedLanguages = ['zh-CN', 'en-US'];
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
// 连续识别相关
// bool _isContinuousRecognitionActive = false;
// StreamController<RecognitionEvent>? _eventStreamController;
// StreamSubscription? _eventSubscription;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言
String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage;
// 当前音频源类型
AudioSourceType _audioSourceType = AudioSourceType.microphone;
AzureAstService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
// final _env = _storage.read("ENV") as Map<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<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> stopContinuousTranslation(bool isSave) async {
try {
final bool result =
await _channel.invokeMethod('stopContinuousTranslation', {
'isSave': isSave,
});
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<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;
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false;
rethrow;
}
}
}

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

@ -865,4 +865,10 @@ class VolcanoAsrApiService implements AsrService {
// TODO: implement startContinuousRecognition
throw UnimplementedError();
}
@override
Future<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
}

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

@ -400,4 +400,10 @@ class VolcanoAsrService extends GetxService implements AsrService {
// TODO: implement startContinuousRecognition
throw UnimplementedError();
}
@override
Future<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
}

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

@ -324,4 +324,10 @@ class XunfeiAsrService extends GetxService implements AsrService {
// TODO: implement startContinuousRecognition
throw UnimplementedError();
}
@override
Future<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
}

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

@ -232,28 +232,26 @@ class LoginView extends GetView<LoginController> {
margin: EdgeInsets.symmetric(horizontal: 8.w),
),
Expanded(
child: GetBuilder<LoginController>(
builder: (controller) => TextField(
controller: controller.contactController,
decoration: InputDecoration(
hintText: controller.selectedCountryFlag == '✉️'
? 'email'.tr // 邮箱
: 'phoneNumber'.tr, // 手机号码
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode
? Colors.grey[500]
: Colors.black38, // 调整提示文字颜色
),
border: InputBorder.none,
fillColor: Colors.transparent,
filled: true,
focusedBorder: InputBorder.none,
),
style: TextStyle(
child: TextField(
controller: controller.contactController,
decoration: InputDecoration(
hintText: controller.selectedCountryFlag == '✉️'
? 'email'.tr // 邮箱
: 'phoneNumber'.tr, // 手机号码
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
color: isDarkMode
? Colors.grey[500]
: Colors.black38, // 调整提示文字颜色
),
border: InputBorder.none,
fillColor: Colors.transparent,
filled: true,
focusedBorder: InputBorder.none,
),
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),

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

@ -256,6 +256,7 @@ class MeetingRecordController extends GetxController
// Start audio recording
await _asrService.enableRecord("${dir.path}/$fullFileName.wav");
isRecording.value = true;
// Update state
fileName.value = fullFileName;
@ -271,12 +272,16 @@ class MeetingRecordController extends GetxController
void _startHardwareServices() {
switch (audioType.value) {
case 0:
// Start audio recording
_asrService.setAudioConfig(sampleRate: 16000, channels: 1);
_bleManager.openEncoder();
break;
case 1:
_asrService.setAudioConfig(sampleRate: 16000, channels: 1);
_bleManager.openDecoder();
break;
case 2:
_asrService.setAudioConfig(sampleRate: 16000, channels: 2);
_bleManager.openA2DPDecoder();
break;
}

148
lib/modules/translation/controllers/translation_controller.dart

@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/ast_service.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart';
@ -34,6 +35,8 @@ class TranslationController extends GetxController {
final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
final TtsService _ttsService = Get.find<TtsService>();
final AstService _astService = Get.find<AstService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 蓝牙服务
@ -363,6 +366,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 _astService.path("${dir.path}/8_mic.wav");
// 配置实时翻译参数
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() {
stopRecognition();
@ -410,6 +551,8 @@ class TranslationController extends GetxController {
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
await _asrService.enableRecord(
"${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
await _astService.enableRecord(
"${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav");
}
Future<void> stopRecording() async {
@ -475,6 +618,9 @@ class TranslationController extends GetxController {
_audioSourceType = true;
isTtsEnabled.value = false;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
// 初始化语音翻译服务
await _initializeCallModeTranslationService();
} else {
// 开始连续语音识别
_audioSourceType = false;
@ -526,7 +672,7 @@ class TranslationController extends GetxController {
try {
await _asrService.stopContinuousRecognition();
await _astService.stopContinuousTranslation(true);
// 停止ASR活跃时长计时
_stopAsrActiveTracking();

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

@ -1166,6 +1166,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"))
}

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

File diff suppressed because it is too large

338
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -16,6 +16,7 @@ import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice
import kotlinx.coroutines.*
/** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
@ -35,6 +36,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper
// AST相关
private lateinit var astChannel: MethodChannel
private lateinit var astEventChannel: EventChannel
private var astEventSink: EventChannel.EventSink? = null
private lateinit var azureAstHelper: IntegratedSpeechTranslationService
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
@ -74,6 +81,22 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
}
// AST 事件发送方法
private fun sendAstEvent(event: Map<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}")
}
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
@ -84,7 +107,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
// 初始化TTS通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts")
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化AST通道
astChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast")
astChannel.setMethodCallHandler(AsTMethodHandler())
// 初始化ASR事件通道
asrEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events")
@ -111,11 +136,24 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
ttsEventSink = null
}
})
// 初始化AST事件通道
astEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast_events")
astEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
astEventSink = events
//setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
}
override fun onCancel(arguments: Any?) {
astEventSink = null
}
})
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
azureAstHelper = IntegratedSpeechTranslationService(context)
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
@ -437,7 +475,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val filePath = call.argument<String>("filePath") ?: ""
try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAsrHelper.enableRecord(filePath)
result.success(true)
} catch (e: Exception) {
@ -506,6 +544,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
} else {
FileLogger.w(tag, "无法获取自定义音频输出流")
}
}
@ -584,6 +624,287 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
}
// ASt方法处理器
inner class AsTMethodHandler : MethodCallHandler {
private suspend fun streamAudioFile(file: java.io.File) {
try {
val inputStream = file.inputStream()
val buffer = ByteArray(1024) // 每次读取1KB
// WAV文件参数(假设16kHz, 16bit, 单声道)
val sampleRate = 16000 // 采样率
val bytesPerSample = 2 // 16bit = 2字节
val channels = 1 // 单声道
// 计算每秒需要的字节数
val bytesPerSecond = sampleRate * bytesPerSample * channels
// 计算每个缓冲区对应的播放时间(毫秒)
val bufferDurationMs = (buffer.size * 1000L) / bytesPerSecond
FileLogger.d("TAG", "开始流式读取音频文件,缓冲区大小: ${buffer.size}, 播放间隔: ${bufferDurationMs}ms")
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
// 只发送实际读取的字节数
val audioChunk = if (bytesRead < buffer.size) {
buffer.copyOf(bytesRead)
} else {
buffer
}
// 推送音频数据块
withContext(Dispatchers.Main) {
azureAstHelper.pushAudioData(audioChunk)
FileLogger.d("TAG", "推送音频数据块: ${audioChunk.size} 字节")
}
}
inputStream.close()
FileLogger.d("TAG", "音频文件流式读取完成")
} catch (e: Exception) {
FileLogger.e("TAG", "流式读取音频文件失败: ${e.message}")
}
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"enableRecord" -> {
val filePath = call.argument<String>("filePath") ?: ""
try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAstHelper.enableRecord(filePath)
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)
}
}
"stopContinuousTranslation" -> {
val isSave = call.argument<Boolean>("isSave") ?: false
try {
FileLogger.d(tag, "停止翻译")
azureAstHelper.stopContinuousTranslation()
result.success(true)
} catch (e: Exception) {
result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null)
}
}
"path" -> {
val filePath = call.argument<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) {
// sendAstEvent(
// mapOf(
// "type" to "recognized",
// "text" to text,
// "language" to language,
// "confidence" to confidence
// )
// )
}
override fun onTranslated(
originalText: String,
translatedText: String,
targetLanguage: String
) {
// sendAstEvent(
// mapOf(
// "type" to "translated",
// "originalText" to originalText,
// "translatedText" to translatedText,
// "targetLanguage" to targetLanguage
// )
// )
}
override fun onTranslationStarted(text: String) {
// sendAstEvent(
// mapOf(
// "type" to "translationStarted",
// "text" to text
// )
// )
}
override fun onTranslationFailed(text: String, error: String) {
// sendAstEvent(
// mapOf(
// "type" to "translationFailed",
// "text" to text,
// "error" to error
// )
// )
}
override fun onSynthesisStarted(text: String) {
// sendAstEvent(
// mapOf(
// "type" to "synthesisStarted",
// "text" to text
// )
// )
}
override fun onSynthesisCompleted(text: String) {
// sendAstEvent(
// mapOf(
// "type" to "synthesisCompleted",
// "text" to text
// )
// )
}
override fun onSynthesisFailed(text: String, error: String) {
// sendAstEvent(
// mapOf(
// "type" to "synthesisFailed",
// "text" to text,
// "error" to error
// )
// )
}
override fun onSynthesisProgress(text: String, progress: Float) {
// sendAstEvent(
// mapOf(
// "type" to "synthesisProgress",
// "text" to text,
// "progress" to progress
// )
// )
}
override fun onRecognitionStarted() {
// sendAstEvent(
// mapOf(
// "type" to "recognitionStarted"
// )
// )
}
override fun onRecognitionStopped() {
// sendAstEvent(
// mapOf(
// "type" to "recognitionStopped"
// )
// )
}
override fun onStateChanged(component: String, isActive: Boolean) {
// sendAstEvent(
// mapOf(
// "type" to "stateChanged",
// "component" to component,
// "isActive" to isActive
// )
// )
}
override fun onError(component: String, error: String) {
// sendAstEvent(
// mapOf(
// "type" to "error",
// "component" to component,
// "error" to error
// )
// )
}
}
// 使用协程调用异步初始化方法
GlobalScope.launch(Dispatchers.Main) {
azureAstHelper.initialize(
azureConfig = azureConfig,
translationConfig = translationConfig,
serviceConfig = serviceConfig,
callback = callback
)
}
result.success(true)
}
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
@ -624,6 +945,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
// 可选:处理音频数据
}
override fun onAudioDataReceived1(data: ByteArray) {
azureAstHelper.pushAudioData(data)
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
@ -635,4 +962,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
// 不处理设备信息
}
}
}

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt

@ -24,7 +24,7 @@ object RecordFile {
// 音频配置 - 可配置的采样率和声道数
private var sampleRate = 16000
private var channels = 2 // 1=单声道, 2=立体声
private var channels = 1 // 1=单声道, 2=立体声
// 新增:用于存储最后成功保存的文件
private var lastSavedFile: File? = null

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 WRITE_AUDIO_CHAR_UUID1: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC")
/** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */
val CALL_WRITE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC")
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */
val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb")
@ -23,15 +28,6 @@ object BleConst {
/** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */
val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb")
/** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc")
/** 音频写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
val AUDIO_WRITE_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC")
/** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */
val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc")
/** 客户端特征配置描述符UUID */
val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

454
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -26,6 +26,7 @@ import android.os.Handler
import java.util.concurrent.atomic.AtomicBoolean
import android.util.Log
import androidx.annotation.RequiresPermission
import java.util.concurrent.TimeUnit
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -53,6 +54,10 @@ object BleService {
// 数据相关回调
fun onAudioDataReceived(data: ByteArray)
// 数据相关回调
fun onAudioDataReceived1(data: ByteArray)
// 唤醒信号相关回调
fun onWakeupSignalReceived()
// 设备信息相关回调 - 统一回调接口
@ -77,9 +82,10 @@ object BleService {
private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null
private var audioChar: BluetoothGattCharacteristic? = null
private var writeChar1: BluetoothGattCharacteristic? = null
private var callWriteChar: BluetoothGattCharacteristic? = null
var recordfile: RecordingFile? = null
var recordfile1: RecordingFile? = null
// 扫描相关
private lateinit var scanHandler: Handler
private val scanResults = ArrayList<ScanResult>()
@ -102,19 +108,26 @@ object BleService {
// Opus解码器实例
private var opusManager: OpusManager? = null
private var option: OpusOption? = null
private val mainHandler = Handler(Looper.getMainLooper())
// 音频解码处理线程
private var audioDecodeThread: HandlerThread? = null
private var audioDecodeHandler: Handler? = null
// 音频数据队列和处理线程
// 解码音频数据队列和处理线程
private val audioDataQueue = LinkedBlockingQueue<ByteArray>()
private var audioQueueProcessorThread: Thread? = null
// 音频数据缓存
private val audioDataBuffer = mutableListOf<Byte>()
private val AUDIO_BUFFER_SIZE = 1280// 1280字节缓存阈值
//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 = 40 // 每次发送40字节
private val AUDIO_SEND_INTERVAL = 10L // 发送间隔10ms
// 初始化状态
private var isInitialized = false
@ -138,20 +151,6 @@ object BleService {
try {
this.context = appContext.applicationContext
// otaManager = OTAManager(this.context).apply {
// // 设置数据回调
// setDataCallback(object : OTAManager.DataCallback {
// override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) {
// data?.let {
// Log.i("BleService", "收到数据:${it.size} 字节")
// Log.i("BleService", "认证交互数据${it.contentToString()}")
// // 移除次数限制,除非明确需要
// otaManager?.onReceiveDeviceData(device, it)
// }
// }
// })
// }
// 初始化蓝牙管理器和适配器
bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@ -160,22 +159,14 @@ object BleService {
// 初始化Handler
scanHandler = Handler(Looper.getMainLooper())
// 初始化音频解码线程
audioDecodeThread = HandlerThread("AudioDecodeThread").apply {
start()
audioDecodeHandler = Handler(looper)
}
Log.d(TAG, "音频解码线程初始化成功")
// 启动队列处理
startAudioQueueProcessing()
// 初始化OpusManager和OTAManager
// 初始化OpusManager和OpusOption
try {
opusManager = OpusManager()
startOpusStreamDecoding()
startEncodeStream()
option = OpusOption()
Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
@ -184,7 +175,7 @@ object BleService {
recordfile = RecordingFile(this.context)
recordfile!!.fileName = "不拆分"
recordfile1 = RecordingFile(this.context)
recordfile1 = RecordingFile(this.context)
recordfile1!!.fileName = "重新压缩"
isInitialized = true
Log.d(TAG, "BLE服务初始化成功")
@ -230,6 +221,25 @@ object BleService {
Log.d(TAG, "已清除所有BLE回调")
}
fun writeExternalAudioData(data: ByteArray) {
// 检查OpusManager是否已初始化
if (opusManager == null) {
Log.e(TAG, "opusManager 未初始化")
return
}
// 如果已经在编码流中,直接写入数据
if (opusManager?.isEncodeStream == true) {
Log.d(TAG, "正在进行Opus编码流,写入音频数据")
// 将外部音频数据写入编码流,每次处理1280字节
opusManager?.writeEncodeStream(data)
} else {
Log.w(TAG, "Opus编码流未启动,无法写入音频数据")
// 可选:自动启动编码流
// startOpusEncodeStream()
}
}
// ======================================================================================================
// 扫描功能
// ======================================================================================================
@ -385,15 +395,16 @@ object BleService {
}
return null;
}
fun getDeviceInfos(deviceName: String): List<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) {
@ -411,9 +422,10 @@ object BleService {
}
}
}
return ret
}
/**
* 打印设备信息和服务UUID
*/
@ -542,7 +554,7 @@ object BleService {
notifyChar = null
writeChar = null
audioChar = null
writeChar1 = null
callWriteChar = null
}
}
@ -560,37 +572,17 @@ object BleService {
*/
private val gattCallback = object : BluetoothGattCallback() {
// override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
// if (status == BluetoothGatt.GATT_SUCCESS) {
// Log.i(TAG, "MTU 更新成功: $mtu")
// configureOTA()
// startOTA()
// } else {
// Log.e(TAG, "MTU 更新失败: status=$status")
// }
// }
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
when {
status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> {
updateConnectionState(BleConst.STATE_CONNECTED)
g.discoverServices()
// Log.i(TAG, "连接成功,开始 MTU 协商")
// otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK)
// g.requestMtu(512) // 触发 MTU 修改流程
}
newState == BluetoothProfile.STATE_DISCONNECTED -> {
updateConnectionState(BleConst.STATE_DISCONNECTED)
disconnectGatt()
// otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING)
// g.close()
// otaManager?.release();
}
else -> {
@ -617,17 +609,18 @@ object BleService {
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
// 获取音频服务1特征
val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1)
writeChar1 = audioSvc1?.getCharacteristic(BleConst.WRITE_AUDIO_CHAR_UUID1)
if (writeChar1 == null) {
Log.e(TAG, "未找到主服务所需特征")
// 获取通话音频服务特征
val callAudioSvc = g.getService(BleConst.CALL_AUDIO_SERVICE_UUID)
callWriteChar = callAudioSvc?.getCharacteristic(BleConst.CALL_WRITE_AUDIO_CHAR_UUID)
if (callWriteChar == null) {
Log.e(TAG, "未找到通话音频主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR)
return
}
Log.d(TAG, "找到writeChar1服务所需特征")
Log.d(TAG, "找到通话音频服务所需特征")
writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
writeChar1?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
//要先设置音频服务的通知,否则接收不到
// 设置音频服务的通知(如果存在)
if (audioChar != null) {
@ -636,10 +629,9 @@ object BleService {
} else {
Log.w(TAG, "音频服务特征未找到")
}
// 设置主服务的通知
setupNotifications(g, notifyChar)
//开启解码
}
@ -649,12 +641,6 @@ object BleService {
// 根据特征UUID区分处理
when (c.uuid) {
// 音频特征数据
// BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> {
// Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1")
// processAudioData(data)
// }
// 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
@ -736,26 +722,26 @@ object BleService {
try {
if (opusManager?.isDecodeStream == true) {
recordfile?.saveAudioDataToWav(data)
// 将接收到的数据添加到缓存中
synchronized(audioDataBuffer) {
audioDataBuffer.addAll(data.toList())
// 当缓存达到阈值时,提取完整的帧进行解码
while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) {
// 提取一个完整的帧(1280字节)
val frameData = ByteArray(AUDIO_BUFFER_SIZE)
for (i in 0 until AUDIO_BUFFER_SIZE) {
frameData[i] = audioDataBuffer.removeAt(0)
}
// 将完整的帧加入队列,由专门的线程处理
audioDataQueue.offer(frameData)
Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码")
}
}
// 将完整的帧加入队列,由专门的线程处理
audioDataQueue.offer(data)
// // 将接收到的数据添加到缓存中
// synchronized(audioDataBuffer) {
// audioDataBuffer.addAll(data.toList())
// // 当缓存达到阈值时,提取完整的帧进行解码
// while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) {
// // 提取一个完整的帧(1280字节)
// val frameData = ByteArray(AUDIO_BUFFER_SIZE)
// for (i in 0 until AUDIO_BUFFER_SIZE) {
// frameData[i] = audioDataBuffer.removeAt(0)
// }
// Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码")
// }
// }
} else {
// Log.d(TAG, "Opus解码流未启动,忽略音频数据")
Log.d(TAG, "Opus解码流未启动,忽略音频数据")
}
} catch (e: Exception) {
Log.e(TAG, "处理音频数据异常: ${e.message}", e)
@ -959,8 +945,8 @@ object BleService {
codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON
) {
recordfile1!!.closeFile()
recordfile1!!.creatingFiles()
recordfile1!!.closeFile()
recordfile1!!.creatingFiles()
recordfile!!.closeFile()
recordfile!!.creatingFiles()
} else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) {
@ -1140,18 +1126,17 @@ recordfile1!!.closeFile()
opusManager?.stopDecodeStream()
Log.d(TAG, "已停止正在进行的Opus解码流")
}
// 清理音频数据缓存,确保开始时是干净的状态
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "开始解码前已清理音频数据缓存")
}
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
option!!.setHasHead(hasHeader)
option!!.setChannel(channel)
option!!.setSampleRate(sampleRate)
option!!.setPacketSize(packetSize)
Log.d(TAG, "准备开始Opus数据流解码, 参数: $option")
@ -1160,31 +1145,40 @@ recordfile1!!.closeFile()
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
// Log.d(TAG, "Opus解码数据: ${data.size} bytes")
//解码数据再重新编码回去
val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节)
val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区
val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区
// 拆分交错的左右声道数据
for (i in 0 until sampleCount) {
val stereoIndex = i * 4
val monoIndex = i * 2
// 左声道(低位字节在前,高位字节在后)
leftBuffer[monoIndex] = data[stereoIndex]
leftBuffer[monoIndex + 1] = data[stereoIndex + 1]
// 右声道
rightBuffer[monoIndex] = data[stereoIndex + 2]
rightBuffer[monoIndex + 1] = data[stereoIndex + 3]
}
if (option!!.getChannel() == 2) {
//解码数据再重新编码回去
val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节)
val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区
val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区
// 拆分交错的左右声道数据
for (i in 0 until sampleCount) {
val stereoIndex = i * 4
val monoIndex = i * 2
// // 将左右声道数据分别放入队列
// leftChannelQueue.offer(leftBuffer)
// rightChannelQueue.offer(rightBuffer)
opusManager?.writeEncodeStream(rightBuffer)
// 左声道(低位字节在前,高位字节在后)
leftBuffer[monoIndex] = data[stereoIndex]
leftBuffer[monoIndex + 1] = data[stereoIndex + 1]
notifyAudioDataReceived(rightBuffer)
// 右声道
rightBuffer[monoIndex] = data[stereoIndex + 2]
rightBuffer[monoIndex + 1] = data[stereoIndex + 3]
}
// //重新编码
// opusManager?.writeEncodeStream(rightBuffer)
notifyAudioDataReceived1(leftBuffer)
//回调
notifyAudioDataReceived(rightBuffer)//对方的
// notifyAudioDataReceived1(leftBuffer)//自己的
} else if (option!!.getChannel() == 1) {
notifyAudioDataReceived(data)
} else {
Log.e(TAG, "Opus解码数据错误: ${data.size} bytes")
}
}
}
@ -1212,13 +1206,13 @@ recordfile1!!.closeFile()
private fun stopOpusStreamDecoding(): Boolean {
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
// 清理音频数据缓存
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "已清理音频数据缓存")
}
Log.i(TAG, "已停止Opus数据流解码")
return true
}
@ -1230,55 +1224,167 @@ recordfile1!!.closeFile()
// Opus 编码相关
// ======================================================================================================
private fun startEncodeStream(
hasHeader: Boolean = false, // 通常BLE传输的Opus没有文件头
channel: Int = 1,
sampleRate: Int = 16000, // 确认设备端Opus编码采样率
packetSize: Int = 40 // 确认设备端Opus编码帧长,必须与iOS版本frameSize保持一致
): Boolean {
private fun startOpusEncodeStream(): Boolean {
if (opusManager == null) {
Log.e(TAG, "OpusManager未初始化,无法开始编码")
return false
}
// 如果已经在解码流,先停止
// 如果已经在编码流,先停止
if (opusManager?.isEncodeStream == true) {
opusManager?.stopEncodeStream()
Log.d(TAG, "已停止正在进行的Opus编码流")
Log.d(TAG, "Opus编码流已在运行")
return true
}
// 启动音频发送线程
startAudioSendThread()
var streamStartedSuccessfully = false
opusManager?.startEncodeStream(object : OnEncodeStreamCallback {
override fun onEncodeStream(data: ByteArray?) {
if (data != null) {
// Log.d(TAG, "Opus解码数据: ${data.size} bytes")
//notifyAudioDataReceived(data)
writeChar1!!.value=data
recordfile1!!.saveAudioDataToWav(data)
val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar1)
if (isSuccess == true) {
Log.d(TAG, "发送opus数据")
}
// 编码完成的数据处理:
// 1. 保存到WAV文件
recordfile1?.saveAudioDataToWav(data)
// 2. 将编码后的数据加入发送队列进行分块发送
addAudioDataToSendQueue(data)
} else {
Log.w(TAG, "编码回调收到空数据")
}
}
override fun onStart() {
streamStartedSuccessfully = true
Log.i(TAG, "Opus数据流编码已开始")
// 可以通过回调通知上层解码已开始
}
override fun onComplete(outPath: String?) {
Log.i(TAG, "Opus数据流编码完成: $outPath (通常流式编码不会调用此方法)")
Log.i(TAG, "Opus数据流编码完成: $outPath")
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "Opus数据流编码错误: [$code] $message")
// 可以通过回调通知上层解码错误
}
})
return true // 暂定为调用即成功
return true
}
private fun stopOpusEncodeStream(): Boolean {
if (opusManager?.isEncodeStream == true) {
opusManager?.stopEncodeStream()
stopAudioSendThread() // 停止音频发送线程
Log.i(TAG, "已停止Opus数据流编码")
return true
}
Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化")
return false
}
/**
* 将音频数据分块并加入发送队列
*/
private fun addAudioDataToSendQueue(data: ByteArray) {
try {
// 将大数据分成40字节的小块
var offset = 0
while (offset < data.size) {
val chunkSize = minOf(AUDIO_CHUNK_SIZE, data.size - offset)
val chunk = ByteArray(chunkSize)
System.arraycopy(data, offset, chunk, 0, chunkSize)
// 将分块数据加入队列
if (!audioSendQueue.offer(chunk)) {
Log.w(TAG, "音频发送队列已满,丢弃数据块")
}
offset += chunkSize
}
Log.d(TAG, "音频数据已分块加入队列,原始大小: ${data.size} 字节,分块数: ${(data.size + AUDIO_CHUNK_SIZE - 1) / AUDIO_CHUNK_SIZE}")
} catch (e: Exception) {
Log.e(TAG, "分块音频数据异常: ${e.message}", e)
}
}
/**
* 启动音频数据发送线程
*/
private fun startAudioSendThread() {
if (audioSendThread?.isAlive == true) {
Log.d(TAG, "音频发送线程已在运行")
return
}
isAudioSending.set(true)
audioSendThread = Thread {
Log.i(TAG, "音频发送线程已启动")
while (isAudioSending.get() && !Thread.currentThread().isInterrupted) {
try {
// 从队列中取出音频数据块
val audioChunk = audioSendQueue.poll(100, TimeUnit.MILLISECONDS)
if (audioChunk != null) {
// 发送音频数据块
sendAudioChunk(audioChunk)
// 控制发送频率,避免蓝牙缓冲区溢出
//Thread.sleep(AUDIO_SEND_INTERVAL)
}
} catch (e: InterruptedException) {
Log.d(TAG, "音频发送线程被中断")
break
} catch (e: Exception) {
Log.e(TAG, "音频发送线程异常: ${e.message}", e)
}
}
Log.i(TAG, "音频发送线程已停止")
}.apply {
name = "AudioSendThread"
start()
}
}
/**
* 停止音频数据发送线程
*/
private fun stopAudioSendThread() {
isAudioSending.set(false)
audioSendThread?.interrupt()
audioSendQueue.clear()
Log.i(TAG, "音频发送线程已停止,队列已清空")
}
/**
* 发送单个音频数据块
*/
private fun sendAudioChunk(chunk: ByteArray) {
try {
if (callWriteChar == null || bluetoothGatt == null) {
Log.e(TAG, "蓝牙连接或特征值未准备就绪")
return
}
// 在主线程中执行蓝牙写入操作
audioSendHandler.post {
try {
callWriteChar?.value = chunk
val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar)
if (isSuccess == true) {
Log.d(TAG, "成功发送音频数据块,大小: ${chunk.size} 字节")
} else {
Log.e(TAG, "发送音频数据块失败,大小: ${chunk.size} 字节")
}
} catch (e: Exception) {
Log.e(TAG, "发送音频数据块异常: ${e.message}", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "发送音频数据块异常: ${e.message}", e)
}
}
// ======================================================================================================
// 公开的命令接口
@ -1361,7 +1467,9 @@ recordfile1!!.closeFile()
*/
fun openA2DPDecoder(): Boolean {
Log.i(TAG, "打开编码 0xA2")
startOpusStreamDecoding(false,2, 16000, 80)
startOpusEncodeStream()
//双声道 80字节
startOpusStreamDecoding(false, 2, 16000, 80)
// Log.i(TAG, "打开解码...")
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
@ -1452,7 +1560,7 @@ recordfile1!!.closeFile()
* 连接状态检查
*/
private fun checkConn(): Boolean =
bluetoothGatt != null && writeChar != null && writeChar1 != null &&
bluetoothGatt != null && writeChar != null && callWriteChar != null &&
connectionState.value == BleConst.STATE_CONNECTED
/**
@ -1480,34 +1588,28 @@ recordfile1!!.closeFile()
stopScan()
scanHandler.removeCallbacksAndMessages(null)
disconnectGatt()
// 清理音频解码线程和队列
audioDecodeHandler?.removeCallbacksAndMessages(null)
audioDecodeThread?.quitSafely()
audioDecodeThread = null
audioDecodeHandler = null
// 停止音频队列处理线程
audioQueueProcessorThread?.interrupt()
audioQueueProcessorThread = null
audioDataQueue.clear()
// 停止音频发送线程
stopAudioSendThread()
// 清理音频数据缓存
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "已清理音频数据缓存")
}
Log.d(TAG, "音频解码线程和队列处理线程已清理")
// 释放OpusManager
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
stopOpusEncodeStream()
stopOpusStreamDecoding()
opusManager?.release()
opusManager = null
option = null
Log.d(TAG, "OpusManager已释放")
isInitialized = false // 标记为未初始化
}
@ -1573,8 +1675,7 @@ recordfile1!!.closeFile()
isReply = false
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时
return true
} else
{
} else {
commandQueue.poll()
return false
}
@ -1660,6 +1761,18 @@ recordfile1!!.closeFile()
}
}
}
/**
* 向所有回调监听器分发音频数据
*/
private fun notifyAudioDataReceived1(data: ByteArray) {
for (callback in callbacks) {
try {
callback.onAudioDataReceived1(data)
} catch (e: Exception) {
Log.e(TAG, "分发音频数据回调异常", e)
}
}
}
/**
* 向所有回调监听器分发唤醒信号
@ -1686,7 +1799,7 @@ recordfile1!!.closeFile()
}
}
}
/**
* 启动音频队列处理
*/
@ -1696,6 +1809,7 @@ recordfile1!!.closeFile()
try {
// 从队列中取出音频数据进行解码
val audioData = audioDataQueue.take() // 阻塞等待数据
opusManager?.writeAudioStream(audioData)
} catch (e: InterruptedException) {
Log.d(TAG, "音频队列处理线程被中断")

4
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -354,6 +354,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
}
override fun onAudioDataReceived1(data: ByteArray) {
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
}
override fun onWakeupSignalReceived() {
// 将唤醒事件发送到Flutter
sendEvent(statusEventSink, mapOf("type" to "wakeup"), "发送唤醒信号异常")

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