Browse Source

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

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
abce4ab4ea
  1. 4
      lib/data/services/asr_service.dart
  2. 10
      lib/data/services/ble_manager.dart
  3. 3
      lib/data/services/speech_impl/azure_asr_service.dart
  4. 3
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  5. 3
      lib/data/services/speech_impl/volcano_asr_service.dart
  6. 3
      lib/data/services/speech_impl/xunfei_asr_service.dart
  7. 14
      lib/modules/meeting/controllers/meeting_record_controller.dart
  8. 64
      lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart
  9. 73
      lib/modules/translation/controllers/translation_controller.dart
  10. 26
      lib/modules/translation/views/translation_view.dart
  11. 5
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  12. 5
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  13. 197
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  14. 153
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  15. 47
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt
  16. 32
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  17. 128
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  18. 175
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  19. 5
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift
  20. 226
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift
  21. 66
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift
  22. 22
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  23. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  24. 73
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  25. 10
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  26. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
  27. 30
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
  28. 13
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
  29. 93
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift
  30. 12
      local_plugins/ble_service/lib/ble_service.dart
  31. 5
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt
  32. 2
      pubspec.yaml

4
lib/data/services/asr_service.dart

@ -43,8 +43,10 @@ abstract class AsrService {
/// 开始录音 /// 开始录音
/// ///
/// [filePath] 录音文件路径 /// [filePath] 录音文件路径
/// [audioSourceType] 音频源类型
/// [acceptAudioData] 是否接受音频数据回调,默认为 false /// [acceptAudioData] 是否接受音频数据回调,默认为 false
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}); Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false});
/// 获取音频数据流(如果支持) /// 获取音频数据流(如果支持)
Stream<Uint8List>? getAudioDataStream() => null; Stream<Uint8List>? getAudioDataStream() => null;

10
lib/data/services/ble_manager.dart

@ -856,6 +856,16 @@ class BleManager extends GetxService {
} }
} }
/// 打开解码器
Future<bool> openCallRecordDecoder() async {
try {
return await _bleService.openCallRecordDecoder();
} catch (e) {
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
/// 关闭编解码器 /// 关闭编解码器
Future<bool> closeCodec() async { Future<bool> closeCodec() async {
try { try {

3
lib/data/services/speech_impl/azure_asr_service.dart

@ -465,10 +465,11 @@ class AzureAsrService extends GetxService implements AsrService {
} }
@override @override
Future<bool> enableRecord(String filePath, Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) async { {bool acceptAudioData = false}) async {
try { try {
final bool result = await _channel.invokeMethod('enableRecord', { final bool result = await _channel.invokeMethod('enableRecord', {
'audioSourceType': audioSourceType,
'filePath': filePath, 'filePath': filePath,
'acceptAudioData': acceptAudioData, // 新增参数 'acceptAudioData': acceptAudioData, // 新增参数
}); });

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

@ -813,7 +813,8 @@ class VolcanoAsrApiService implements AsrService {
} }
@override @override
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) { Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) {
// TODO: implement enableRecord // TODO: implement enableRecord
throw UnimplementedError(); throw UnimplementedError();
} }

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

@ -348,7 +348,8 @@ class VolcanoAsrService extends GetxService implements AsrService {
} }
@override @override
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) { Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) {
// TODO: implement enableRecord // TODO: implement enableRecord
throw UnimplementedError(); throw UnimplementedError();
} }

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

@ -272,7 +272,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
} }
@override @override
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) { Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) {
// TODO: implement enableRecord // TODO: implement enableRecord
throw UnimplementedError(); throw UnimplementedError();
} }

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

@ -70,7 +70,7 @@ class MeetingRecordController extends GetxController
// 性能优化:音频处理采样控制 // 性能优化:音频处理采样控制
int _audioProcessCounter = 0; int _audioProcessCounter = 0;
static const int _audioProcessInterval = 3; // 每3个音频包处理1次 static const int _audioProcessInterval = 1; // 每1个音频包处理1次
// 波形流动动画控制器 // 波形流动动画控制器
late AnimationController waveformAnimationController; late AnimationController waveformAnimationController;
@ -306,9 +306,13 @@ class MeetingRecordController extends GetxController
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
final fullFileName = "${fileName.value}_$formattedTime"; final fullFileName = "${fileName.value}_$formattedTime";
// Start audio recording if (fileName.value == "liveRecording".tr) {
await _asrService.enableRecord("${dir.path}$fullFileName.wav", await _asrService.enableRecord(false, "${dir.path}$fullFileName.wav",
acceptAudioData: true); acceptAudioData: true);
} else {
await _asrService.enableRecord(true, "${dir.path}$fullFileName.wav",
acceptAudioData: true);
}
// 通过 asrService 获取音频数据流 // 通过 asrService 获取音频数据流
_audioDataSubscription = _audioDataSubscription =
@ -350,7 +354,7 @@ class MeetingRecordController extends GetxController
break; break;
case 2: case 2:
_asrService.setAudioConfig(sampleRate: 16000, channels: 2); _asrService.setAudioConfig(sampleRate: 16000, channels: 2);
_bleManager.openA2DPDecoder(); _bleManager.openCallRecordDecoder();
break; break;
} }
} }

64
lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart

@ -35,38 +35,38 @@ class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
controller.addMeetingAudio(); controller.addMeetingAudio();
}, },
), ),
// if (controller.meetingController.type == 1) if (controller.meetingController.type == 1)
// _buildItem( _buildItem(
// context, // 传递 context 参数 context, // 传递 context 参数
// 'audioVideoRecording'.tr, // 对应中文:音、视频录音 'audioVideoRecording'.tr, // 对应中文:音、视频录音
// Icons.videocam, Icons.videocam,
// Colors.green, Colors.green,
// onTap: controller.bleManager.isConnected onTap: controller.bleManager.isConnected
// ? () async { ? () async {
// Get.back(); Get.back();
// await Get.toNamed(Routes.meetingRecord, arguments: { await Get.toNamed(Routes.meetingRecord, arguments: {
// 'audioTypes': 1, 'audioTypes': 1,
// }); });
// controller.addMeetingAudio(); controller.addMeetingAudio();
// } }
// : null, : null,
// ), ),
// if (controller.meetingController.type == 1) if (controller.meetingController.type == 1)
// _buildItem( _buildItem(
// context, // 传递 context 参数 context, // 传递 context 参数
// 'callRecording'.tr, // 对应中文:通话录音 'callRecording'.tr, // 对应中文:通话录音
// Icons.phone, Icons.phone,
// Colors.orange, Colors.orange,
// onTap: controller.bleManager.isConnected onTap: controller.bleManager.isConnected
// ? () async { ? () async {
// Get.back(); Get.back();
// await Get.toNamed(Routes.meetingRecord, arguments: { await Get.toNamed(Routes.meetingRecord, arguments: {
// 'audioTypes': 2, 'audioTypes': 2,
// }); });
// controller.addMeetingAudio(); controller.addMeetingAudio();
// } }
// : null, : null,
// ), ),
_buildItem( _buildItem(
context, // 传递 context 参数 context, // 传递 context 参数
'importAudio'.tr, // 对应中文:导入音频 'importAudio'.tr, // 对应中文:导入音频

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

@ -74,7 +74,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
final isTranslating = false.obs; final isTranslating = false.obs;
final isTtsEnabled = true.obs; final isTtsEnabled = true.obs;
final isRecording = false.obs; final isRecording = false.obs;
final lasyIsRecording = false.obs; final isCreateRecord = false.obs;
final hasRecordPermission = false.obs; final hasRecordPermission = false.obs;
// ==================== 语言相关 ==================== // ==================== 语言相关 ====================
@ -128,6 +128,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
if (mode == 'call') { if (mode == 'call') {
await _initializeCallModeTranslationService(); await _initializeCallModeTranslationService();
} else {
_reinitializeAsrService();
} }
} }
@ -186,8 +188,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// [speakerId] 说话者ID /// [speakerId] 说话者ID
Future<void> _startFaceToFaceRecognition(int speakerId) async { Future<void> _startFaceToFaceRecognition(int speakerId) async {
try { try {
await _asrService.startContinuousRecognition(_audioSourceType); await _asrService.startContinuousRecognition(_audioSourceType); //开启识别
await continueRecording(); _startAsrActiveTracking(); //开启识别活动跟踪
if (isRecording.value) {
if (isCreateRecord.value) {
//已经创建文件
await continueRecording();
} else {
await startRecording();
}
}
Logger.info('面对面翻译:激活说话者 $speakerId'); Logger.info('面对面翻译:激活说话者 $speakerId');
} catch (e) { } catch (e) {
Logger.error('启动面对面翻译失败: ${e.toString()}'); Logger.error('启动面对面翻译失败: ${e.toString()}');
@ -198,13 +208,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 停止面对面翻译的语音识别 /// 停止面对面翻译的语音识别
Future<void> _stopFaceToFaceRecognition() async { Future<void> _stopFaceToFaceRecognition() async {
try { try {
await pauseRecording(); //暂停 if (isRecording.value) {
await pauseRecording(); //暂停
}
if (faceToFaceRecognitionText.value.isNotEmpty) { if (faceToFaceRecognitionText.value.isNotEmpty) {
handleFinalResult(faceToFaceRecognitionText.value); handleFinalResult(faceToFaceRecognitionText.value);
faceToFaceRecognitionText.value = ''; faceToFaceRecognitionText.value = '';
} }
await _asrService.stopContinuousRecognition(); await _asrService.stopContinuousRecognition();
_stopAsrActiveTracking(); _stopAsrActiveTracking(); //停止识别活动跟踪
isRecognizing.value = false; isRecognizing.value = false;
currentSessionId = null; currentSessionId = null;
@ -444,6 +456,19 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> _initializeCallModeTranslationService() async { Future<void> _initializeCallModeTranslationService() async {
try { try {
Logger.info('开始初始化通话模式语音翻译服务'); Logger.info('开始初始化通话模式语音翻译服务');
// 初始化 ASR 服务时,明确指定需要支持的语言
final List<String> asrSupportedLanguages = [targetLanguageCode.value];
Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages');
await _asrService.initialize(supportedLanguages: asrSupportedLanguages);
// 获取识别事件流
var recognitionStream = await _asrService.recognizeCallback();
_recognitionSubscription?.cancel();
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent);
final List<String> callModeLanguages = [ final List<String> callModeLanguages = [
sourceLanguageCode.value, sourceLanguageCode.value,
targetLanguageCode.value targetLanguageCode.value
@ -498,7 +523,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 切换录音功能开关 /// 切换录音功能开关
Future<void> toggleisRecord() async { Future<void> toggleisRecord() async {
isRecording.toggle(); isRecording.toggle();
if (isRecording.value && isRecognizing.value) { if (isRecording.value &&
isRecognizing.value &&
currentMode.value != 'faceToFace') {
startRecording(); startRecording();
} else if (!isRecording.value) { } else if (!isRecording.value) {
stopRecording(); stopRecording();
@ -508,21 +535,27 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 开始录音 /// 开始录音
Future<void> startRecording() async { Future<void> startRecording() async {
_timerManager.startTimer(); _timerManager.startTimer();
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
await _asrService.enableRecord( if (currentMode.value == 'call' || currentMode.value == 'audioVideo') {
"${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); await _asrService.enableRecord(
if (currentMode.value == 'call') { true, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
await _astService.enableRecord( } else {
"${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); await _asrService.enableRecord(
false, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
} }
// if (currentMode.value == 'call') {
// await _astService.enableRecord(
// "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav");
// }
isCreateRecord.value = true;
} }
/// 停止录音 /// 停止录音
Future<void> stopRecording() async { Future<void> stopRecording() async {
_timerManager.stopTimer(); _timerManager.stopTimer();
_asrService.stopRecord(true); _asrService.stopRecord(true);
lasyIsRecording.value = false; isCreateRecord.value = false;
Logger.info('录音已停止,计时器已重置'); Logger.info('录音已停止,计时器已重置');
} }
@ -530,7 +563,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> pauseRecording() async { Future<void> pauseRecording() async {
_timerManager.pauseTimer(); _timerManager.pauseTimer();
_asrService.pauseRecord(); _asrService.pauseRecord();
lasyIsRecording.value = false;
Logger.info(' 录音已暂停'); Logger.info(' 录音已暂停');
} }
@ -538,7 +571,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> continueRecording() async { Future<void> continueRecording() async {
_timerManager.resumeTimer(); _timerManager.resumeTimer();
_asrService.resumeRecord(); _asrService.resumeRecord();
lasyIsRecording.value = true;
Logger.info(' 录音已继续'); Logger.info(' 录音已继续');
} }
@ -552,8 +585,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
try { try {
await _initializeSession(); await _initializeSession();
await _configureAudioMode(); await _configureAudioMode();
await _startAsrService(); if (currentMode.value != 'faceToFace') {
await _finalizeRecognitionStart(); await _startAsrService();
await _finalizeRecognitionStart();
}
} catch (e) { } catch (e) {
isRecognizing.value = false; isRecognizing.value = false;
Logger.error('启动语音识别失败: ${e.toString()}'); Logger.error('启动语音识别失败: ${e.toString()}');
@ -622,9 +657,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 启动语音识别 /// 启动语音识别
Future<void> _startAsrService() async { Future<void> _startAsrService() async {
if (currentMode.value != 'faceToFace') { await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.startContinuousRecognition(_audioSourceType);
}
} }
/// 完成识别启动 /// 完成识别启动

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

@ -1087,19 +1087,19 @@ class TranslationView extends GetView<TranslationController> {
await controller.startRecognition(); await controller.startRecognition();
}, },
), ),
// _buildModeItem( _buildModeItem(
// context, context,
// 'audioVideoTranslation'.tr, 'audioVideoTranslation'.tr,
// Icons.videocam, Icons.videocam,
// Colors.orange, Colors.orange,
// onTap: controller.bleManager.isConnected onTap: controller.bleManager.isConnected
// ? () async { ? () async {
// Get.back(); Get.back();
// await controller.changeTranslationMode('audioVideo'); await controller.changeTranslationMode('audioVideo');
// await controller.startRecognition(); await controller.startRecognition();
// } }
// : null, : null,
// ), ),
_buildModeItem( _buildModeItem(
context, context,
'callTranslation'.tr, 'callTranslation'.tr,

5
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -130,13 +130,10 @@ Log.d(TAG, "手动启动语音识别: ")
} }
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray, channel: Int) {
// Log.d(TAG, "onAudioDataReceived, data: ${data.size}") // Log.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data) AgentService.pushAudioData(data)
// 可选:处理音频数据 // 可选:处理音频数据
}
override fun onAudioDataReceivedCall(data: ByteArray) {
} }
/** /**
* 处理唤醒信号 * 处理唤醒信号

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

@ -1507,13 +1507,10 @@ extension AgentServiceImpl: BleService.Callback {
func onConnectionStateChanged(state: Int) { func onConnectionStateChanged(state: Int) {
} }
func onAudioDataReceived(data: Data) { func onAudioDataReceived(data: Data, channel: Int32) {
pushAudioData(data) pushAudioData(data)
} }
func onAudioDataReceivedCall(data: Data) {
}
func onWakeupSignalReceived() { func onWakeupSignalReceived() {
stopTts() stopTts()

197
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -31,9 +31,6 @@ class AzureAsrHelper(private val context: Context) {
private var audioConfig: AudioConfig? = null private var audioConfig: AudioConfig? = null
private var continuousCallback: ContinuousRecognizeCallback? = null private var continuousCallback: ContinuousRecognizeCallback? = null
// 状态管理
private var isContinuousRecognitionActive = false
// 配置参数 // 配置参数
private var currentLanguage = "zh-CN" private var currentLanguage = "zh-CN"
private var supportedLanguages = arrayOf("zh-CN") private var supportedLanguages = arrayOf("zh-CN")
@ -44,8 +41,8 @@ class AzureAsrHelper(private val context: Context) {
// 音频源配置 // 音频源配置
var audioSourceType = AudioSourceType.MICROPHONE var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理 // 音频处理 - Initialize immediately to avoid null checks
var audioStream: SimpleAudioReceiver? = null var audioStream: SimpleAudioReceiver = SimpleAudioReceiver(context)
// 网络状态监听 // 网络状态监听
private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context) private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context)
@ -181,10 +178,10 @@ class AzureAsrHelper(private val context: Context) {
private fun setupRecognizer(): Boolean { private fun setupRecognizer(): Boolean {
try { try {
// 如果正在进行连续识别,先停止 // 如果正在进行连续识别,先停止
if (isContinuousRecognitionActive) { if (audioStream.isContinuousRecognitionActive) {
// 直接停止,不等待结果 // 直接停止,不等待结果
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
} }
setupMicrophoneStream() setupMicrophoneStream()
@ -214,18 +211,15 @@ class AzureAsrHelper(private val context: Context) {
try { try {
Log.d(tag, "设置麦克风流 - 使用拉流方式: }") Log.d(tag, "设置麦克风流 - 使用拉流方式: }")
if (audioStream == null) { // Initialize if not already done
// 创建外部音频拉流对象 if (!audioStream.isInitialized()) {
audioStream = SimpleAudioReceiver(context) audioStream.initAudioRecord()
audioStream!!.initAudioRecord()
} }
audioConfig = AudioConfig.fromStreamInput(audioStream!!.pushAudioStream) audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "设置麦克风流失败: ${e.message}") Log.e(tag, "设置麦克风流失败: ${e.message}")
//microphoneStream = null
} }
} }
@ -241,7 +235,7 @@ class AzureAsrHelper(private val context: Context) {
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null
): Boolean { ): Boolean {
Log.d(tag, "startContinuousRecognition:$isContinuousRecognitionActive ") Log.d(tag, "startContinuousRecognition:${audioStream.isContinuousRecognitionActive} ")
// 检查网络状态 // 检查网络状态
if (!checkNetworkStatus()) { if (!checkNetworkStatus()) {
@ -250,7 +244,7 @@ class AzureAsrHelper(private val context: Context) {
return false return false
} }
if (isContinuousRecognitionActive) { if (audioStream.isContinuousRecognitionActive) {
return true return true
} }
if (!isRecognizerValid()) { if (!isRecognizerValid()) {
@ -266,13 +260,12 @@ class AzureAsrHelper(private val context: Context) {
this.audioSourceType = audioSourceType this.audioSourceType = audioSourceType
try { try {
Log.d(tag, "startContinuousRecognition: ${audioStream!!.isWriting()}") Log.d(tag, "startContinuousRecognition: ${audioStream.isWriting()}")
// 启动音频处理(若已启动则跳过) // 启动音频处理(若已启动则跳过)
if (!audioStream!!.isWriting()) { if (!audioStream.isContinuousRecognitionActive) {
Log.d(tag, "startAudioRecord:$audioSourceType") Log.d(tag, "startAudioRecord:$audioSourceType")
audioStream!!.startAudioRecord( audioStream.startAudioRecord(
when (audioSourceType) { when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
}, },
@ -283,12 +276,12 @@ class AzureAsrHelper(private val context: Context) {
} }
recognizer?.startContinuousRecognitionAsync() recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true audioStream.isContinuousRecognitionActive = true
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "开始连续识别失败: ${e.message}") Log.e(tag, "开始连续识别失败: ${e.message}")
stopContinuousRecognition() stopContinuousRecognition()
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
return false return false
} }
} }
@ -297,7 +290,7 @@ class AzureAsrHelper(private val context: Context) {
* 设置事件监听器 * 设置事件监听器
*/ */
fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean {
Log.d(tag, "设置ssssss监听器:${speechConfig} ") Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ")
if (speechConfig == null) { if (speechConfig == null) {
callback.onError(1002,"语音服务未初始化") callback.onError(1002,"语音服务未初始化")
return false return false
@ -357,7 +350,7 @@ class AzureAsrHelper(private val context: Context) {
Log.d(tag, "会话结束事件") Log.d(tag, "会话结束事件")
if (audioSourceType == AudioSourceType.EXTERNAL) { if (audioSourceType == AudioSourceType.EXTERNAL) {
callback.onSessionStopped() callback.onSessionStopped()
isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
} }
} }
) )
@ -392,26 +385,25 @@ class AzureAsrHelper(private val context: Context) {
} }
try { try {
Log.d(tag, "停止连续语音识别: ") Log.d(tag, "停止连续语音识别: ")
// 停止音频处理 // 停止音频处理
audioStream!!.stopMicrophoneCapture() audioStream.stopMicrophoneCapture()
// 直接停止连续识别(SDK内部已是异步操作) // 直接停止连续识别(SDK内部已是异步操作)
recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS)
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
return true return true
} catch (e: Exception) { } catch (e: Exception) {
// 强制重置状态 // 强制重置状态
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
Log.e(tag, "停止连续识别失败: ${e.message}") Log.e(tag, "停止连续识别失败: ${e.message}")
// 停止音频处理 // 停止音频处理
audioStream!!.stopMicrophoneCapture() audioStream.stopMicrophoneCapture()
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
recognizer?.close()
recognizer?.close()
recognizer = null recognizer = null
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
return false return false
} }
} }
@ -419,7 +411,7 @@ class AzureAsrHelper(private val context: Context) {
/** /**
* 检查连续识别是否活跃 * 检查连续识别是否活跃
*/ */
fun isContinuousRecognitionActive(): Boolean = isContinuousRecognitionActive fun isContinuousRecognitionActive(): Boolean = audioStream.isContinuousRecognitionActive
/** /**
* 释放所有资源 * 释放所有资源
@ -427,19 +419,18 @@ class AzureAsrHelper(private val context: Context) {
fun dispose() { fun dispose() {
try { try {
// 如果正在进行连续识别,先停止 // 如果正在进行连续识别,先停止
if (isContinuousRecognitionActive) { if (audioStream.isContinuousRecognitionActive) {
// 直接停止,不等待结果 // 直接停止,不等待结果
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
} }
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 清理网络监听 // 清理网络监听
clearNetworkDetection() clearNetworkDetection()
// 停止录音 // 停止录音
if (audioStream!!.recordfile != null) { audioStream.recordfile?.closeFile(true)
audioStream!!.recordfile!!.closeFile(true)
}
// 释放recognizer // 释放recognizer
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
@ -452,11 +443,11 @@ class AzureAsrHelper(private val context: Context) {
audioConfig?.close() audioConfig?.close()
audioConfig = null audioConfig = null
// 确保状态被重置 // 确保状态被重置
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
} catch (e: Exception) { } catch (e: Exception) {
// 确保状态被重置 // 确保状态被重置
isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
audioConfig = null audioConfig = null
recognizer = null recognizer = null
speechConfig = null speechConfig = null
@ -468,20 +459,14 @@ class AzureAsrHelper(private val context: Context) {
* 停止音频处理 * 停止音频处理
*/ */
private fun stopAudioProcessing() { private fun stopAudioProcessing() {
audioStream?.let { try {
try { Log.e(tag, "停止音频处理: }")
Log.e(tag, "停止音频处理: }") audioStream.releaseAudioResources()
it.releaseAudioResources() audioStream.pushAudioStream?.close()
} catch (e: Exception) {
it.pushAudioStream?.close() Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
audioStream = null }
} catch (e: Exception) {
Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
} // 关闭音频流(根据实际实现可能需要)
} }
@ -492,47 +477,61 @@ class AzureAsrHelper(private val context: Context) {
/** /**
* 开启录音 * 开启录音
*/ */
fun enableRecord(filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null) { fun enableRecord(audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE,filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null) {
Log.i(tag, "开启录音:") Log.i(tag, "开启录音:")
if (audioStream == null) { this.audioSourceType = audioSourceType
// 创建外部音频拉流对象 if(audioSourceType == AudioSourceType.EXTERNAL){
audioStream = SimpleAudioReceiver(context) Log.i(tag, "外部音频源不在这里录音")
audioStream!!.initAudioRecord() return
}
if (audioStream!!.recordfile == null) {
audioStream!!.recordfile = RecordFile()
} }
// Initialize if not already done
// 启动音频处理 if (!audioStream.isInitialized()) {
audioStream!!.startAudioRecord(SimpleAudioReceiver.AudioSourceType.MICROPHONE audioStream.initAudioRecord()
}
,audioDataCallback)
audioStream!!.recordfile!!.closeFile(true) if (audioStream.recordfile == null) {
audioStream.recordfile = RecordFile()
}
audioStream!!.recordfile!!.creatingFiles(filePath)
if (!audioStream.isContinuousRecognitionActive) {
// 启动音频处理
Log.i(tag, "开启录音:audioSourceType${audioSourceType}")
audioStream.startAudioRecord( when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
},
audioDataCallback
)
}
audioStream.isRecord = true
audioStream.recordfile?.closeFile(true)
audioStream.recordfile?.creatingFiles(filePath)
} }
/** /**
* 移动文件到新路径 * 移动文件到新路径
*/ */
fun moveFile(sourcePath: String, destPath: String): Boolean { fun moveFile(sourcePath: String, destPath: String): Boolean {
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里移动文件")
return false
}
Log.i(tag, "移动文件到新路径:") Log.i(tag, "移动文件到新路径:")
if (audioStream!!.recordfile == null)return false audioStream.recordfile?.moveFile(sourcePath, destPath) ?: return false
audioStream!!.recordfile!!.moveFile(sourcePath, destPath)
return true return true
} }
/** /**
* 重命名指定路径的音频文件 * 重命名指定路径的音频文件
*/ */
fun renameFile(filePath: String, newName: String): Boolean { fun renameFile(filePath: String, newName: String): Boolean {
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里重命名文件")
return false
}
Log.i(tag, "重命名指定路径的音频文件:") Log.i(tag, "重命名指定路径的音频文件:")
audioStream.recordfile?.renameFile(filePath, newName) ?: return false
if (audioStream!!.recordfile == null)return false
audioStream!!.recordfile!!.renameFile(filePath, newName)
return true return true
} }
@ -540,35 +539,45 @@ class AzureAsrHelper(private val context: Context) {
* 停止录音 * 停止录音
*/ */
fun pauseRecord() { fun pauseRecord() {
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里暂停录音")
return
}
Log.i(tag, "停止连续录音:") Log.i(tag, "停止连续录音:")
if (audioStream!!.recordfile == null)return audioStream.recordfile ?: return
audioStream.isRecord = false
audioStream!!.stopMicrophoneCapture()
} }
/** /**
* 继续录音 * 继续录音
*/ */
fun resumeRecord() { fun resumeRecord() {
Log.i(tag, "继续录音:") if(audioSourceType == AudioSourceType.EXTERNAL){
if (audioStream!!.recordfile == null)return Log.i(tag, "外部音频源不在这里继续录音")
return
audioStream!!.resumeRecord() }
Log.i(tag, "继续录音:")
audioStream.recordfile ?: return
audioStream.isRecord = true
} }
/** /**
* 关闭录音 * 关闭录音
*/ */
fun stopRecord(isSave: Boolean) { fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:") if(audioSourceType == AudioSourceType.EXTERNAL){
if (audioStream == null) { Log.i(tag, "外部音频源不在这里关闭录音")
return return
} }
if (audioStream!!.recordfile == null)return Log.i(tag, "关闭录音:")
audioStream!!.stopMicrophoneCapture() audioStream.recordfile ?: return
audioStream!!.recordfile!!.closeFile(isSave)
if (!audioStream.isContinuousRecognitionActive) {
audioStream.stopMicrophoneCapture()
}
audioStream.isRecord = false
audioStream.recordfile?.closeFile(isSave)
} }

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

@ -19,7 +19,7 @@ import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice import com.deep_voice.speech.tts.AudioOutputDevice
import kotlinx.coroutines.* import kotlinx.coroutines.*
import com.yunqiinnovation.azure_speech.tools.RecordFile
/** AzureSpeechPlugin */ /** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private val tag = "AzureSpeechPlugin" private val tag = "AzureSpeechPlugin"
@ -49,7 +49,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private lateinit var audioDataEventChannel: EventChannel private lateinit var audioDataEventChannel: EventChannel
private var audioDataEventSink: EventChannel.EventSink? = null private var audioDataEventSink: EventChannel.EventSink? = null
private var recordfile: RecordFile? = null
private var isRecord = false
// 是否已添加TTS事件监听器 // 是否已添加TTS事件监听器
private var isTtsListenerAdded = false private var isTtsListenerAdded = false
@ -446,7 +447,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val newName = call.argument<String>("newName") ?: "" val newName = call.argument<String>("newName") ?: ""
try { try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") // FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAsrHelper.renameFile(filePath, newName) if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
recordfile?.renameFile(filePath, newName)
}else{
azureAsrHelper.renameFile(filePath, newName)
}
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null) result.error("ENABLERECORD_ERROR", e.message, null)
@ -458,8 +464,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val destPath = call.argument<String>("destPath") ?: "" val destPath = call.argument<String>("destPath") ?: ""
try { try {
FileLogger.d(tag, "音频文件名称为: ${sourcePath}") // FileLogger.d(tag, "音频文件名称为: ${sourcePath}") //
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
azureAsrHelper.moveFile(sourcePath, destPath) isRecord = false
recordfile?.moveFile(sourcePath, destPath)
}else{
azureAsrHelper.moveFile(sourcePath, destPath)
}
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null) result.error("ENABLERECORD_ERROR", e.message, null)
@ -470,11 +480,28 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val filePath = call.argument<String>("filePath") ?: "" val filePath = call.argument<String>("filePath") ?: ""
// 是否接受音频数据 // 是否接受音频数据
val acceptAudioData = call.argument<Boolean>("acceptAudioData") ?: false val acceptAudioData = call.argument<Boolean>("acceptAudioData") ?: false
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
try { try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
if(recordfile == null){
recordfile = RecordFile()
}
recordfile?.closeFile(true)
recordfile?.creatingFiles(filePath)
isRecord = true
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
// 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调
}
FileLogger.d(tag, "音频文件名称为: ${filePath}") FileLogger.d(tag, "音频文件名称为: ${filePath}")
// 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调 azureAsrHelper.enableRecord(audioSourceType, filePath, if(acceptAudioData) {
azureAsrHelper.enableRecord(filePath, if(acceptAudioData) {
// 创建音频数据回调,将音频数据发送到 Flutter 层 // 创建音频数据回调,将音频数据发送到 Flutter 层
object : SimpleAudioReceiver.AudioDataCallback { object : SimpleAudioReceiver.AudioDataCallback {
override fun onAudio(audioData: ByteArray) { override fun onAudio(audioData: ByteArray) {
@ -499,18 +526,36 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
"pauseRecord" -> { "pauseRecord" -> {
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
}else{
azureAsrHelper.pauseRecord()
azureAsrHelper.pauseRecord() azureAsrHelper.pauseRecord()
}
result.success(true) result.success(true)
} }
"resumeRecord" -> { "resumeRecord" -> {
azureAsrHelper.resumeRecord() if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = true
}else{
azureAsrHelper.resumeRecord()
}
result.success(true) result.success(true)
} }
"stopRecord" -> { "stopRecord" -> {
val isSave = call.argument<Boolean>("isSave") ?: false val isSave = call.argument<Boolean>("isSave") ?: false
try { try {
azureAsrHelper.stopRecord(isSave) if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
recordfile?.closeFile(true)
}
else
{
azureAsrHelper.stopRecord(isSave)
}
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null) result.error("ENABLERECORD_ERROR", e.message, null)
@ -521,7 +566,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val sampleRate = call.argument<Int>("sampleRate") ?: 16000 val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val channels = call.argument<Int>("channels") ?: 1 val channels = call.argument<Int>("channels") ?: 1
try { try {
azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels) // azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels)
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
result.error("SET_AUDIO_CONFIG_ERROR", e.message, null) result.error("SET_AUDIO_CONFIG_ERROR", e.message, null)
@ -688,17 +733,17 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) { when (call.method) {
"enableRecord" -> { // "enableRecord" -> {
val filePath = call.argument<String>("filePath") ?: "" // val filePath = call.argument<String>("filePath") ?: ""
try { // try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") // // FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAstHelper.enableRecord(filePath) // //azureAstHelper.enableRecord(filePath)
result.success(true) // result.success(true)
} catch (e: Exception) { // } catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null) // result.error("ENABLERECORD_ERROR", e.message, null)
} // }
} // }
"startContinuousTranslation" -> { "startContinuousTranslation" -> {
try { try {
FileLogger.d(tag, "开启翻译") FileLogger.d(tag, "开启翻译")
@ -996,20 +1041,70 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
override fun onConnectionStateChanged(state: Int) { override fun onConnectionStateChanged(state: Int) {
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray,channel:Int) {
if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { if (channel == 0) {
return
}
else if (channel == 1) {
if (isRecord) {
recordfile?.setAudioConfig(16000,1)
recordfile?.saveAudioDataToWav(data)
// 构建音频数据事件映射
val audioEvent = mapOf(
"type" to "audioData",
"data" to data,
"timestamp" to System.currentTimeMillis(),
"size" to data.size
)
// 发送音频数据事件到 Flutter 层
sendAudioDataEvent(audioEvent)
}
if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) {
azureAsrHelper.audioStream?.saveAudioDataTo(data) azureAsrHelper.audioStream?.saveAudioDataTo(data)
} }
// 可选:处理音频数据 }
} else if (channel == 2) {
if (isRecord) {
override fun onAudioDataReceivedCall(data: ByteArray) { recordfile?.setAudioConfig(16000,2)
recordfile?.saveAudioDataToWav(data)
// 构建音频数据事件映射
val audioEvent = mapOf(
"type" to "audioData",
"data" to data,
"timestamp" to System.currentTimeMillis(),
"size" to data.size
)
// 发送音频数据事件到 Flutter 层
sendAudioDataEvent(audioEvent)
}
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]
}
// FileLogger.d(tag, "onAudioDataReceivedCall: ${data.size}")
azureAstHelper.pushAudioData(data) if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) {
azureAsrHelper.audioStream?.saveAudioDataTo(rightBuffer)
}
azureAstHelper?.pushAudioData(leftBuffer)
}
// 可选:处理音频数据 // 可选:处理音频数据
} }
/** /**
* 处理唤醒信号 * 处理唤醒信号
* 在收到唤醒信号时启动语音识别 * 在收到唤醒信号时启动语音识别

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

@ -117,10 +117,10 @@ class RecordFile {
fos = FileOutputStream(currentAudioFile, true) fos = FileOutputStream(currentAudioFile, true)
startWriteThread() startWriteThread()
// 如果是双声道,创建左右声道文件 // // 如果是双声道,创建左右声道文件
if (channels == 2) { // if (channels == 2) {
createStereoChannelFiles(filePath) // createStereoChannelFiles(filePath)
} // }
} }
/** /**
@ -242,14 +242,14 @@ class RecordFile {
internal fun saveAudioDataToWav(buffer: ByteArray) { internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) return if (fos == null || currentAudioFile == null) return
if (channels == 2) { // if (channels == 2) {
// 双声道:拆分左右声道 // // 双声道:拆分左右声道
splitStereoToMono(buffer) // splitStereoToMono(buffer)
} else { // } else {
// 单声道:直接保存 // 单声道:直接保存
writeQueue.offer(buffer.copyOf()) writeQueue.offer(buffer.copyOf())
totalBytesWritten += buffer.size totalBytesWritten += buffer.size
} // }
} }
/** /**
@ -454,21 +454,22 @@ class RecordFile {
} }
} }
// 处理左右声道文件(如果是双声道) // // 处理左右声道文件(如果是双声道)
if (channels == 2) { // if (channels == 2) {
leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave) // leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave)
rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave) // rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave)
// 等待左右声道写入线程结束 // // 等待左右声道写入线程结束
leftChannelWriteThread?.join(500) // leftChannelWriteThread?.join(500)
rightChannelWriteThread?.join(500) // rightChannelWriteThread?.join(500)
} // }
return if (channels == 2) { // return if (channels == 2) {
leftChannelSuccess && rightChannelSuccess // leftChannelSuccess && rightChannelSuccess
} else { // } else {
mainFileSuccess // mainFileSuccess
} // }
return mainFileSuccess
} catch (e: Exception) { } catch (e: Exception) {
Log.e("tag", "更新WAV文件头失败: ${e.message}") Log.e("tag", "更新WAV文件头失败: ${e.message}")
return false return false

32
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt

@ -49,7 +49,7 @@ class SimpleAudioReceiver(private val context: Context) {
private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在 private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在
private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据 private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据
private var writeThread: Thread? = null private var writeThread: Thread? = null
private var isInitialized = false
// 音频配置 // 音频配置
private val channelConfig = AudioFormat.CHANNEL_IN_MONO private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
@ -59,6 +59,8 @@ class SimpleAudioReceiver(private val context: Context) {
// 录音文件处理 // 录音文件处理
var recordfile: RecordFile? = null var recordfile: RecordFile? = null
var isRecord =false
var isContinuousRecognitionActive = false
/** /**
* 获取最优音频格式 * 获取最优音频格式
*/ */
@ -94,8 +96,9 @@ class SimpleAudioReceiver(private val context: Context) {
// 初始化音频管理器 // 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
isInitialized = true
} }
fun isInitialized(): Boolean = isInitialized
/** /**
* 启动写入线程 * 启动写入线程
*/ */
@ -140,14 +143,14 @@ class SimpleAudioReceiver(private val context: Context) {
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try { try {
// Log.d(TAG, "写入数据大小: ${finalData.size}") Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}")
if(audioDataCallback!=null){ if(audioDataCallback!=null){
audioDataCallback?.onAudio(finalData) audioDataCallback?.onAudio(finalData)
} }
if(pushAudioStream!=null&&audioDataCallback==null){ if(pushAudioStream!=null&&isContinuousRecognitionActive){
pushAudioStream?.write(finalData) pushAudioStream?.write(finalData)
} }
if(recordfile!=null){ if(recordfile!=null&&isRecord){
recordfile?.saveAudioDataToWav(finalData) recordfile?.saveAudioDataToWav(finalData)
} }
@ -342,7 +345,23 @@ class SimpleAudioReceiver(private val context: Context) {
Log.e(TAG, "继续录音失败: ${e.message}") Log.e(TAG, "继续录音失败: ${e.message}")
} }
} }
/**
* 暂停麦克风捕获
*/
fun pauseMicrophoneCapture() {
try {
isWriting.set(false)
// 停止录音
audioRecord?.stop()
// 恢复音频模式 这里会导致关闭麦克风卡住待优化
//restoreCommunicationAudioMode()
} catch (e: Exception) {
Log.e(TAG, "暂停麦克风捕获失败: ${e.message}")
}
}
/** /**
* 停止麦克风捕获 * 停止麦克风捕获
*/ */
@ -376,6 +395,7 @@ class SimpleAudioReceiver(private val context: Context) {
// 释放录音实例 // 释放录音实例
audioRecord?.release() audioRecord?.release()
audioRecord = null audioRecord = null
isInitialized = false
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "释放音频资源失败: ${e.message}") Log.e(TAG, "释放音频资源失败: ${e.message}")
e.printStackTrace() e.printStackTrace()

128
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -39,7 +39,7 @@ public class AzureAsrHelper: NSObject {
// 防抖机制相关变量 - 只保留开始操作的防抖 // 防抖机制相关变量 - 只保留开始操作的防抖
private var lastAudioStartTime: Int64 = 0 private var lastAudioStartTime: Int64 = 0
private let audioOperationDebounceInterval: Int64 = 500 // 500毫秒防抖间隔 private let audioOperationDebounceInterval: Int64 = 0 // 500毫秒防抖间隔
private var audioStartWorkItem: DispatchWorkItem? private var audioStartWorkItem: DispatchWorkItem?
// 串行队列和状态标记,用于顺序控制音频启动/停止,避免竞态 // 串行队列和状态标记,用于顺序控制音频启动/停止,避免竞态
private let audioOpQueue = DispatchQueue(label: "com.azure.speech.audio.operations") private let audioOpQueue = DispatchQueue(label: "com.azure.speech.audio.operations")
@ -47,8 +47,7 @@ public class AzureAsrHelper: NSObject {
private var isAudioStarted = false private var isAudioStarted = false
private var pendingStopRequest = false private var pendingStopRequest = false
// 状态管理
private var _isContinuousRecognitionActive = false
// 配置参数 // 配置参数
private var currentLanguage = "zh-CN" private var currentLanguage = "zh-CN"
@ -66,7 +65,7 @@ public class AzureAsrHelper: NSObject {
case external case external
} }
private var audioSourceType = AudioSourceType.microphone public var audioSourceType = AudioSourceType.microphone
// 音频处理 // 音频处理
// private var externalAudioStream: ExternalAudioPullStream? // private var externalAudioStream: ExternalAudioPullStream?
@ -240,7 +239,7 @@ public class AzureAsrHelper: NSObject {
/// ///
/// 修复点: /// 修复点:
/// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效 /// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效
/// 2) 任务执行前二次校验:必须是当前挂起任务且 _isContinuousRecognitionActive 仍为 true 才执行启动 /// 2) 任务执行前二次校验:必须是当前挂起任务且 audioStream?.isContinuousRecognitionActive 仍为 true 才执行启动
private func startAudioRecordAsync( private func startAudioRecordAsync(
audioSourceType: AudioSourceType, audioSourceType: AudioSourceType,
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil,
@ -264,9 +263,9 @@ public class AzureAsrHelper: NSObject {
completion(false) completion(false)
return return
} }
// print("startAudioRecordAsync\(_isContinuousRecognitionActive)") // print("startAudioRecordAsync\(audioStream?.isContinuousRecognitionActive)")
// // 二次校验:stop 可能已发生,确认仍需启动 // // 二次校验:stop 可能已发生,确认仍需启动
// guard self._isContinuousRecognitionActive else { // guard self.audioStream?.isContinuousRecognitionActive else {
// os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info) // os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info)
// completion(false) // completion(false)
// return // return
@ -321,10 +320,12 @@ public class AzureAsrHelper: NSObject {
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? audioDataCallback: SimpleAudioReceiver.AudioDataCallback?
) -> Bool { ) -> Bool {
do { do {
audioStream?.startAudioRecord( if(!audioStream!.isContinuousRecognitionActive){
audioSourceType: audioSourceType == .microphone ? .microphone : .external, audioStream?.startAudioRecord(
audioDataCallback: audioDataCallback audioSourceType: audioSourceType == .microphone ? .microphone : .external,
) audioDataCallback: audioDataCallback
)
}
os_log("音频录制启动成功", log: log, type: .info) os_log("音频录制启动成功", log: log, type: .info)
return true return true
} catch { } catch {
@ -382,7 +383,7 @@ public class AzureAsrHelper: NSObject {
* @param audioDataCallback 音频数据回调 * @param audioDataCallback 音频数据回调
* @return 是否成功开始识别(调度成功即返回 true) * @return 是否成功开始识别(调度成功即返回 true)
* *
* 修复点:在音频启动成功后的闭包中,先判断 _isContinuousRecognitionActive 是否仍然为 true, * 修复点:在音频启动成功后的闭包中,先判断 audioStream?.isContinuousRecognitionActive 是否仍然为 true,
* 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。 * 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。
*/ */
public func startContinuousRecognition( public func startContinuousRecognition(
@ -399,7 +400,7 @@ public class AzureAsrHelper: NSObject {
return false return false
} }
if _isContinuousRecognitionActive { if audioStream?.isContinuousRecognitionActive == true {
print("连续识别已在进行中") print("连续识别已在进行中")
return true return true
} }
@ -421,7 +422,7 @@ public class AzureAsrHelper: NSObject {
print("startContinuousRecognition:\(audioSourceType)") print("startContinuousRecognition:\(audioSourceType)")
// 移除这里的状态设置,等音频启动成功后再设置 // 移除这里的状态设置,等音频启动成功后再设置
// self._isContinuousRecognitionActive = true // self.audioStream?.isContinuousRecognitionActive = true
// 异步启动音频处理 // 异步启动音频处理
startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in
@ -429,8 +430,8 @@ public class AzureAsrHelper: NSObject {
if success { if success {
// 只有在音频启动成功后才设置状态为 true // 只有在音频启动成功后才设置状态为 true
self._isContinuousRecognitionActive = true self.audioStream?.isContinuousRecognitionActive = true
print("startContinuousRecognition:\(self._isContinuousRecognitionActive)") print("startContinuousRecognition:\(self.audioStream?.isContinuousRecognitionActive)")
// 启动语音识别 // 启动语音识别
do { do {
@ -439,18 +440,18 @@ public class AzureAsrHelper: NSObject {
} catch { } catch {
os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription) os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
self.stopContinuousRecognition() self.stopContinuousRecognition()
self._isContinuousRecognitionActive = false self.audioStream?.isContinuousRecognitionActive = false
} }
} else { } else {
os_log("音频启动失败,无法开始识别", log: self.log, type: .error) os_log("音频启动失败,无法开始识别", log: self.log, type: .error)
self._isContinuousRecognitionActive = false self.audioStream?.isContinuousRecognitionActive = false
} }
} }
return true return true
} catch { } catch {
stopContinuousRecognition() stopContinuousRecognition()
_isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
return false return false
} }
} }
@ -478,7 +479,7 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return } guard let self = self else { return }
if success { if success {
// 标记不再活跃,供启动任务二次校验 // 标记不再活跃,供启动任务二次校验
self._isContinuousRecognitionActive = false self.audioStream?.isContinuousRecognitionActive = false
os_log("音频停止成功", log: self.log, type: .info) os_log("音频停止成功", log: self.log, type: .info)
} else { } else {
os_log("音频停止失败", log: self.log, type: .error) os_log("音频停止失败", log: self.log, type: .error)
@ -497,7 +498,7 @@ public class AzureAsrHelper: NSObject {
return true return true
} catch { } catch {
// 强制重置状态 // 强制重置状态
_isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
// 关键:即使异常,也取消未执行的启动任务 // 关键:即使异常,也取消未执行的启动任务
cancelPendingAudioStart() cancelPendingAudioStart()
@ -532,7 +533,7 @@ public class AzureAsrHelper: NSObject {
* 检查连续识别是否活跃 * 检查连续识别是否活跃
*/ */
public func isContinuousRecognitionActive() -> Bool { public func isContinuousRecognitionActive() -> Bool {
return self._isContinuousRecognitionActive return self.audioStream?.isContinuousRecognitionActive == true
} }
/** /**
@ -542,10 +543,10 @@ public class AzureAsrHelper: NSObject {
do { do {
print("释放所有资源:") print("释放所有资源:")
// 如果正在进行连续识别,先停止 // 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive { if audioStream?.isContinuousRecognitionActive == true {
// 直接停止,不等待结果 // 直接停止,不等待结果
try? recognizer?.stopContinuousRecognition() try? recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
} }
// 取消防抖任务 - 只取消开始操作的防抖 // 取消防抖任务 - 只取消开始操作的防抖
audioStartWorkItem?.cancel() audioStartWorkItem?.cancel()
@ -559,11 +560,11 @@ public class AzureAsrHelper: NSObject {
audioConfig = nil audioConfig = nil
audioStream = nil audioStream = nil
// 确保状态被重置 // 确保状态被重置
_isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
//externalAudioStream = nil //externalAudioStream = nil
} catch { } catch {
// 确保状态被重置 // 确保状态被重置
_isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
//externalAudioStream = nil //externalAudioStream = nil
audioConfig = nil audioConfig = nil
recognizer = nil recognizer = nil
@ -599,13 +600,13 @@ public class AzureAsrHelper: NSObject {
*/ */
private func setupRecognizer() -> Bool { private func setupRecognizer() -> Bool {
do { do {
print("设置识别器\(_isContinuousRecognitionActive)") print("设置识别器\(audioStream?.isContinuousRecognitionActive ?? false)")
// 如果正在进行连续识别,先停止 // 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive { if audioStream?.isContinuousRecognitionActive == true {
try? recognizer?.stopContinuousRecognition() try? recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
} }
// 【优化】只有在音频流未初始化时才创建,避免重复初始化 // 【优化】只有在音频流未初始化时才创建,避免重复初始化
if audioStream == nil || audioConfig == nil { if audioStream == nil || audioConfig == nil {
@ -741,7 +742,7 @@ public class AzureAsrHelper: NSObject {
self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang) self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang)
// 如果正在识别,需要重新设置识别器 // 如果正在识别,需要重新设置识别器
if _isContinuousRecognitionActive { if audioStream?.isContinuousRecognitionActive == true {
let wasActive = stopContinuousRecognition() let wasActive = stopContinuousRecognition()
if wasActive { if wasActive {
return setupRecognizer() return setupRecognizer()
@ -815,7 +816,7 @@ public class AzureAsrHelper: NSObject {
print("会话结束事件:") print("会话结束事件:")
// 直接在当前线程调用回调 // 直接在当前线程调用回调
callback.onSessionStopped() callback.onSessionStopped()
self._isContinuousRecognitionActive = false self.audioStream?.isContinuousRecognitionActive = false
//self.stopAudioProcessing() //self.stopAudioProcessing()
} }
@ -865,12 +866,17 @@ public class AzureAsrHelper: NSObject {
/** /**
* 开启录音 * 开启录音
* @param audioSourceType 音频源类型
* @param filePath 录音文件路径 * @param filePath 录音文件路径
* @param audioDataCallback 音频数据回调接口 * @param audioDataCallback 音频数据回调接口
*/ */
public func enableRecord(filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil) { public func enableRecord( audioSourceType: AudioSourceType = .microphone,filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil) {
os_log("开启录音: %{public}@", log: log, type: .info, filePath) os_log("开启录音: %{public}@", log: log, type: .info, filePath)
self.audioSourceType = audioSourceType
if(audioSourceType == .external){
os_log("外部音频源不在这里录音", log: log, type: .info)
return
}
if audioStream == nil { if audioStream == nil {
// 创建外部音频拉流对象 // 创建外部音频拉流对象
audioStream = SimpleAudioReceiver() audioStream = SimpleAudioReceiver()
@ -881,21 +887,17 @@ public class AzureAsrHelper: NSObject {
audioStream?.recordfile = RecordFile() audioStream?.recordfile = RecordFile()
} }
self.audioSourceType = .microphone
// 修复:移除多余的 audioDataCallback 参数 // 修复:移除多余的 audioDataCallback 参数
// 修复:添加类型转换 // 修复:添加类型转换
audioStream?.startAudioRecord( if(!audioStream!.isContinuousRecognitionActive){
audioSourceType: { // 异步启动音频处理
switch audioSourceType { startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in
case .microphone: guard let self = self else { return }
return SimpleAudioReceiver.AudioSourceType.microphone
case .external: }
return SimpleAudioReceiver.AudioSourceType.external }
} audioStream?.isRecord = true
}(),
audioDataCallback: audioDataCallback
)
audioStream?.recordfile?.closeFile(isSave: true) audioStream?.recordfile?.closeFile(isSave: true)
audioStream?.recordfile?.creatingFiles(atPath: filePath) audioStream?.recordfile?.creatingFiles(atPath: filePath)
} }
@ -905,7 +907,10 @@ public class AzureAsrHelper: NSObject {
*/ */
public func moveFile(sourcePath: String, destPath: String)-> Bool { public func moveFile(sourcePath: String, destPath: String)-> Bool {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里移动文件", log: log, type: .info)
return true
}
audioStream?.recordfile?.moveFile(from: sourcePath, to: destPath) // Fixed method call audioStream?.recordfile?.moveFile(from: sourcePath, to: destPath) // Fixed method call
return true return true
} }
@ -915,6 +920,10 @@ public class AzureAsrHelper: NSObject {
* 重命名指定路径的音频文件 * 重命名指定路径的音频文件
*/ */
public func renameFile(filePath: String, newName: String)-> Bool { public func renameFile(filePath: String, newName: String)-> Bool {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里重命名文件", log: log, type: .info)
return true
}
audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call
return true return true
} }
@ -925,32 +934,47 @@ public class AzureAsrHelper: NSObject {
* 暂停录音 * 暂停录音
*/ */
public func pauseRecord() { public func pauseRecord() {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里暂停录音", log: log, type: .info)
return
}
os_log("暂停录音", log: log, type: .info) os_log("暂停录音", log: log, type: .info)
guard let audioStream = audioStream, audioStream.recordfile != nil else { guard let audioStream = audioStream, audioStream.recordfile != nil else {
return return
} }
audioStream.stopMicrophoneCapture() audioStream.isRecord = false
} }
/** /**
* 继续录音 * 继续录音
*/ */
public func resumeRecord() { public func resumeRecord() {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里继续录音", log: log, type: .info)
return
}
os_log("继续录音", log: log, type: .info) os_log("继续录音", log: log, type: .info)
guard let audioStream = audioStream, audioStream.recordfile != nil else { guard let audioStream = audioStream, audioStream.recordfile != nil else {
return return
} }
audioStream.resumeRecord() audioStream.isRecord = true
} }
/** /**
* 关闭录音 * 关闭录音
*/ */
public func stopRecord(isSave: Bool) { public func stopRecord(isSave: Bool) {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里关闭录音", log: log, type: .info)
return
}
guard let audioStream = audioStream, audioStream.recordfile != nil else { guard let audioStream = audioStream, audioStream.recordfile != nil else {
return return
} }
audioStream.stopMicrophoneCapture() audioStream.isRecord = false
if (!audioStream.isContinuousRecognitionActive) {
audioStream.stopMicrophoneCapture()
}
audioStream.recordfile?.isPause = false // audioStream.recordfile?.isPause = false //
audioStream.recordfile?.closeFile(isSave: true) // audioStream.recordfile?.closeFile(isSave: true) //
} }

175
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -49,10 +49,6 @@ import ble_service
} }
// 替换原有的 StreamHandler 类 // 替换原有的 StreamHandler 类
private typealias AsrEventStreamHandler = BaseEventStreamHandler
private typealias TtsEventStreamHandler = BaseEventStreamHandler
private typealias AudioDataEventStreamHandler = BaseEventStreamHandler
private typealias AstEventStreamHandler = BaseEventStreamHandler
// 事件类型枚举 // 事件类型枚举
@ -72,6 +68,11 @@ private typealias AstEventStreamHandler = BaseEventStreamHandler
let methodHandler: (FlutterMethodCall, @escaping FlutterResult) -> Void let methodHandler: (FlutterMethodCall, @escaping FlutterResult) -> Void
} }
private typealias AsrEventStreamHandler = BaseEventStreamHandler
private typealias TtsEventStreamHandler = BaseEventStreamHandler
private typealias AudioDataEventStreamHandler = BaseEventStreamHandler
private typealias AstEventStreamHandler = BaseEventStreamHandler
// ASR相关 // ASR相关
private var asrChannel: FlutterMethodChannel? private var asrChannel: FlutterMethodChannel?
private var asrEventChannel: FlutterEventChannel? private var asrEventChannel: FlutterEventChannel?
@ -96,8 +97,9 @@ private typealias AstEventStreamHandler = BaseEventStreamHandler
private var audioDataEventChannel: FlutterEventChannel? private var audioDataEventChannel: FlutterEventChannel?
internal var audioDataEventSink: FlutterEventSink? internal var audioDataEventSink: FlutterEventSink?
// 录音相关
public var recordfile: RecordFile?
private var isRecord = false
// 创建事件回调 // 创建事件回调
private var astEventCallback: AstEventCallback? private var astEventCallback: AstEventCallback?
// 是否已添加AST事件监听器 // 是否已添加AST事件监听器
@ -391,8 +393,14 @@ private func sendAstEvent(_ event: [String: Any]) {
return return
} }
do { do {
print("音频文件名称为: (filePath)") print("renameFile音频文件名称为: \(filePath),新名称为: \(newName)")
try azureAsrHelper.renameFile(filePath: filePath, newName: newName) if(azureAsrHelper.audioSourceType == .external){
try recordfile?.renameFile(at: filePath, to: newName)
}
else
{
try azureAsrHelper.renameFile(filePath: filePath, newName: filePath)
}
result(true) result(true)
} catch { } catch {
result(FlutterError(code: "RENAMEFILE_ERROR", message: error.localizedDescription, details: nil)) result(FlutterError(code: "RENAMEFILE_ERROR", message: error.localizedDescription, details: nil))
@ -406,8 +414,13 @@ private func sendAstEvent(_ event: [String: Any]) {
return return
} }
do { do {
print("音频文件名称为: (sourcePath)") print("moveFile音频文件名称为: \(sourcePath),新名称为: \(destPath)")
try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath) if(azureAsrHelper.audioSourceType == .external){
try recordfile?.moveFile(from: sourcePath, to: destPath)
}
else{
try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath)
}
result(true) result(true)
} catch { } catch {
result(FlutterError(code: "MOVEFILE_ERROR", message: error.localizedDescription, details: nil)) result(FlutterError(code: "MOVEFILE_ERROR", message: error.localizedDescription, details: nil))
@ -422,13 +435,28 @@ private func sendAstEvent(_ event: [String: Any]) {
// 检查是否需要接受音频数据 // 检查是否需要接受音频数据
let acceptAudioData = args["acceptAudioData"] as? Bool ?? false let acceptAudioData = args["acceptAudioData"] as? Bool ?? false
// 检查是否需要接受音频数据
let isExternalActive = args["audioSourceType"] as? Bool ?? false
if isExternalActive {
if(recordfile == nil){
recordfile = RecordFile()
}
recordfile?.closeFile(isSave: true)
recordfile?.creatingFiles(atPath: filePath)
isRecord = true
}
// 根据参数确定音频源类型
let audioSourceType = isExternalActive ?
AzureAsrHelper.AudioSourceType.external :
AzureAsrHelper.AudioSourceType.microphone
print("startContinuousRecognition: \(audioSourceType)")
do { do {
if acceptAudioData { if acceptAudioData {
// 使用复用的音频数据回调实例 // 使用复用的音频数据回调实例
try azureAsrHelper.enableRecord(filePath: filePath, audioDataCallback: audioDataCallback) try azureAsrHelper.enableRecord(audioSourceType: audioSourceType, filePath: filePath, audioDataCallback: audioDataCallback)
} else { } else {
try azureAsrHelper.enableRecord(filePath: filePath) try azureAsrHelper.enableRecord(audioSourceType: audioSourceType, filePath: filePath)
} }
result(true) result(true)
} catch { } catch {
@ -438,22 +466,34 @@ private func sendAstEvent(_ event: [String: Any]) {
case "pauseRecord": case "pauseRecord":
isRecord = false
azureAsrHelper.pauseRecord() azureAsrHelper.pauseRecord()
result(true) result(true)
case "resumeRecord": case "resumeRecord":
isRecord = true
azureAsrHelper.resumeRecord() azureAsrHelper.resumeRecord()
result(true) result(true)
case "stopRecord": case "stopRecord":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let isSave = args["isSave"] as? Bool else { let isSave = args["isSave"] as? Bool else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "isSave 参数不能为空", details: nil)) result(FlutterError(code: "INVALID_ARGUMENTS", message: "isSave 参数不能为空", details: nil))
return return
} }
do { do {
print("stopRecord : \(azureAsrHelper.audioSourceType)")
try azureAsrHelper.stopRecord(isSave: isSave) if(azureAsrHelper.audioSourceType == .external){
print("stopRecord音频文件名称为:)")
isRecord = false
recordfile?.closeFile(isSave: true)
}
else
{
try azureAsrHelper.stopRecord(isSave: isSave)
}
result(true) result(true)
} catch { } catch {
result(FlutterError(code: "STOPRECORD_ERROR", message: error.localizedDescription, details: nil)) result(FlutterError(code: "STOPRECORD_ERROR", message: error.localizedDescription, details: nil))
@ -595,20 +635,20 @@ private func sendAstEvent(_ event: [String: Any]) {
// MARK: - AST 方法处理 // MARK: - AST 方法处理
private func handleAstMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { private func handleAstMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method { switch call.method {
case "enableRecord": // case "enableRecord":
guard let args = call.arguments as? [String: Any], // guard let args = call.arguments as? [String: Any],
let filePath = args["filePath"] as? String else { // let filePath = args["filePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing filePath", details: nil)) // result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing filePath", details: nil))
return // return
} // }
do { // do {
os_log("音频文件名称为: %@", log: log, type: .info, filePath) // os_log("音频文件名称为: %@", log: log, type: .info, filePath)
try azureAstHelper.enableRecord(filePath: filePath) // try azureAstHelper.enableRecord(filePath: filePath)
result(true) // result(true)
} catch { // } catch {
result(FlutterError(code: "ENABLERECORD_ERROR", message: error.localizedDescription, details: nil)) // result(FlutterError(code: "ENABLERECORD_ERROR", message: error.localizedDescription, details: nil))
} // }
case "startContinuousTranslation": case "startContinuousTranslation":
do { do {
@ -935,28 +975,79 @@ extension AzureSpeechPlugin: BleService.Callback {
/// 接收到音频数据回调 /// 接收到音频数据回调
/// - Parameter data: 音频数据 /// - Parameter data: 音频数据
public func onAudioDataReceived(data: Data) { public func onAudioDataReceived(data: Data, channel: Int32) {
// 安全解包版本 if channel == 0 {
return
} else if channel == 1 {
if (isRecord) {
recordfile?.setAudioConfig(sampleRate: 16000, channels: 1)
recordfile?.saveAudioDataToWav(data)
handleAudioData(data)
}
// 安全解包版本
guard let audioStream = azureAsrHelper.audioStream else { guard let audioStream = azureAsrHelper.audioStream else {
os_log("音频流未初始化", type: .error) os_log("音频流未初始化", type: .error)
return return
} }
// print("liwei--------------接收到音频数据回调") azureAsrHelper.audioStream?.saveAudioDataTo(data: data)
audioStream.saveAudioDataTo(data: data) }
} else if channel == 2 {
if (isRecord) {
/// 接收到音频数据回调1 recordfile?.setAudioConfig(sampleRate: 16000, channels: 2)
/// - Parameter data: 音频数据 recordfile?.saveAudioDataToWav(data)
public func onAudioDataReceivedCall(data: Data) { handleAudioData(data)
// 安全解包版本 }
guard let audioStream = azureAstHelper.audioProcessor else { let sampleCount = data.count / 4 // 每个样本4字节(左右声道各2字节)
guard sampleCount > 0 else {
print("双声道数据样本数为0")
return
}
var leftBuffer = Data()
var rightBuffer = Data()
leftBuffer.reserveCapacity(sampleCount * 2)
rightBuffer.reserveCapacity(sampleCount * 2)
data.withUnsafeBytes { bytes in
let uint8Ptr = bytes.bindMemory(to: UInt8.self)
for i in 0..<sampleCount {
let stereoIndex = i * 4
// 左声道(低位字节在前,高位字节在后)
leftBuffer.append(uint8Ptr[stereoIndex])
leftBuffer.append(uint8Ptr[stereoIndex + 1])
// 右声道
rightBuffer.append(uint8Ptr[stereoIndex + 2])
rightBuffer.append(uint8Ptr[stereoIndex + 3])
}
}
print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes")
// 安全解包版本
guard let audioStream = azureAsrHelper.audioStream else {
os_log("音频流未初始化", type: .error) os_log("音频流未初始化", type: .error)
return return
} }
azureAsrHelper.audioStream?.saveAudioDataTo(data: rightBuffer)
// // 安全解包版本
// guard let audioProcessor = azureAstHelper.audioProcessor else {
// os_log("音频流未初始化", type: .error)
// return
// }
// print("liwei--------------接收到音频数据回调1") // print("liwei--------------接收到音频数据回调1")
audioStream.saveAudioDataTo(data: data) azureAstHelper.pushAudioData(audioData: leftBuffer)
}
} }
/// 接收到编码数据回调 /// 接收到编码数据回调
/// - Parameter data: 编码数据 /// - Parameter data: 编码数据
public func onEncodedDataReceived(data: Data) { public func onEncodedDataReceived(data: Data) {

5
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift

@ -331,6 +331,7 @@ import os.log
private func initializeAudioProcessor() { private func initializeAudioProcessor() {
audioProcessor = SimpleAudioReceiver() audioProcessor = SimpleAudioReceiver()
audioProcessor?.initAudioRecord() audioProcessor?.initAudioRecord()
// 检查音频配置是否已存在 // 检查音频配置是否已存在
if audioConfig == nil, let pushStream = audioProcessor?.pushAudioStream { if audioConfig == nil, let pushStream = audioProcessor?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream) audioConfig = SPXAudioConfiguration(streamInput: pushStream)
@ -547,6 +548,7 @@ import os.log
* 开始连续语音翻译 * 开始连续语音翻译
*/ */
public func startContinuousTranslation() -> Bool { public func startContinuousTranslation() -> Bool {
audioProcessor?.isContinuousRecognitionActive = true
os_log("尝试启动连续翻译,当前状态:初始化=%@, 识别中=%@", log: log, type: .info, os_log("尝试启动连续翻译,当前状态:初始化=%@, 识别中=%@", log: log, type: .info,
serviceState.isInitialized ? "true" : "false", serviceState.isInitialized ? "true" : "false",
serviceState.isRecognizing ? "true" : "false") serviceState.isRecognizing ? "true" : "false")
@ -597,7 +599,8 @@ import os.log
*/ */
public func stopContinuousTranslation() { public func stopContinuousTranslation() {
do { do {
self.serviceState.isRecognizing = false audioProcessor?.isContinuousRecognitionActive = false
self.serviceState.isRecognizing = false
try recognizer?.stopContinuousRecognition() try recognizer?.stopContinuousRecognition()
audioProcessor?.stopMicrophoneCapture() audioProcessor?.stopMicrophoneCapture()
recordFile?.closeFile(isSave: true) recordFile?.closeFile(isSave: true)

226
local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift

@ -10,10 +10,11 @@ public class MicrophoneCapture: NSObject {
*/ */
public static let shared = MicrophoneCapture() public static let shared = MicrophoneCapture()
private var audioEngine: AVAudioEngine! // 修复:将强制解包改为可选类型,避免内存访问错误
private var audioSession: AVAudioSession! private var audioEngine: AVAudioEngine?
private var audioSession: AVAudioSession?
private var audioFormat: AVAudioFormat? private var audioFormat: AVAudioFormat?
private var audioInputNode: AVAudioInputNode! private var audioInputNode: AVAudioInputNode?
private var sampleRate: Double = 16000 // 修改为16000采样率,符合Microsoft要求 private var sampleRate: Double = 16000 // 修改为16000采样率,符合Microsoft要求
public var isCapturing = false public var isCapturing = false
public var hasBluetoothDevices = false public var hasBluetoothDevices = false
@ -84,8 +85,13 @@ public class MicrophoneCapture: NSObject {
audioFormat = getOptimalAudioFormat() audioFormat = getOptimalAudioFormat()
audioSession = AVAudioSession.sharedInstance() audioSession = AVAudioSession.sharedInstance()
// 修复:添加安全检查
guard let audioSession = audioSession else {
print("音频会话初始化失败")
return
}
do { do {
try audioSession.setPreferredSampleRate(sampleRate) try audioSession.setPreferredSampleRate(sampleRate)
try audioSession.setPreferredIOBufferDuration(0.005) // 5ms缓冲 try audioSession.setPreferredIOBufferDuration(0.005) // 5ms缓冲
@ -121,10 +127,11 @@ public class MicrophoneCapture: NSObject {
* 配置蓝牙模式的音频路由:末端在手机,输出端在耳机 * 配置蓝牙模式的音频路由:末端在手机,输出端在耳机
*/ */
private func configureAudioForBluetoothMode() { private func configureAudioForBluetoothMode() {
guard let availableInputs = audioSession.availableInputs else { return } guard let audioSession = audioSession,
let availableInputs = audioSession.availableInputs else { return }
// 强制使用内置麦克风作为输入(手机) // 强制使用内置麦克风作为输入(手机)
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) { if let builtInMic = availableInputs.first(where: { $0.portType == AVAudioSession.Port.builtInMic }) {
do { do {
try audioSession.setPreferredInput(builtInMic) try audioSession.setPreferredInput(builtInMic)
print("蓝牙模式:音频输入设置为内置麦克风(手机)") print("蓝牙模式:音频输入设置为内置麦克风(手机)")
@ -139,7 +146,7 @@ public class MicrophoneCapture: NSObject {
// 确保音频输出通过蓝牙耳机 // 确保音频输出通过蓝牙耳机
let hasBluetoothOutput = currentRoute.outputs.contains { output in let hasBluetoothOutput = currentRoute.outputs.contains { output in
[.bluetoothHFP, .bluetoothLE].contains(output.portType) [AVAudioSession.Port.bluetoothHFP, AVAudioSession.Port.bluetoothLE].contains(where: { $0 == output.portType })
} }
if hasBluetoothOutput { if hasBluetoothOutput {
@ -164,10 +171,11 @@ public class MicrophoneCapture: NSObject {
* 配置手机模式的音频路由:末端和输出端都在手机 * 配置手机模式的音频路由:末端和输出端都在手机
*/ */
private func configureAudioForPhoneMode() { private func configureAudioForPhoneMode() {
guard let availableInputs = audioSession.availableInputs else { return } guard let audioSession = audioSession,
let availableInputs = audioSession.availableInputs else { return }
// 使用内置麦克风作为输入(手机) // 使用内置麦克风作为输入(手机)
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) { if let builtInMic = availableInputs.first(where: { $0.portType == AVAudioSession.Port.builtInMic }) {
do { do {
// 设置音频会话参数 // 设置音频会话参数
try audioSession.setCategory(.playAndRecord, try audioSession.setCategory(.playAndRecord,
@ -199,6 +207,9 @@ public class MicrophoneCapture: NSObject {
public func startCapture() throws { public func startCapture() throws {
print("开始捕获\(isCapturing)") print("开始捕获\(isCapturing)")
guard !isCapturing else { return } guard !isCapturing else { return }
guard let audioSession = audioSession else {
throw NSError(domain: "音频会话未初始化", code: -1)
}
// 检查麦克风权限 // 检查麦克风权限
switch audioSession.recordPermission { switch audioSession.recordPermission {
@ -250,20 +261,24 @@ public class MicrophoneCapture: NSObject {
* 根据Microsoft音频堆栈要求优化格式转换 * 根据Microsoft音频堆栈要求优化格式转换
*/ */
private func setupAudioEngine() throws { private func setupAudioEngine() throws {
// 修复:先安全清理现有资源
cleanupAudioEngine()
audioEngine = AVAudioEngine() audioEngine = AVAudioEngine()
guard let audioEngine = audioEngine else {
throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "音频引擎初始化失败"])
}
audioInputNode = audioEngine.inputNode audioInputNode = audioEngine.inputNode
// 获取实际硬件采样率 guard let audioInputNode = audioInputNode else {
sampleRate = audioSession.sampleRate throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "音频输入节点获取失败"])
// 安全地停止音频引擎
if let engine = audioEngine {
engine.stop()
} }
// 安全地移除音频处理块 // 获取实际硬件采样率
if let inputNode = audioInputNode { if let audioSession = audioSession {
inputNode.removeTap(onBus: 0) sampleRate = audioSession.sampleRate
} }
// 设置音频格式 // 设置音频格式
@ -274,72 +289,56 @@ public class MicrophoneCapture: NSObject {
try audioInputNode.setVoiceProcessingEnabled(true) try audioInputNode.setVoiceProcessingEnabled(true)
} }
// 检查输入格式是否符合Microsoft要求 // 检查输入格式是否符合Microsoft要求
let needsConversion = !isMicrosoftCompatibleFormat(inputFormat) let needsConversion = !isMicrosoftCompatibleFormat(inputFormat)
// if needsConversion { // 需要格式转换
// 需要格式转换 guard let targetFormat = audioFormat,
guard let targetFormat = audioFormat, let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else {
let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else { throw NSError(domain: "AudioSetup", code: 2, userInfo: [NSLocalizedDescriptionKey: "音频格式转换器创建失败"])
throw NSError(domain: "AudioSetup", code: 2) }
}
print("输入格式不符合Microsoft要求,进行格式转换")
print("输入格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)")
print("目标格式: \(targetFormat.sampleRate)Hz, \(targetFormat.commonFormat.rawValue)")
// 添加tap进行格式转换
audioInputNode.installTap(onBus: 0,
bufferSize: 1024,
format: inputFormat) { [weak self] buffer, when in
print("输入格式不符合Microsoft要求,进行格式转换") guard let strongSelf = self else { return }
print("输入格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)")
print("目标格式: \(targetFormat.sampleRate)Hz, \(targetFormat.commonFormat.rawValue)")
// 添加tap进行格式转换 // 修复:添加安全检查,避免强制解包崩溃
audioInputNode.installTap(onBus: 0, guard let convertedBuffer = AVAudioPCMBuffer(
bufferSize: 1024, pcmFormat: targetFormat,
format: inputFormat) { [weak self] buffer, when in frameCapacity: AVAudioFrameCount(
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate
guard let strongSelf = self else { return }
// 创建目标格式的音频缓冲区
let convertedBuffer = AVAudioPCMBuffer(
pcmFormat: targetFormat,
frameCapacity: AVAudioFrameCount(
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate
)
)!
var error: NSError?
// 执行音频格式转换
let status = converter.convert(
to: convertedBuffer,
error: &error,
withInputFrom: { inNumPackets, outStatus in
outStatus.pointee = .haveData
return buffer
}
) )
) else {
// 转换成功且无错误 print("创建转换缓冲区失败")
if status == .haveData, error == nil { return
let data = strongSelf.audioBufferToData(convertedBuffer)
//print("转换后数据长度: \(data.count)")
strongSelf.audioDataHandler?(data)
}
} }
// } else {
// // 格式已符合Microsoft要求,直接使用
// print("输入格式符合Microsoft要求,无需转换")
// print("格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)")
// audioInputNode.installTap(onBus: 0, var error: NSError?
// bufferSize: 1024, // 执行音频格式转换
// format: inputFormat) { [weak self] buffer, when in let status = converter.convert(
to: convertedBuffer,
// guard let strongSelf = self else { return } error: &error,
//strongSelf.processAudioBuffer(buffer) withInputFrom: { inNumPackets, outStatus in
// // 直接处理原始音频数据 outStatus.pointee = .haveData
// let data = strongSelf.audioBufferToData(convertedBuffer) return buffer
// print("转换后数据长度: \(data.count)") }
// strongSelf.audioDataHandler?(data) )
// } // 转换成功且无错误
// } if status == .haveData, error == nil {
let data = strongSelf.audioBufferToData(convertedBuffer)
strongSelf.audioDataHandler?(data)
} else if let error = error {
print("音频格式转换失败: \(error.localizedDescription)")
}
}
// 启动引擎 // 启动引擎
do { do {
@ -347,9 +346,26 @@ public class MicrophoneCapture: NSObject {
isCapturing = true isCapturing = true
} catch { } catch {
print("音频引擎启动失败: \(error.localizedDescription)") print("音频引擎启动失败: \(error.localizedDescription)")
throw error
} }
} }
/**
* 安全清理音频引擎资源
* 防止内存访问错误
*/
private func cleanupAudioEngine() {
// 安全地停止音频引擎
audioEngine?.stop()
// 安全地移除音频处理块
audioInputNode?.removeTap(onBus: 0)
// 重置引用
audioInputNode = nil
audioEngine = nil
}
/** /**
* 将音频缓冲区转换为Data格式 * 将音频缓冲区转换为Data格式
* 支持16位整数和32位浮点格式 * 支持16位整数和32位浮点格式
@ -423,36 +439,36 @@ public class MicrophoneCapture: NSObject {
guard isCapturing else { return } guard isCapturing else { return }
isCapturing = false isCapturing = false
// 安全地停止音频引擎 // 修复:使用安全的清理方法
if let engine = audioEngine { cleanupAudioEngine()
engine.stop()
}
// 安全地移除音频处理块 // 安全地处理音频会话
if let inputNode = audioInputNode { guard let audioSession = audioSession else { return }
inputNode.removeTap(onBus: 0)
}
try? audioSession.setActive(false) do {
if self.hasBluetoothDevices { try audioSession.setActive(false)
// 设置音频会话参数
try? audioSession.setCategory(.playback, if self.hasBluetoothDevices {
mode: .videoChat, // 设置音频会话参数
options: [ try audioSession.setCategory(.playback,
mode: .videoChat,
.allowBluetoothA2DP, options: [
.allowBluetooth .allowBluetoothA2DP,
.allowBluetooth
]) // 添加音频优先级控制 ]) // 添加音频优先级控制
} else { } else {
try? audioSession.setCategory(.playback, try audioSession.setCategory(.playback,
mode: .videoChat, mode: .videoChat,
options: [ options: [
.mixWithOthers, .mixWithOthers,
.defaultToSpeaker]) .defaultToSpeaker])
}
try audioSession.overrideOutputAudioPort(.none)
try audioSession.setActive(true)
} catch {
print("音频会话配置失败: \(error.localizedDescription)")
} }
try? audioSession.overrideOutputAudioPort(.none)
try? audioSession.setActive(true)
} }
/** /**
@ -462,6 +478,10 @@ public class MicrophoneCapture: NSObject {
func reset() { func reset() {
stopCapture() stopCapture()
audioDataHandler = nil audioDataHandler = nil
// 修复:完全清理资源
cleanupAudioEngine()
audioSession = nil
audioFormat = nil
} }

66
local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift

@ -10,11 +10,47 @@ public class RecordFile {
private var isWriting = false private var isWriting = false
private var dataBuffer = Data() private var dataBuffer = Data()
private var totalBytesWritten = 0 private var totalBytesWritten = 0
private let sampleRate: UInt32 = 16000
// 音频配置 - 可配置的采样率和声道数
private var sampleRate: UInt32 = 16000
private var channels: UInt32 = 1 // 1=单声道, 2=立体声
private var lastSavedFile: URL? private var lastSavedFile: URL?
public var isPause = false public var isPause = false
var fileName = "" var fileName = ""
public init() {}
public init() {}
/**
* 设置音频配置
* @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000)
* @param channels 声道数 (1=单声道, 2=立体声)
*/
public func setAudioConfig(sampleRate: UInt32, channels: UInt32) {
self.sampleRate = sampleRate
self.channels = channels
print("音频配置已更新: 采样率=\(sampleRate)Hz, 声道数=\(channels)")
}
/**
* 获取当前音频配置信息
* @return Dictionary包含采样率和声道数信息
*/
public func getAudioConfig() -> [String: UInt32] {
return [
"sampleRate": sampleRate,
"channels": channels
]
}
/**
* 检查是否为双声道模式
* @return true表示双声道,false表示单声道
*/
public func isStereoMode() -> Bool {
return channels == 2
}
// 创建音频文件并初始化 WAV 头 // 创建音频文件并初始化 WAV 头
public func creatingFiles(atPath path: String) { public func creatingFiles(atPath path: String) {
guard fileHandle == nil else { return } guard fileHandle == nil else { return }
@ -87,10 +123,16 @@ public class RecordFile {
} }
} }
// 生成 WAV 文件头 /**
* 生成WAV文件头,支持可配置的采样率和声道数
* @param dataLength 音频数据长度
* @return WAV文件头数据
*/
private func generateWavHeader(dataLength: Int) -> Data { private func generateWavHeader(dataLength: Int) -> Data {
let totalLength = 36 + dataLength let totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据
let byteRate = sampleRate * 2 let bytesPerSample: UInt32 = 2 // 16位PCM
let byteRate = sampleRate * channels * bytesPerSample // 采样率 * 声道数 * 字节/样本
let blockAlign = channels * bytesPerSample // 声道数 * 字节/样本
var header = Data() var header = Data()
header.append("RIFF".data(using: .ascii)!) header.append("RIFF".data(using: .ascii)!)
@ -104,20 +146,26 @@ public class RecordFile {
header.append("fmt ".data(using: .ascii)!) header.append("fmt ".data(using: .ascii)!)
header.append(Data(bytes: [16, 0, 0, 0])) // PCM 头长度 header.append(Data(bytes: [16, 0, 0, 0])) // PCM 头长度
header.append(Data(bytes: [1, 0])) // PCM 格式 header.append(Data(bytes: [1, 0])) // PCM 格式
header.append(Data(bytes: [1, 0])) // 单声道 header.append(Data(bytes: [
UInt8(channels & 0xFF),
UInt8((channels >> 8) & 0xFF)
])) // 声道数
header.append(Data(bytes: [ header.append(Data(bytes: [
UInt8(sampleRate & 0xFF), UInt8(sampleRate & 0xFF),
UInt8((sampleRate >> 8) & 0xFF), UInt8((sampleRate >> 8) & 0xFF),
UInt8((sampleRate >> 16) & 0xFF), UInt8((sampleRate >> 16) & 0xFF),
UInt8((sampleRate >> 24) & 0xFF) UInt8((sampleRate >> 24) & 0xFF)
])) ])) // 采样率
header.append(Data(bytes: [ header.append(Data(bytes: [
UInt8(byteRate & 0xFF), UInt8(byteRate & 0xFF),
UInt8((byteRate >> 8) & 0xFF), UInt8((byteRate >> 8) & 0xFF),
UInt8((byteRate >> 16) & 0xFF), UInt8((byteRate >> 16) & 0xFF),
UInt8((byteRate >> 24) & 0xFF) UInt8((byteRate >> 24) & 0xFF)
])) ])) // 字节率
header.append(Data(bytes: [2, 0])) // 块对齐 header.append(Data(bytes: [
UInt8(blockAlign & 0xFF),
UInt8((blockAlign >> 8) & 0xFF)
])) // 块对齐
header.append(Data(bytes: [16, 0])) // 样本位数 header.append(Data(bytes: [16, 0])) // 样本位数
header.append("data".data(using: .ascii)!) header.append("data".data(using: .ascii)!)
header.append(Data(bytes: [ header.append(Data(bytes: [

22
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -42,6 +42,9 @@ public class SimpleAudioReceiver: NSObject {
private var audioDataCallback: AudioDataCallback? private var audioDataCallback: AudioDataCallback?
private var isRunning = false private var isRunning = false
public var _isWriting = false public var _isWriting = false
public var isRecord = false
public var isContinuousRecognitionActive = false
private var isInitialized = false
private let bufferSize: Int = 4096 private let bufferSize: Int = 4096
private var writeThread: DispatchQueue? private var writeThread: DispatchQueue?
private var currentRoute: AudioOutputRoute? private var currentRoute: AudioOutputRoute?
@ -63,7 +66,6 @@ public class SimpleAudioReceiver: NSObject {
// .audioDataHandler = { data in // .audioDataHandler = { data in
// self.writeQueue.offer(data) // self.writeQueue.offer(data)
// } // }
print("初始化了") print("初始化了")
pushAudioStream = SPXPushAudioInputStream() pushAudioStream = SPXPushAudioInputStream()
@ -74,8 +76,18 @@ public class SimpleAudioReceiver: NSObject {
// 创建新的写线程 // 创建新的写线程
writeThread = DispatchQueue(label: "audio.stream.writer") writeThread = DispatchQueue(label: "audio.stream.writer")
startWriteThread() startWriteThread()
isInitialized = true
} }
/**
* 检查音频接收器是否已初始化
* @return 如果已初始化则返回true,否则返回false
*/
public func getIsInitialized() -> Bool {
print("getIsInitialized=\(isInitialized)")
return isInitialized
}
/** /**
* 设置音频配置参数 * 设置音频配置参数
@ -144,11 +156,11 @@ public class SimpleAudioReceiver: NSObject {
if self.audioDataCallback != nil { if self.audioDataCallback != nil {
self.audioDataCallback!.onAudio(dataToWrite) self.audioDataCallback!.onAudio(dataToWrite)
} }
if self.pushAudioStream != nil && self.audioDataCallback == nil { if self.pushAudioStream != nil && isContinuousRecognitionActive {
try self.pushAudioStream?.write(dataToWrite) try self.pushAudioStream?.write(dataToWrite)
} }
if self.recordfile != nil { if self.recordfile != nil && isRecord{
// print("写入recordfile数据长度: \(dataToWrite.count)") // print("写入recordfile数据长度: \(dataToWrite.count)")
recordfile?.saveAudioDataToWav(dataToWrite) recordfile?.saveAudioDataToWav(dataToWrite)
} }
@ -330,6 +342,10 @@ public func restoreOriginalAudioState() {
print("stopMicrophoneCapture") print("stopMicrophoneCapture")
} }
/**
* 释放音频资源
* 安全地清理音频引擎和相关资源
*/
public func releaseAudioResources() { public func releaseAudioResources() {
print("释放了") print("释放了")
stopMicrophoneCapture() stopMicrophoneCapture()

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

@ -90,8 +90,10 @@ object BleConst {
const val CODEC_CONTROL_CLOSE = 0x00 const val CODEC_CONTROL_CLOSE = 0x00
/** 音乐或者通话远端声音 */ /** 音乐或者通话远端声音 */
const val CODEC_CONTROL_DECODE_ON = 0xA1 const val CODEC_CONTROL_DECODE_ON = 0xA1
/** mic和dac(音乐或者通话远端)声音 */ /** mic和dac(音乐或者通话远端)声音+翻译后重新编码 */
const val CODEC_CONTROL_A2DP_PLAY = 0xA2 const val CODEC_CONTROL_A2DP_PLAY = 0xA2
/** mic和dac(音乐或者通话远端)声音 */
const val CODEC_CONTROL_CALL_RECORD_PLAY = 0xA3
/** 打开编码指令 */ /** 打开编码指令 */
const val CODEC_CONTROL_ENCODE_ON = 0xB1 const val CODEC_CONTROL_ENCODE_ON = 0xB1
/** 左声道 */ /** 左声道 */

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

@ -53,9 +53,7 @@ object BleService {
fun onConnectionStateChanged(state: Int) fun onConnectionStateChanged(state: Int)
// 数据相关回调 // 数据相关回调
fun onAudioDataReceived(data: ByteArray) fun onAudioDataReceived(data: ByteArray,channel:Int)
// 数据相关回调
fun onAudioDataReceivedCall(data: ByteArray)
// 唤醒信号相关回调 // 唤醒信号相关回调
fun onWakeupSignalReceived() fun onWakeupSignalReceived()
@ -1054,11 +1052,13 @@ startBytesStatistics()
BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码" BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码"
BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码" BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码"
BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式" BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式"
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式"
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码" BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码"
else -> "未知状态($codecStatus)" else -> "未知状态($codecStatus)"
} }
if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON || if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON ||
codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON
) { ) {
recordfile1!!.closeFile() recordfile1!!.closeFile()
@ -1276,38 +1276,8 @@ startBytesStatistics()
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) { override fun onDecodeStream(data: ByteArray?) {
if (data != null) { if (data != null) {
// Log.d(TAG, "Opus解码数据: ${data.size} bytes") if (option!!.getChannel() >= 1) {
if (option!!.getChannel() == 2) { notifyAudioDataReceived(data,option!!.getChannel())
//解码数据再重新编码回去
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 { } else {
Log.e(TAG, "Opus解码数据错误: ${data.size} bytes") Log.e(TAG, "Opus解码数据错误: ${data.size} bytes")
} }
@ -1707,6 +1677,22 @@ startBytesStatistics()
) )
) )
} }
/**
* 控制编解码 - 打开解码
*/
fun openCallRecordDecoder(): Boolean {
Log.i(TAG, "打开编码 0xA3")
//双声道 80字节
startOpusStreamDecoding(false, 2, 16000, 80)
// Log.i(TAG, "打开解码...")
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()
)
)
}
/** /**
* 控制编解码 - A2DP播放 * 控制编解码 - A2DP播放
@ -1980,28 +1966,17 @@ startBytesStatistics()
/** /**
* 向所有回调监听器分发音频数据 * 向所有回调监听器分发音频数据
*/ */
private fun notifyAudioDataReceived(data: ByteArray) { private fun notifyAudioDataReceived(data: ByteArray,channel:Int) {
for (callback in callbacks) {
try {
callback.onAudioDataReceived(data)
} catch (e: Exception) {
Log.e(TAG, "分发音频数据回调异常", e)
}
}
}
/**
* 向所有回调监听器分发音频数据
*/
private fun notifyAudioDataReceived1(data: ByteArray) {
for (callback in callbacks) { for (callback in callbacks) {
try { try {
callback.onAudioDataReceivedCall(data) callback.onAudioDataReceived(data,channel)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "分发音频数据回调异常", e) Log.e(TAG, "分发音频数据回调异常", e)
} }
} }
} }
/** /**
* 向所有回调监听器分发唤醒信号 * 向所有回调监听器分发唤醒信号
*/ */

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

@ -209,6 +209,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val success = BleService.openA2DPDecoder() val success = BleService.openA2DPDecoder()
result.success(success) result.success(success)
} }
"openCallRecordDecoder" -> {
val success = BleService.openCallRecordDecoder()
result.success(success)
}
"closeCodec" -> { "closeCodec" -> {
val success = BleService.closeCodec() val success = BleService.closeCodec()
result.success(success) result.success(success)
@ -357,11 +361,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
sendEvent(statusEventSink, stateMap, "发送连接状态异常") sendEvent(statusEventSink, stateMap, "发送连接状态异常")
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray,channel:Int) {
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
}
override fun onAudioDataReceivedCall(data: ByteArray) {
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") // sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
} }

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift

@ -93,6 +93,8 @@ class BleConst {
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1
/** mic和dac(音乐或者通话远端)声音 */ /** mic和dac(音乐或者通话远端)声音 */
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2
/** mic和dac(音乐或者通话远端)声音 */
static let CODEC_CONTROL_CALL_RECORD_PLAY: UInt8 = 0xA3
/** 打开编码指令 */ /** 打开编码指令 */
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1
/** 左声道 */ /** 左声道 */

30
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -27,8 +27,8 @@ public class BleService: NSObject {
func onConnectionStateChanged(state: Int) func onConnectionStateChanged(state: Int)
// 数据相关回调 // 数据相关回调
func onAudioDataReceived(data: Data) func onAudioDataReceived(data: Data, channel: Int32)
func onAudioDataReceivedCall(data: Data)
// 唤醒信号回调 // 唤醒信号回调
func onWakeupSignalReceived() func onWakeupSignalReceived()
@ -524,15 +524,9 @@ private var cmdReplyType: UInt8 = 0
} }
/// 通知所有回调接收到音频数据 /// 通知所有回调接收到音频数据
private func notifyAudioDataReceived(data: Data) { private func notifyAudioDataReceived(data: Data, channel: Int32) {
for callback in callbacks {
callback.onAudioDataReceived(data: data)
}
}
/// 通知所有回调接收到音频数据
private func notifyAudioDataReceived1(data: Data) {
for callback in callbacks { for callback in callbacks {
callback.onAudioDataReceivedCall(data: data) callback.onAudioDataReceived(data: data, channel: channel)
} }
} }
@ -642,6 +636,12 @@ private var cmdReplyType: UInt8 = 0
let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT]) let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
} }
/// 打开通话记录解码器并开始录制
func openCallRecordDecoder() -> Bool {
os_log("打开通话记录解码并开始录制...", log: logger, type: .info)
let paramData = Data([BleConst.CODEC_CONTROL_CALL_RECORD_PLAY, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
/// 关闭编解码器并停止录制 /// 关闭编解码器并停止录制
/// - Returns: 操作是否成功 /// - Returns: 操作是否成功
@ -983,6 +983,8 @@ private var cmdReplyType: UInt8 = 0
statusDesc = "已打开解码" statusDesc = "已打开解码"
case Int(BleConst.CODEC_CONTROL_A2DP_PLAY): case Int(BleConst.CODEC_CONTROL_A2DP_PLAY):
statusDesc = "A2DP播放模式" statusDesc = "A2DP播放模式"
case Int(BleConst.CODEC_CONTROL_CALL_RECORD_PLAY):
statusDesc = "通话录音播放模式"
case Int(BleConst.CODEC_CONTROL_ENCODE_ON): case Int(BleConst.CODEC_CONTROL_ENCODE_ON):
statusDesc = "已打开编码" statusDesc = "已打开编码"
default: default:
@ -1184,14 +1186,10 @@ private var cmdReplyType: UInt8 = 0
@available(iOS 13.0, *) @available(iOS 13.0, *)
extension BleService: SwiftOpusAudioProcessor.AudioDataCallback { extension BleService: SwiftOpusAudioProcessor.AudioDataCallback {
/// 接收到解码后的PCM音频数据 /// 接收到解码后的PCM音频数据
func onAudioDataReceived(data: Data) { func onAudioDataReceived(data: Data, channel: Int32) {
notifyAudioDataReceived(data: data) notifyAudioDataReceived(data: data, channel: channel)
} }
/// 接收到解码后的PCM音频数据
func onAudioDataReceivedCall(data: Data) {
notifyAudioDataReceived1(data: data)
}
/// 接收到编码后的音频数据 /// 接收到编码后的音频数据
/// - Parameter data: 编码后的音频数据 /// - Parameter data: 编码后的音频数据

13
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift

@ -100,6 +100,9 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "openA2DPDecoder": case "openA2DPDecoder":
result(BleService.shared.openA2DPDecoder()) result(BleService.shared.openA2DPDecoder())
case "openCallRecordDecoder":
result(BleService.shared.openCallRecordDecoder())
case "closeCodec": case "closeCodec":
result(BleService.shared.closeCodec()) result(BleService.shared.closeCodec())
@ -225,21 +228,19 @@ extension SwiftBleServicePlugin: BleService.Callback {
} }
} }
public func onAudioDataReceived(data: Data) { public func onAudioDataReceived(data: Data, channel: Int32) {
if let eventSink = dataEventSink { if let eventSink = dataEventSink {
DispatchQueue.main.async { DispatchQueue.main.async {
eventSink([ eventSink([
"type": "audioData", "type": "audioData",
"data": FlutterStandardTypedData(bytes: data) "data": FlutterStandardTypedData(bytes: data),
"channel": channel
]) ])
} }
} }
} }
public func onAudioDataReceivedCall(data: Data) {
}
public func onWakeupSignalReceived() { public func onWakeupSignalReceived() {
if let eventSink = statusEventSink { if let eventSink = statusEventSink {
DispatchQueue.main.async { DispatchQueue.main.async {

93
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -8,8 +8,7 @@ class SwiftOpusAudioProcessor: NSObject {
/// 音频数据回调协议 /// 音频数据回调协议
protocol AudioDataCallback: AnyObject { protocol AudioDataCallback: AnyObject {
func onAudioDataReceived(data: Data) func onAudioDataReceived(data: Data, channel: Int32)
func onAudioDataReceivedCall(data: Data)
func onEncodedDataReceived(data: Data) // 编码数据回调 func onEncodedDataReceived(data: Data) // 编码数据回调
} }
@ -284,61 +283,61 @@ class SwiftOpusAudioProcessor: NSObject {
} }
print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)") print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)")
DispatchQueue.main.async { [weak self] in
if channels == 2 { self?.callback?.onAudioDataReceived(data: data, channel: self?.channels ?? 0)
processStereoAudioData(data)
} else if channels == 1 {
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: data)
} }
} else { // if channels == 2 {
print("不支持的声道数: \(channels)") // processStereoAudioData(data)
} // } else if channels == 1 {
// } else {
// print("不支持的声道数: \(channels)")
// }
} }
/// 处理双声道音频数据,分离左右声道 // /// 处理双声道音频数据,分离左右声道
/// - Parameter stereoData: 交错的双声道PCM数据 // /// - Parameter stereoData: 交错的双声道PCM数据
private func processStereoAudioData(_ stereoData: Data) { // private func processStereoAudioData(_ stereoData: Data) {
let sampleCount = stereoData.count / 4 // 每个样本4字节(左右声道各2字节) // let sampleCount = stereoData.count / 4 // 每个样本4字节(左右声道各2字节)
guard sampleCount > 0 else { // guard sampleCount > 0 else {
print("双声道数据样本数为0") // print("双声道数据样本数为0")
return // return
} // }
var leftBuffer = Data() // var leftBuffer = Data()
var rightBuffer = Data() // var rightBuffer = Data()
leftBuffer.reserveCapacity(sampleCount * 2) // leftBuffer.reserveCapacity(sampleCount * 2)
rightBuffer.reserveCapacity(sampleCount * 2) // rightBuffer.reserveCapacity(sampleCount * 2)
stereoData.withUnsafeBytes { bytes in // stereoData.withUnsafeBytes { bytes in
let uint8Ptr = bytes.bindMemory(to: UInt8.self) // let uint8Ptr = bytes.bindMemory(to: UInt8.self)
for i in 0..<sampleCount { // for i in 0..<sampleCount {
let stereoIndex = i * 4 // let stereoIndex = i * 4
// 左声道(低位字节在前,高位字节在后) // // 左声道(低位字节在前,高位字节在后)
leftBuffer.append(uint8Ptr[stereoIndex]) // leftBuffer.append(uint8Ptr[stereoIndex])
leftBuffer.append(uint8Ptr[stereoIndex + 1]) // leftBuffer.append(uint8Ptr[stereoIndex + 1])
// 右声道 // // 右声道
rightBuffer.append(uint8Ptr[stereoIndex + 2]) // rightBuffer.append(uint8Ptr[stereoIndex + 2])
rightBuffer.append(uint8Ptr[stereoIndex + 3]) // rightBuffer.append(uint8Ptr[stereoIndex + 3])
} // }
} // }
print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes") // print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes")
// 使用SpeexKit编码右声道数据 // // 使用SpeexKit编码右声道数据
//encodePCMData(rightBuffer) // //encodePCMData(rightBuffer)
// 回调分离后的音频数据 // // 回调分离后的音频数据
DispatchQueue.main.async { [weak self] in // DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: rightBuffer) // self?.callback?.onAudioDataReceived(data: rightBuffer)
self?.callback?.onAudioDataReceivedCall(data: leftBuffer) // self?.callback?.onAudioDataReceivedCall(data: leftBuffer)
} // }
} // }
// MARK: - 状态查询方法 // MARK: - 状态查询方法

12
local_plugins/ble_service/lib/ble_service.dart

@ -138,6 +138,18 @@ class BleService {
} }
} }
/// 打开解码器
Future<bool> openCallRecordDecoder() async {
try {
final result =
await _methodChannel.invokeMethod<bool>('openCallRecordDecoder');
return result ?? false;
} catch (e) {
print('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器 /// 关闭编解码器
Future<bool> closeCodec() async { Future<bool> closeCodec() async {
try { try {

5
local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

@ -378,14 +378,11 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
} }
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray,channel:Int) {
// 可选:处理音频数据 // 可选:处理音频数据
} }
override fun onAudioDataReceivedCall(data: ByteArray) {
}
/** /**
* 处理唤醒信号 * 处理唤醒信号
* 在收到唤醒信号时启动语音识别 * 在收到唤醒信号时启动语音识别

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans name: voitrans
description: "Voitrans - AI Voice Assistant." description: "Voitrans - AI Voice Assistant."
publish_to: "none" publish_to: "none"
version: 1.0.12+29 version: 1.0.13+34
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save