diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index 4a8b34c2d..1a74af25c 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -43,8 +43,10 @@ abstract class AsrService { /// 开始录音 /// /// [filePath] 录音文件路径 + /// [audioSourceType] 音频源类型 /// [acceptAudioData] 是否接受音频数据回调,默认为 false - Future enableRecord(String filePath, {bool acceptAudioData = false}); + Future enableRecord(bool audioSourceType, String filePath, + {bool acceptAudioData = false}); /// 获取音频数据流(如果支持) Stream? getAudioDataStream() => null; diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 963f22c25..414268fed 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -856,6 +856,16 @@ class BleManager extends GetxService { } } + /// 打开解码器 + Future openCallRecordDecoder() async { + try { + return await _bleService.openCallRecordDecoder(); + } catch (e) { + Logger.error('打开解码器失败: ${e.toString()}'); + return false; + } + } + /// 关闭编解码器 Future closeCodec() async { try { diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 9c46e528e..eb003736a 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -465,10 +465,11 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String filePath, + Future enableRecord(bool audioSourceType, String filePath, {bool acceptAudioData = false}) async { try { final bool result = await _channel.invokeMethod('enableRecord', { + 'audioSourceType': audioSourceType, 'filePath': filePath, 'acceptAudioData': acceptAudioData, // 新增参数 }); diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 87e0863d8..80714696f 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -813,7 +813,8 @@ class VolcanoAsrApiService implements AsrService { } @override - Future enableRecord(String filePath, {bool acceptAudioData = false}) { + Future enableRecord(bool audioSourceType, String filePath, + {bool acceptAudioData = false}) { // TODO: implement enableRecord throw UnimplementedError(); } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index 6d090d642..3ed457c3b 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -348,7 +348,8 @@ class VolcanoAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String filePath, {bool acceptAudioData = false}) { + Future enableRecord(bool audioSourceType, String filePath, + {bool acceptAudioData = false}) { // TODO: implement enableRecord throw UnimplementedError(); } diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index 29daf7015..9dda44c97 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -272,7 +272,8 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future enableRecord(String filePath, {bool acceptAudioData = false}) { + Future enableRecord(bool audioSourceType, String filePath, + {bool acceptAudioData = false}) { // TODO: implement enableRecord throw UnimplementedError(); } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 96bc62d21..4b8762fc3 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -306,9 +306,13 @@ class MeetingRecordController extends GetxController final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); final fullFileName = "${fileName.value}_$formattedTime"; - // Start audio recording - await _asrService.enableRecord("${dir.path}$fullFileName.wav", - acceptAudioData: true); + if (fileName.value == "liveRecording".tr) { + await _asrService.enableRecord(false, "${dir.path}$fullFileName.wav", + acceptAudioData: true); + } else { + await _asrService.enableRecord(true, "${dir.path}$fullFileName.wav", + acceptAudioData: true); + } // 通过 asrService 获取音频数据流 _audioDataSubscription = @@ -350,7 +354,7 @@ class MeetingRecordController extends GetxController break; case 2: _asrService.setAudioConfig(sampleRate: 16000, channels: 2); - _bleManager.openA2DPDecoder(); + _bleManager.openCallRecordDecoder(); break; } } diff --git a/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart index 150c3549f..7f594cdc6 100644 --- a/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart @@ -35,38 +35,38 @@ class NavigationBarBottomSheet extends GetView { controller.addMeetingAudio(); }, ), - // if (controller.meetingController.type == 1) - // _buildItem( - // context, // 传递 context 参数 - // 'audioVideoRecording'.tr, // 对应中文:音、视频录音 - // Icons.videocam, - // Colors.green, - // onTap: controller.bleManager.isConnected - // ? () async { - // Get.back(); - // await Get.toNamed(Routes.meetingRecord, arguments: { - // 'audioTypes': 1, - // }); - // controller.addMeetingAudio(); - // } - // : null, - // ), - // if (controller.meetingController.type == 1) - // _buildItem( - // context, // 传递 context 参数 - // 'callRecording'.tr, // 对应中文:通话录音 - // Icons.phone, - // Colors.orange, - // onTap: controller.bleManager.isConnected - // ? () async { - // Get.back(); - // await Get.toNamed(Routes.meetingRecord, arguments: { - // 'audioTypes': 2, - // }); - // controller.addMeetingAudio(); - // } - // : null, - // ), + if (controller.meetingController.type == 1) + _buildItem( + context, // 传递 context 参数 + 'audioVideoRecording'.tr, // 对应中文:音、视频录音 + Icons.videocam, + Colors.green, + onTap: controller.bleManager.isConnected + ? () async { + Get.back(); + await Get.toNamed(Routes.meetingRecord, arguments: { + 'audioTypes': 1, + }); + controller.addMeetingAudio(); + } + : null, + ), + if (controller.meetingController.type == 1) + _buildItem( + context, // 传递 context 参数 + 'callRecording'.tr, // 对应中文:通话录音 + Icons.phone, + Colors.orange, + onTap: controller.bleManager.isConnected + ? () async { + Get.back(); + await Get.toNamed(Routes.meetingRecord, arguments: { + 'audioTypes': 2, + }); + controller.addMeetingAudio(); + } + : null, + ), _buildItem( context, // 传递 context 参数 'importAudio'.tr, // 对应中文:导入音频 diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 97d54cb09..e0af016e1 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -74,7 +74,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { final isTranslating = false.obs; final isTtsEnabled = true.obs; final isRecording = false.obs; - final lasyIsRecording = false.obs; + final isCreateRecord = false.obs; final hasRecordPermission = false.obs; // ==================== 语言相关 ==================== @@ -186,8 +186,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// [speakerId] 说话者ID Future _startFaceToFaceRecognition(int speakerId) async { try { - await _asrService.startContinuousRecognition(_audioSourceType); - await continueRecording(); + await _asrService.startContinuousRecognition(_audioSourceType); //开启识别 + _startAsrActiveTracking(); //开启识别活动跟踪 + if (isRecording.value) { + if (isCreateRecord.value) { + //已经创建文件 + await continueRecording(); + } else { + await startRecording(); + } + } Logger.info('面对面翻译:激活说话者 $speakerId'); } catch (e) { Logger.error('启动面对面翻译失败: ${e.toString()}'); @@ -198,13 +206,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 停止面对面翻译的语音识别 Future _stopFaceToFaceRecognition() async { try { - await pauseRecording(); //暂停 + if (isRecording.value) { + await pauseRecording(); //暂停 + } if (faceToFaceRecognitionText.value.isNotEmpty) { handleFinalResult(faceToFaceRecognitionText.value); faceToFaceRecognitionText.value = ''; } await _asrService.stopContinuousRecognition(); - _stopAsrActiveTracking(); + _stopAsrActiveTracking(); //停止识别活动跟踪 isRecognizing.value = false; currentSessionId = null; @@ -498,7 +508,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 切换录音功能开关 Future toggleisRecord() async { isRecording.toggle(); - if (isRecording.value && isRecognizing.value) { + if (isRecording.value && + isRecognizing.value && + currentMode.value != 'faceToFace') { startRecording(); } else if (!isRecording.value) { stopRecording(); @@ -508,21 +520,27 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 开始录音 Future startRecording() async { _timerManager.startTimer(); - final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); - await _asrService.enableRecord( - "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); - if (currentMode.value == 'call') { - await _astService.enableRecord( - "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); + if (currentMode.value == 'call' || currentMode.value == 'audioVideo') { + await _asrService.enableRecord( + true, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + } else { + 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 stopRecording() async { _timerManager.stopTimer(); _asrService.stopRecord(true); - lasyIsRecording.value = false; + isCreateRecord.value = false; Logger.info('录音已停止,计时器已重置'); } @@ -530,7 +548,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Future pauseRecording() async { _timerManager.pauseTimer(); _asrService.pauseRecord(); - lasyIsRecording.value = false; + Logger.info(' 录音已暂停'); } @@ -538,7 +556,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Future continueRecording() async { _timerManager.resumeTimer(); _asrService.resumeRecord(); - lasyIsRecording.value = true; + Logger.info(' 录音已继续'); } @@ -552,8 +570,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { try { await _initializeSession(); await _configureAudioMode(); - await _startAsrService(); - await _finalizeRecognitionStart(); + if (currentMode.value != 'faceToFace') { + await _startAsrService(); + await _finalizeRecognitionStart(); + } } catch (e) { isRecognizing.value = false; Logger.error('启动语音识别失败: ${e.toString()}'); @@ -622,9 +642,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 启动语音识别 Future _startAsrService() async { - if (currentMode.value != 'faceToFace') { - await _asrService.startContinuousRecognition(_audioSourceType); - } + await _asrService.startContinuousRecognition(_audioSourceType); } /// 完成识别启动 diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 58a5d33c9..a27210718 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -1087,19 +1087,19 @@ class TranslationView extends GetView { await controller.startRecognition(); }, ), - // _buildModeItem( - // context, - // 'audioVideoTranslation'.tr, - // Icons.videocam, - // Colors.orange, - // onTap: controller.bleManager.isConnected - // ? () async { - // Get.back(); - // await controller.changeTranslationMode('audioVideo'); - // await controller.startRecognition(); - // } - // : null, - // ), + _buildModeItem( + context, + 'audioVideoTranslation'.tr, + Icons.videocam, + Colors.orange, + onTap: controller.bleManager.isConnected + ? () async { + Get.back(); + await controller.changeTranslationMode('audioVideo'); + await controller.startRecognition(); + } + : null, + ), _buildModeItem( context, 'callTranslation'.tr, diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index 73a2e51c9..97d4df5f2 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -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}") AgentService.pushAudioData(data) // 可选:处理音频数据 - } - override fun onAudioDataReceivedCall(data: ByteArray) { - } /** * 处理唤醒信号 diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 3fbdb5ae7..c1b27f252 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -1507,13 +1507,10 @@ extension AgentServiceImpl: BleService.Callback { func onConnectionStateChanged(state: Int) { } - func onAudioDataReceived(data: Data) { + func onAudioDataReceived(data: Data, channel: Int32) { pushAudioData(data) } - - func onAudioDataReceivedCall(data: Data) { - } func onWakeupSignalReceived() { stopTts() diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 8f7d5b571..fbc8bcf4d 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -31,9 +31,6 @@ class AzureAsrHelper(private val context: Context) { private var audioConfig: AudioConfig? = null private var continuousCallback: ContinuousRecognizeCallback? = null - // 状态管理 - private var isContinuousRecognitionActive = false - // 配置参数 private var currentLanguage = "zh-CN" private var supportedLanguages = arrayOf("zh-CN") @@ -44,8 +41,8 @@ class AzureAsrHelper(private val context: Context) { // 音频源配置 var audioSourceType = AudioSourceType.MICROPHONE - // 音频处理 - var audioStream: SimpleAudioReceiver? = null + // 音频处理 - Initialize immediately to avoid null checks + var audioStream: SimpleAudioReceiver = SimpleAudioReceiver(context) // 网络状态监听 private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context) @@ -181,10 +178,10 @@ class AzureAsrHelper(private val context: Context) { private fun setupRecognizer(): Boolean { try { // 如果正在进行连续识别,先停止 - if (isContinuousRecognitionActive) { + if (audioStream.isContinuousRecognitionActive) { // 直接停止,不等待结果 recognizer?.stopContinuousRecognitionAsync() - isContinuousRecognitionActive = false + audioStream.isContinuousRecognitionActive = false } setupMicrophoneStream() @@ -214,18 +211,15 @@ class AzureAsrHelper(private val context: Context) { try { Log.d(tag, "设置麦克风流 - 使用拉流方式: }") - if (audioStream == null) { - // 创建外部音频拉流对象 - audioStream = SimpleAudioReceiver(context) - audioStream!!.initAudioRecord() + // Initialize if not already done + if (!audioStream.isInitialized()) { + audioStream.initAudioRecord() } - audioConfig = AudioConfig.fromStreamInput(audioStream!!.pushAudioStream) - + audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream) } catch (e: Exception) { Log.e(tag, "设置麦克风流失败: ${e.message}") - //microphoneStream = null } } @@ -241,7 +235,7 @@ class AzureAsrHelper(private val context: Context) { audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null ): Boolean { - Log.d(tag, "startContinuousRecognition:$isContinuousRecognitionActive ") + Log.d(tag, "startContinuousRecognition:${audioStream.isContinuousRecognitionActive} ") // 检查网络状态 if (!checkNetworkStatus()) { @@ -250,7 +244,7 @@ class AzureAsrHelper(private val context: Context) { return false } - if (isContinuousRecognitionActive) { + if (audioStream.isContinuousRecognitionActive) { return true } if (!isRecognizerValid()) { @@ -266,13 +260,12 @@ class AzureAsrHelper(private val context: Context) { this.audioSourceType = audioSourceType try { - Log.d(tag, "startContinuousRecognition: ${audioStream!!.isWriting()}") + Log.d(tag, "startContinuousRecognition: ${audioStream.isWriting()}") // 启动音频处理(若已启动则跳过) - if (!audioStream!!.isWriting()) { + if (!audioStream.isContinuousRecognitionActive) { Log.d(tag, "startAudioRecord:$audioSourceType") - audioStream!!.startAudioRecord( + audioStream.startAudioRecord( when (audioSourceType) { - AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL }, @@ -283,12 +276,12 @@ class AzureAsrHelper(private val context: Context) { } recognizer?.startContinuousRecognitionAsync() - isContinuousRecognitionActive = true + audioStream.isContinuousRecognitionActive = true return true } catch (e: Exception) { Log.e(tag, "开始连续识别失败: ${e.message}") - stopContinuousRecognition() - isContinuousRecognitionActive = false + stopContinuousRecognition() + audioStream.isContinuousRecognitionActive = false return false } } @@ -297,7 +290,7 @@ class AzureAsrHelper(private val context: Context) { * 设置事件监听器 */ fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { - Log.d(tag, "设置ssssss监听器:${speechConfig} ") + Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ") if (speechConfig == null) { callback.onError(1002,"语音服务未初始化") return false @@ -357,7 +350,7 @@ class AzureAsrHelper(private val context: Context) { Log.d(tag, "会话结束事件") if (audioSourceType == AudioSourceType.EXTERNAL) { callback.onSessionStopped() - isContinuousRecognitionActive = false + audioStream?.isContinuousRecognitionActive = false } } ) @@ -392,26 +385,25 @@ class AzureAsrHelper(private val context: Context) { } try { Log.d(tag, "停止连续语音识别: ") - // 停止音频处理 - audioStream!!.stopMicrophoneCapture() + // 停止音频处理 + audioStream.stopMicrophoneCapture() // 直接停止连续识别(SDK内部已是异步操作) recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) recognizer?.close() recognizer = null - isContinuousRecognitionActive = false + audioStream.isContinuousRecognitionActive = false return true } catch (e: Exception) { // 强制重置状态 - isContinuousRecognitionActive = false + audioStream.isContinuousRecognitionActive = false Log.e(tag, "停止连续识别失败: ${e.message}") - // 停止音频处理 - audioStream!!.stopMicrophoneCapture() + // 停止音频处理 + audioStream.stopMicrophoneCapture() recognizer?.stopContinuousRecognitionAsync() - - recognizer?.close() + recognizer?.close() recognizer = null - isContinuousRecognitionActive = false + audioStream.isContinuousRecognitionActive = 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() { try { // 如果正在进行连续识别,先停止 - if (isContinuousRecognitionActive) { + if (audioStream.isContinuousRecognitionActive) { // 直接停止,不等待结果 recognizer?.stopContinuousRecognitionAsync() - isContinuousRecognitionActive = false + audioStream.isContinuousRecognitionActive = false } // 停止音频处理 stopAudioProcessing() // 清理网络监听 clearNetworkDetection() // 停止录音 - if (audioStream!!.recordfile != null) { - audioStream!!.recordfile!!.closeFile(true) - } + audioStream.recordfile?.closeFile(true) + // 释放recognizer recognizer?.close() recognizer = null @@ -452,11 +443,11 @@ class AzureAsrHelper(private val context: Context) { audioConfig?.close() audioConfig = null // 确保状态被重置 - isContinuousRecognitionActive = false + audioStream.isContinuousRecognitionActive = false } catch (e: Exception) { // 确保状态被重置 - isContinuousRecognitionActive = false + audioStream.isContinuousRecognitionActive = false audioConfig = null recognizer = null speechConfig = null @@ -468,20 +459,14 @@ class AzureAsrHelper(private val context: Context) { * 停止音频处理 */ private fun stopAudioProcessing() { - audioStream?.let { - try { - Log.e(tag, "停止音频处理: }") - it.releaseAudioResources() - - it.pushAudioStream?.close() - - audioStream = null - } catch (e: Exception) { - Log.e(tag, "关闭麦克风流失败: ${e.message}") - e.printStackTrace() - } - } // 关闭音频流(根据实际实现可能需要) - + try { + Log.e(tag, "停止音频处理: }") + audioStream.releaseAudioResources() + audioStream.pushAudioStream?.close() + } 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, "开启录音:") - if (audioStream == null) { - // 创建外部音频拉流对象 - audioStream = SimpleAudioReceiver(context) - audioStream!!.initAudioRecord() - } - if (audioStream!!.recordfile == null) { - audioStream!!.recordfile = RecordFile() + this.audioSourceType = audioSourceType + if(audioSourceType == AudioSourceType.EXTERNAL){ + Log.i(tag, "外部音频源不在这里录音") + return } - - // 启动音频处理 - audioStream!!.startAudioRecord(SimpleAudioReceiver.AudioSourceType.MICROPHONE - - ,audioDataCallback) - audioStream!!.recordfile!!.closeFile(true) - - - audioStream!!.recordfile!!.creatingFiles(filePath) - + // Initialize if not already done + if (!audioStream.isInitialized()) { + audioStream.initAudioRecord() + } + + if (audioStream.recordfile == null) { + audioStream.recordfile = RecordFile() + } + + 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 { + if(audioSourceType == AudioSourceType.EXTERNAL){ + Log.i(tag, "外部音频源不在这里移动文件") + return false + } Log.i(tag, "移动文件到新路径:") - if (audioStream!!.recordfile == null)return false - audioStream!!.recordfile!!.moveFile(sourcePath, destPath) + audioStream.recordfile?.moveFile(sourcePath, destPath) ?: return false return true } - /** * 重命名指定路径的音频文件 */ fun renameFile(filePath: String, newName: String): Boolean { + if(audioSourceType == AudioSourceType.EXTERNAL){ + Log.i(tag, "外部音频源不在这里重命名文件") + return false + } Log.i(tag, "重命名指定路径的音频文件:") - - if (audioStream!!.recordfile == null)return false - audioStream!!.recordfile!!.renameFile(filePath, newName) + audioStream.recordfile?.renameFile(filePath, newName) ?: return false return true } @@ -540,35 +539,45 @@ class AzureAsrHelper(private val context: Context) { * 停止录音 */ fun pauseRecord() { + if(audioSourceType == AudioSourceType.EXTERNAL){ + Log.i(tag, "外部音频源不在这里暂停录音") + return + } Log.i(tag, "停止连续录音:") - if (audioStream!!.recordfile == null)return - - audioStream!!.stopMicrophoneCapture() - - + audioStream.recordfile ?: return + audioStream.isRecord = false } /** * 继续录音 */ fun resumeRecord() { - Log.i(tag, "继续录音:") - if (audioStream!!.recordfile == null)return - - audioStream!!.resumeRecord() + if(audioSourceType == AudioSourceType.EXTERNAL){ + Log.i(tag, "外部音频源不在这里继续录音") + return + } + Log.i(tag, "继续录音:") + audioStream.recordfile ?: return + audioStream.isRecord = true } /** * 关闭录音 */ fun stopRecord(isSave: Boolean) { - Log.i(tag, "关闭录音:") - if (audioStream == null) { + if(audioSourceType == AudioSourceType.EXTERNAL){ + Log.i(tag, "外部音频源不在这里关闭录音") return } - if (audioStream!!.recordfile == null)return - audioStream!!.stopMicrophoneCapture() - audioStream!!.recordfile!!.closeFile(isSave) + Log.i(tag, "关闭录音:") + audioStream.recordfile ?: return + + if (!audioStream.isContinuousRecognitionActive) { + audioStream.stopMicrophoneCapture() + } + + audioStream.isRecord = false + audioStream.recordfile?.closeFile(isSave) } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 2341bfb48..fdf4e5e5b 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -19,7 +19,7 @@ import com.deep_voice.speech.tts.TtsEventType import com.yunqiinnovation.ble_service.BleService import com.deep_voice.speech.tts.AudioOutputDevice import kotlinx.coroutines.* - +import com.yunqiinnovation.azure_speech.tools.RecordFile /** AzureSpeechPlugin */ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { private val tag = "AzureSpeechPlugin" @@ -49,7 +49,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { private lateinit var audioDataEventChannel: EventChannel private var audioDataEventSink: EventChannel.EventSink? = null - + private var recordfile: RecordFile? = null + private var isRecord = false // 是否已添加TTS事件监听器 private var isTtsListenerAdded = false @@ -446,7 +447,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val newName = call.argument("newName") ?: "" try { FileLogger.d(tag, "音频文件名称为: ${filePath}") // - azureAsrHelper.renameFile(filePath, newName) + if(azureAsrHelper.audioSourceType == .external){ + isRecord = false + recordfile.renameFile(filePath, newName) + }else{ + azureAsrHelper.renameFile(filePath, newName) + } result.success(true) } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) @@ -458,8 +464,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val destPath = call.argument("destPath") ?: "" try { FileLogger.d(tag, "音频文件名称为: ${sourcePath}") // - - azureAsrHelper.moveFile(sourcePath, destPath) + if(azureAsrHelper.audioSourceType == .external){ + isRecord = false + recordfile.moveFile(sourcePath, destPath) + }else{ + azureAsrHelper.moveFile(sourcePath, destPath) + } result.success(true) } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) @@ -470,11 +480,28 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val filePath = call.argument("filePath") ?: "" // 是否接受音频数据 val acceptAudioData = call.argument("acceptAudioData") ?: false + val useExternalAudio = call.argument("audioSourceType") ?: false + + 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}") - // 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调 - azureAsrHelper.enableRecord(filePath, if(acceptAudioData) { + azureAsrHelper.enableRecord(audioSourceType, filePath, if(acceptAudioData) { // 创建音频数据回调,将音频数据发送到 Flutter 层 object : SimpleAudioReceiver.AudioDataCallback { override fun onAudio(audioData: ByteArray) { @@ -499,18 +526,36 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } "pauseRecord" -> { + if(azureAsrHelper.audioSourceType == .external){ + isRecord = false + }else{ + azureAsrHelper.pauseRecord() + + azureAsrHelper.pauseRecord() + } result.success(true) } "resumeRecord" -> { - azureAsrHelper.resumeRecord() + if(azureAsrHelper.audioSourceType == .external){ + isRecord = true + }else{ + azureAsrHelper.resumeRecord() + } result.success(true) } "stopRecord" -> { val isSave = call.argument("isSave") ?: false try { - azureAsrHelper.stopRecord(isSave) + if(azureAsrHelper.audioSourceType == .external){ + isRecord = false + recordfile?.closeFile(true) + } + else + { + azureAsrHelper.stopRecord(isSave) + } result.success(true) } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) @@ -521,7 +566,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val sampleRate = call.argument("sampleRate") ?: 16000 val channels = call.argument("channels") ?: 1 try { - azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels) + // azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels) result.success(true) } catch (e: Exception) { 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) { when (call.method) { - "enableRecord" -> { - val filePath = call.argument("filePath") ?: "" - try { - FileLogger.d(tag, "音频文件名称为: ${filePath}") // + // "enableRecord" -> { + // val filePath = call.argument("filePath") ?: "" + // try { + // FileLogger.d(tag, "音频文件名称为: ${filePath}") // - azureAstHelper.enableRecord(filePath) - result.success(true) - } catch (e: Exception) { - result.error("ENABLERECORD_ERROR", e.message, null) - } - } + // //azureAstHelper.enableRecord(filePath) + // result.success(true) + // } catch (e: Exception) { + // result.error("ENABLERECORD_ERROR", e.message, null) + // } + // } "startContinuousTranslation" -> { try { FileLogger.d(tag, "开启翻译") @@ -996,20 +1041,70 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { override fun onConnectionStateChanged(state: Int) { } - override fun onAudioDataReceived(data: ByteArray) { - if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { + override fun onAudioDataReceived(data: ByteArray,channel:Int) { + 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) } - // 可选:处理音频数据 - } - - override fun onAudioDataReceivedCall(data: ByteArray) { + } + else if (channel == 2) { + if (isRecord) { + 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) + } + // 可选:处理音频数据 } + /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别 diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt index 66a24e058..35e356892 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt @@ -117,10 +117,10 @@ class RecordFile { fos = FileOutputStream(currentAudioFile, true) startWriteThread() - // 如果是双声道,创建左右声道文件 - if (channels == 2) { - createStereoChannelFiles(filePath) - } + // // 如果是双声道,创建左右声道文件 + // if (channels == 2) { + // createStereoChannelFiles(filePath) + // } } /** @@ -242,14 +242,14 @@ class RecordFile { internal fun saveAudioDataToWav(buffer: ByteArray) { if (fos == null || currentAudioFile == null) return - if (channels == 2) { - // 双声道:拆分左右声道 - splitStereoToMono(buffer) - } else { + // if (channels == 2) { + // // 双声道:拆分左右声道 + // splitStereoToMono(buffer) + // } else { // 单声道:直接保存 writeQueue.offer(buffer.copyOf()) totalBytesWritten += buffer.size - } + // } } /** @@ -454,21 +454,22 @@ class RecordFile { } } - // 处理左右声道文件(如果是双声道) - if (channels == 2) { - leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave) - rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave) + // // 处理左右声道文件(如果是双声道) + // if (channels == 2) { + // leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave) + // rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave) - // 等待左右声道写入线程结束 - leftChannelWriteThread?.join(500) - rightChannelWriteThread?.join(500) - } - - return if (channels == 2) { - leftChannelSuccess && rightChannelSuccess - } else { - mainFileSuccess - } + // // 等待左右声道写入线程结束 + // leftChannelWriteThread?.join(500) + // rightChannelWriteThread?.join(500) + // } + + // return if (channels == 2) { + // leftChannelSuccess && rightChannelSuccess + // } else { + // mainFileSuccess + // } + return mainFileSuccess } catch (e: Exception) { Log.e("tag", "更新WAV文件头失败: ${e.message}") return false diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt index bec452ee3..51a40aef4 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt @@ -49,7 +49,7 @@ class SimpleAudioReceiver(private val context: Context) { private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在 private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据 private var writeThread: Thread? = null - + private var isInitialized = false // 音频配置 private val channelConfig = AudioFormat.CHANNEL_IN_MONO private val audioFormat = AudioFormat.ENCODING_PCM_16BIT @@ -59,6 +59,8 @@ class SimpleAudioReceiver(private val context: Context) { // 录音文件处理 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 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 try { - // Log.d(TAG, "写入数据大小: ${finalData.size}") + Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}") if(audioDataCallback!=null){ audioDataCallback?.onAudio(finalData) } - if(pushAudioStream!=null&&audioDataCallback==null){ + if(pushAudioStream!=null&&isContinuousRecognitionActive){ pushAudioStream?.write(finalData) } - if(recordfile!=null){ + if(recordfile!=null&&isRecord){ recordfile?.saveAudioDataToWav(finalData) } @@ -342,7 +345,23 @@ class SimpleAudioReceiver(private val context: Context) { 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 = null + isInitialized = false } catch (e: Exception) { Log.e(TAG, "释放音频资源失败: ${e.message}") e.printStackTrace() diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 0fe8d7891..9f60f9218 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -39,7 +39,7 @@ public class AzureAsrHelper: NSObject { // 防抖机制相关变量 - 只保留开始操作的防抖 private var lastAudioStartTime: Int64 = 0 - private let audioOperationDebounceInterval: Int64 = 500 // 500毫秒防抖间隔 + private let audioOperationDebounceInterval: Int64 = 0 // 500毫秒防抖间隔 private var audioStartWorkItem: DispatchWorkItem? // 串行队列和状态标记,用于顺序控制音频启动/停止,避免竞态 private let audioOpQueue = DispatchQueue(label: "com.azure.speech.audio.operations") @@ -47,8 +47,7 @@ public class AzureAsrHelper: NSObject { private var isAudioStarted = false private var pendingStopRequest = false - // 状态管理 - private var _isContinuousRecognitionActive = false + // 配置参数 private var currentLanguage = "zh-CN" @@ -66,7 +65,7 @@ public class AzureAsrHelper: NSObject { case external } - private var audioSourceType = AudioSourceType.microphone + public var audioSourceType = AudioSourceType.microphone // 音频处理 // private var externalAudioStream: ExternalAudioPullStream? @@ -240,7 +239,7 @@ public class AzureAsrHelper: NSObject { /// /// 修复点: /// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效 - /// 2) 任务执行前二次校验:必须是当前挂起任务且 _isContinuousRecognitionActive 仍为 true 才执行启动 + /// 2) 任务执行前二次校验:必须是当前挂起任务且 audioStream?.isContinuousRecognitionActive 仍为 true 才执行启动 private func startAudioRecordAsync( audioSourceType: AudioSourceType, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, @@ -264,9 +263,9 @@ public class AzureAsrHelper: NSObject { completion(false) return } - // print("startAudioRecordAsync\(_isContinuousRecognitionActive)") + // print("startAudioRecordAsync\(audioStream?.isContinuousRecognitionActive)") // // 二次校验:stop 可能已发生,确认仍需启动 - // guard self._isContinuousRecognitionActive else { + // guard self.audioStream?.isContinuousRecognitionActive else { // os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info) // completion(false) // return @@ -321,10 +320,12 @@ public class AzureAsrHelper: NSObject { audioDataCallback: SimpleAudioReceiver.AudioDataCallback? ) -> Bool { do { - audioStream?.startAudioRecord( - audioSourceType: audioSourceType == .microphone ? .microphone : .external, - audioDataCallback: audioDataCallback - ) + if(!audioStream!.isContinuousRecognitionActive){ + audioStream?.startAudioRecord( + audioSourceType: audioSourceType == .microphone ? .microphone : .external, + audioDataCallback: audioDataCallback + ) + } os_log("音频录制启动成功", log: log, type: .info) return true } catch { @@ -382,7 +383,7 @@ public class AzureAsrHelper: NSObject { * @param audioDataCallback 音频数据回调 * @return 是否成功开始识别(调度成功即返回 true) * - * 修复点:在音频启动成功后的闭包中,先判断 _isContinuousRecognitionActive 是否仍然为 true, + * 修复点:在音频启动成功后的闭包中,先判断 audioStream?.isContinuousRecognitionActive 是否仍然为 true, * 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。 */ public func startContinuousRecognition( @@ -399,7 +400,7 @@ public class AzureAsrHelper: NSObject { return false } - if _isContinuousRecognitionActive { + if audioStream?.isContinuousRecognitionActive == true { print("连续识别已在进行中") return true } @@ -421,7 +422,7 @@ public class AzureAsrHelper: NSObject { print("startContinuousRecognition:\(audioSourceType)") // 移除这里的状态设置,等音频启动成功后再设置 - // self._isContinuousRecognitionActive = true + // self.audioStream?.isContinuousRecognitionActive = true // 异步启动音频处理 startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in @@ -429,8 +430,8 @@ public class AzureAsrHelper: NSObject { if success { // 只有在音频启动成功后才设置状态为 true - self._isContinuousRecognitionActive = true - print("startContinuousRecognition:\(self._isContinuousRecognitionActive)") + self.audioStream?.isContinuousRecognitionActive = true + print("startContinuousRecognition:\(self.audioStream?.isContinuousRecognitionActive)") // 启动语音识别 do { @@ -439,18 +440,18 @@ public class AzureAsrHelper: NSObject { } catch { os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription) self.stopContinuousRecognition() - self._isContinuousRecognitionActive = false + self.audioStream?.isContinuousRecognitionActive = false } } else { os_log("音频启动失败,无法开始识别", log: self.log, type: .error) - self._isContinuousRecognitionActive = false + self.audioStream?.isContinuousRecognitionActive = false } } return true } catch { stopContinuousRecognition() - _isContinuousRecognitionActive = false + audioStream?.isContinuousRecognitionActive = false return false } } @@ -478,7 +479,7 @@ public class AzureAsrHelper: NSObject { guard let self = self else { return } if success { // 标记不再活跃,供启动任务二次校验 - self._isContinuousRecognitionActive = false + self.audioStream?.isContinuousRecognitionActive = false os_log("音频停止成功", log: self.log, type: .info) } else { os_log("音频停止失败", log: self.log, type: .error) @@ -497,7 +498,7 @@ public class AzureAsrHelper: NSObject { return true } catch { // 强制重置状态 - _isContinuousRecognitionActive = false + audioStream?.isContinuousRecognitionActive = false // 关键:即使异常,也取消未执行的启动任务 cancelPendingAudioStart() @@ -532,7 +533,7 @@ public class AzureAsrHelper: NSObject { * 检查连续识别是否活跃 */ public func isContinuousRecognitionActive() -> Bool { - return self._isContinuousRecognitionActive + return self.audioStream?.isContinuousRecognitionActive == true } /** @@ -542,10 +543,10 @@ public class AzureAsrHelper: NSObject { do { print("释放所有资源:") // 如果正在进行连续识别,先停止 - if _isContinuousRecognitionActive { + if audioStream?.isContinuousRecognitionActive == true { // 直接停止,不等待结果 try? recognizer?.stopContinuousRecognition() - _isContinuousRecognitionActive = false + audioStream?.isContinuousRecognitionActive = false } // 取消防抖任务 - 只取消开始操作的防抖 audioStartWorkItem?.cancel() @@ -559,11 +560,11 @@ public class AzureAsrHelper: NSObject { audioConfig = nil audioStream = nil // 确保状态被重置 - _isContinuousRecognitionActive = false + audioStream?.isContinuousRecognitionActive = false //externalAudioStream = nil } catch { // 确保状态被重置 - _isContinuousRecognitionActive = false + audioStream?.isContinuousRecognitionActive = false //externalAudioStream = nil audioConfig = nil recognizer = nil @@ -599,13 +600,13 @@ public class AzureAsrHelper: NSObject { */ private func setupRecognizer() -> Bool { do { - print("设置识别器\(_isContinuousRecognitionActive)") + print("设置识别器\(audioStream?.isContinuousRecognitionActive ?? false)") // 如果正在进行连续识别,先停止 - if _isContinuousRecognitionActive { - try? recognizer?.stopContinuousRecognition() - _isContinuousRecognitionActive = false - } + if audioStream?.isContinuousRecognitionActive == true { + try? recognizer?.stopContinuousRecognition() + audioStream?.isContinuousRecognitionActive = false + } // 【优化】只有在音频流未初始化时才创建,避免重复初始化 if audioStream == nil || audioConfig == nil { @@ -741,7 +742,7 @@ public class AzureAsrHelper: NSObject { self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang) // 如果正在识别,需要重新设置识别器 - if _isContinuousRecognitionActive { + if audioStream?.isContinuousRecognitionActive == true { let wasActive = stopContinuousRecognition() if wasActive { return setupRecognizer() @@ -815,7 +816,7 @@ public class AzureAsrHelper: NSObject { print("会话结束事件:") // 直接在当前线程调用回调 callback.onSessionStopped() - self._isContinuousRecognitionActive = false + self.audioStream?.isContinuousRecognitionActive = false //self.stopAudioProcessing() } @@ -865,12 +866,17 @@ public class AzureAsrHelper: NSObject { /** * 开启录音 + * @param audioSourceType 音频源类型 * @param filePath 录音文件路径 * @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) - + self.audioSourceType = audioSourceType + if(audioSourceType == .external){ + os_log("外部音频源不在这里录音", log: log, type: .info) + return + } if audioStream == nil { // 创建外部音频拉流对象 audioStream = SimpleAudioReceiver() @@ -881,21 +887,17 @@ public class AzureAsrHelper: NSObject { audioStream?.recordfile = RecordFile() } - self.audioSourceType = .microphone + // 修复:移除多余的 audioDataCallback 参数 // 修复:添加类型转换 - audioStream?.startAudioRecord( - audioSourceType: { - switch audioSourceType { - case .microphone: - return SimpleAudioReceiver.AudioSourceType.microphone - case .external: - return SimpleAudioReceiver.AudioSourceType.external - } - }(), - audioDataCallback: audioDataCallback - ) - + if(!audioStream!.isContinuousRecognitionActive){ + // 异步启动音频处理 + startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in + guard let self = self else { return } + + } + } + audioStream?.isRecord = true audioStream?.recordfile?.closeFile(isSave: true) audioStream?.recordfile?.creatingFiles(atPath: filePath) } @@ -905,7 +907,10 @@ public class AzureAsrHelper: NSObject { */ 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 return true } @@ -915,6 +920,10 @@ public class AzureAsrHelper: NSObject { * 重命名指定路径的音频文件 */ 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 return true } @@ -925,32 +934,47 @@ public class AzureAsrHelper: NSObject { * 暂停录音 */ public func pauseRecord() { + if(audioSourceType == .external){ + os_log(" 外部音频源不在这里暂停录音", log: log, type: .info) + return + } os_log("暂停录音", log: log, type: .info) guard let audioStream = audioStream, audioStream.recordfile != nil else { return } - audioStream.stopMicrophoneCapture() + audioStream.isRecord = false } /** * 继续录音 */ public func resumeRecord() { + if(audioSourceType == .external){ + os_log(" 外部音频源不在这里继续录音", log: log, type: .info) + return + } os_log("继续录音", log: log, type: .info) guard let audioStream = audioStream, audioStream.recordfile != nil else { return } - audioStream.resumeRecord() + audioStream.isRecord = true } /** * 关闭录音 */ public func stopRecord(isSave: Bool) { + if(audioSourceType == .external){ + os_log(" 外部音频源不在这里关闭录音", log: log, type: .info) + return + } guard let audioStream = audioStream, audioStream.recordfile != nil else { return } - audioStream.stopMicrophoneCapture() + audioStream.isRecord = false + if (!audioStream.isContinuousRecognitionActive) { + audioStream.stopMicrophoneCapture() + } audioStream.recordfile?.isPause = false // audioStream.recordfile?.closeFile(isSave: true) // } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index 83503dbcb..4a272ea12 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -49,10 +49,6 @@ import ble_service } // 替换原有的 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 } + private typealias AsrEventStreamHandler = BaseEventStreamHandler + private typealias TtsEventStreamHandler = BaseEventStreamHandler + private typealias AudioDataEventStreamHandler = BaseEventStreamHandler + private typealias AstEventStreamHandler = BaseEventStreamHandler + // ASR相关 private var asrChannel: FlutterMethodChannel? private var asrEventChannel: FlutterEventChannel? @@ -96,8 +97,9 @@ private typealias AstEventStreamHandler = BaseEventStreamHandler private var audioDataEventChannel: FlutterEventChannel? internal var audioDataEventSink: FlutterEventSink? - - + // 录音相关 + public var recordfile: RecordFile? + private var isRecord = false // 创建事件回调 private var astEventCallback: AstEventCallback? // 是否已添加AST事件监听器 @@ -391,8 +393,14 @@ private func sendAstEvent(_ event: [String: Any]) { return } do { - print("音频文件名称为: (filePath)") - try azureAsrHelper.renameFile(filePath: filePath, newName: newName) + print("renameFile音频文件名称为: \(filePath),新名称为: \(newName)") + if(azureAsrHelper.audioSourceType == .external){ + try recordfile?.renameFile(at: filePath, to: newName) + } + else + { + try azureAsrHelper.renameFile(filePath: filePath, newName: filePath) + } result(true) } catch { result(FlutterError(code: "RENAMEFILE_ERROR", message: error.localizedDescription, details: nil)) @@ -406,8 +414,13 @@ private func sendAstEvent(_ event: [String: Any]) { return } do { - print("音频文件名称为: (sourcePath)") - try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath) + print("moveFile音频文件名称为: \(sourcePath),新名称为: \(destPath)") + if(azureAsrHelper.audioSourceType == .external){ + try recordfile?.moveFile(from: sourcePath, to: destPath) + } + else{ + try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath) + } result(true) } catch { 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 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 { if acceptAudioData { // 使用复用的音频数据回调实例 - try azureAsrHelper.enableRecord(filePath: filePath, audioDataCallback: audioDataCallback) + try azureAsrHelper.enableRecord(audioSourceType: audioSourceType, filePath: filePath, audioDataCallback: audioDataCallback) } else { - try azureAsrHelper.enableRecord(filePath: filePath) + try azureAsrHelper.enableRecord(audioSourceType: audioSourceType, filePath: filePath) } result(true) } catch { @@ -438,22 +466,34 @@ private func sendAstEvent(_ event: [String: Any]) { case "pauseRecord": + isRecord = false azureAsrHelper.pauseRecord() result(true) case "resumeRecord": + isRecord = true azureAsrHelper.resumeRecord() result(true) case "stopRecord": + + guard let args = call.arguments as? [String: Any], let isSave = args["isSave"] as? Bool else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "isSave 参数不能为空", details: nil)) return } do { - - try azureAsrHelper.stopRecord(isSave: isSave) + print("stopRecord : \(azureAsrHelper.audioSourceType)") + if(azureAsrHelper.audioSourceType == .external){ + print("stopRecord音频文件名称为:)") + isRecord = false + recordfile?.closeFile(isSave: true) + } + else + { + try azureAsrHelper.stopRecord(isSave: isSave) + } result(true) } catch { result(FlutterError(code: "STOPRECORD_ERROR", message: error.localizedDescription, details: nil)) @@ -595,20 +635,20 @@ private func sendAstEvent(_ event: [String: Any]) { // MARK: - AST 方法处理 private func handleAstMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { - case "enableRecord": - guard let args = call.arguments as? [String: Any], - let filePath = args["filePath"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing filePath", details: nil)) - return - } - - do { - os_log("音频文件名称为: %@", log: log, type: .info, filePath) - try azureAstHelper.enableRecord(filePath: filePath) - result(true) - } catch { - result(FlutterError(code: "ENABLERECORD_ERROR", message: error.localizedDescription, details: nil)) - } + // case "enableRecord": + // guard let args = call.arguments as? [String: Any], + // let filePath = args["filePath"] as? String else { + // result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing filePath", details: nil)) + // return + // } + + // do { + // os_log("音频文件名称为: %@", log: log, type: .info, filePath) + // try azureAstHelper.enableRecord(filePath: filePath) + // result(true) + // } catch { + // result(FlutterError(code: "ENABLERECORD_ERROR", message: error.localizedDescription, details: nil)) + // } case "startContinuousTranslation": do { @@ -935,28 +975,79 @@ extension AzureSpeechPlugin: BleService.Callback { /// 接收到音频数据回调 /// - 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 { os_log("音频流未初始化", type: .error) return } - // print("liwei--------------接收到音频数据回调") - audioStream.saveAudioDataTo(data: data) - } - - /// 接收到音频数据回调1 - /// - Parameter data: 音频数据 - public func onAudioDataReceivedCall(data: Data) { - // 安全解包版本 - guard let audioStream = azureAstHelper.audioProcessor else { + azureAsrHelper.audioStream?.saveAudioDataTo(data: data) + } + else if channel == 2 { + if (isRecord) { + recordfile?.setAudioConfig(sampleRate: 16000, channels: 2) + recordfile?.saveAudioDataToWav(data) + handleAudioData(data) + } + 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.. Bool { + audioProcessor?.isContinuousRecognitionActive = true os_log("尝试启动连续翻译,当前状态:初始化=%@, 识别中=%@", log: log, type: .info, serviceState.isInitialized ? "true" : "false", serviceState.isRecognizing ? "true" : "false") @@ -597,7 +599,8 @@ import os.log */ public func stopContinuousTranslation() { do { - self.serviceState.isRecognizing = false + audioProcessor?.isContinuousRecognitionActive = false + self.serviceState.isRecognizing = false try recognizer?.stopContinuousRecognition() audioProcessor?.stopMicrophoneCapture() recordFile?.closeFile(isSave: true) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift index a175aa829..857d30041 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift @@ -10,10 +10,11 @@ public class MicrophoneCapture: NSObject { */ 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 audioInputNode: AVAudioInputNode! + private var audioInputNode: AVAudioInputNode? private var sampleRate: Double = 16000 // 修改为16000采样率,符合Microsoft要求 public var isCapturing = false public var hasBluetoothDevices = false @@ -84,8 +85,13 @@ public class MicrophoneCapture: NSObject { audioFormat = getOptimalAudioFormat() audioSession = AVAudioSession.sharedInstance() + // 修复:添加安全检查 + guard let audioSession = audioSession else { + print("音频会话初始化失败") + return + } + do { - try audioSession.setPreferredSampleRate(sampleRate) try audioSession.setPreferredIOBufferDuration(0.005) // 5ms缓冲 @@ -121,10 +127,11 @@ public class MicrophoneCapture: NSObject { * 配置蓝牙模式的音频路由:末端在手机,输出端在耳机 */ 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 { try audioSession.setPreferredInput(builtInMic) print("蓝牙模式:音频输入设置为内置麦克风(手机)") @@ -139,7 +146,7 @@ public class MicrophoneCapture: NSObject { // 确保音频输出通过蓝牙耳机 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 { @@ -164,10 +171,11 @@ public class MicrophoneCapture: NSObject { * 配置手机模式的音频路由:末端和输出端都在手机 */ 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 { // 设置音频会话参数 try audioSession.setCategory(.playAndRecord, @@ -199,6 +207,9 @@ public class MicrophoneCapture: NSObject { public func startCapture() throws { print("开始捕获\(isCapturing)") guard !isCapturing else { return } + guard let audioSession = audioSession else { + throw NSError(domain: "音频会话未初始化", code: -1) + } // 检查麦克风权限 switch audioSession.recordPermission { @@ -250,20 +261,24 @@ public class MicrophoneCapture: NSObject { * 根据Microsoft音频堆栈要求优化格式转换 */ private func setupAudioEngine() throws { + // 修复:先安全清理现有资源 + cleanupAudioEngine() + audioEngine = AVAudioEngine() - + + guard let audioEngine = audioEngine else { + throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "音频引擎初始化失败"]) + } + audioInputNode = audioEngine.inputNode - // 获取实际硬件采样率 - sampleRate = audioSession.sampleRate - // 安全地停止音频引擎 - if let engine = audioEngine { - engine.stop() + guard let audioInputNode = audioInputNode else { + throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "音频输入节点获取失败"]) } - // 安全地移除音频处理块 - if let inputNode = audioInputNode { - inputNode.removeTap(onBus: 0) + // 获取实际硬件采样率 + if let audioSession = audioSession { + sampleRate = audioSession.sampleRate } // 设置音频格式 @@ -274,72 +289,56 @@ public class MicrophoneCapture: NSObject { try audioInputNode.setVoiceProcessingEnabled(true) } - // 检查输入格式是否符合Microsoft要求 let needsConversion = !isMicrosoftCompatibleFormat(inputFormat) - // if needsConversion { - // 需要格式转换 - guard let targetFormat = audioFormat, - let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else { - throw NSError(domain: "AudioSetup", code: 2) - } + // 需要格式转换 + guard let targetFormat = audioFormat, + let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else { + throw NSError(domain: "AudioSetup", code: 2, userInfo: [NSLocalizedDescriptionKey: "音频格式转换器创建失败"]) + } + + 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要求,进行格式转换") - print("输入格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)") - print("目标格式: \(targetFormat.sampleRate)Hz, \(targetFormat.commonFormat.rawValue)") + guard let strongSelf = self else { return } - // 添加tap进行格式转换 - audioInputNode.installTap(onBus: 0, - bufferSize: 1024, - format: inputFormat) { [weak self] buffer, when in - - 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 - } + // 修复:添加安全检查,避免强制解包崩溃 + guard let convertedBuffer = AVAudioPCMBuffer( + pcmFormat: targetFormat, + frameCapacity: AVAudioFrameCount( + targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate ) - - // 转换成功且无错误 - if status == .haveData, error == nil { - let data = strongSelf.audioBufferToData(convertedBuffer) - //print("转换后数据长度: \(data.count)") - strongSelf.audioDataHandler?(data) - } + ) else { + print("创建转换缓冲区失败") + return } - // } else { - // // 格式已符合Microsoft要求,直接使用 - // print("输入格式符合Microsoft要求,无需转换") - // print("格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)") - // audioInputNode.installTap(onBus: 0, - // bufferSize: 1024, - // format: inputFormat) { [weak self] buffer, when in - - // guard let strongSelf = self else { return } - //strongSelf.processAudioBuffer(buffer) - // // 直接处理原始音频数据 - // let data = strongSelf.audioBufferToData(convertedBuffer) - // print("转换后数据长度: \(data.count)") - // strongSelf.audioDataHandler?(data) - - // } - // } + var error: NSError? + // 执行音频格式转换 + let status = converter.convert( + to: convertedBuffer, + error: &error, + withInputFrom: { inNumPackets, outStatus in + outStatus.pointee = .haveData + return buffer + } + ) + + // 转换成功且无错误 + if status == .haveData, error == nil { + let data = strongSelf.audioBufferToData(convertedBuffer) + strongSelf.audioDataHandler?(data) + } else if let error = error { + print("音频格式转换失败: \(error.localizedDescription)") + } + } // 启动引擎 do { @@ -347,9 +346,26 @@ public class MicrophoneCapture: NSObject { isCapturing = true } catch { print("音频引擎启动失败: \(error.localizedDescription)") + throw error } } + /** + * 安全清理音频引擎资源 + * 防止内存访问错误 + */ + private func cleanupAudioEngine() { + // 安全地停止音频引擎 + audioEngine?.stop() + + // 安全地移除音频处理块 + audioInputNode?.removeTap(onBus: 0) + + // 重置引用 + audioInputNode = nil + audioEngine = nil + } + /** * 将音频缓冲区转换为Data格式 * 支持16位整数和32位浮点格式 @@ -423,36 +439,36 @@ public class MicrophoneCapture: NSObject { guard isCapturing else { return } isCapturing = false - // 安全地停止音频引擎 - if let engine = audioEngine { - engine.stop() - } + // 修复:使用安全的清理方法 + cleanupAudioEngine() - // 安全地移除音频处理块 - if let inputNode = audioInputNode { - inputNode.removeTap(onBus: 0) - } + // 安全地处理音频会话 + guard let audioSession = audioSession else { return } - try? audioSession.setActive(false) - if self.hasBluetoothDevices { - // 设置音频会话参数 - try? audioSession.setCategory(.playback, - mode: .videoChat, - options: [ - - .allowBluetoothA2DP, - .allowBluetooth - - ]) // 添加音频优先级控制 - } else { - try? audioSession.setCategory(.playback, - mode: .videoChat, - options: [ - .mixWithOthers, - .defaultToSpeaker]) + do { + try audioSession.setActive(false) + + if self.hasBluetoothDevices { + // 设置音频会话参数 + try audioSession.setCategory(.playback, + mode: .videoChat, + options: [ + .allowBluetoothA2DP, + .allowBluetooth + ]) // 添加音频优先级控制 + } else { + try audioSession.setCategory(.playback, + mode: .videoChat, + options: [ + .mixWithOthers, + .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() { stopCapture() audioDataHandler = nil + // 修复:完全清理资源 + cleanupAudioEngine() + audioSession = nil + audioFormat = nil } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift index ba6733fc9..6fa231100 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift @@ -10,11 +10,47 @@ public class RecordFile { private var isWriting = false private var dataBuffer = Data() 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? public var isPause = false 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 头 public func creatingFiles(atPath path: String) { guard fileHandle == nil else { return } @@ -87,10 +123,16 @@ public class RecordFile { } } - // 生成 WAV 文件头 + /** + * 生成WAV文件头,支持可配置的采样率和声道数 + * @param dataLength 音频数据长度 + * @return WAV文件头数据 + */ private func generateWavHeader(dataLength: Int) -> Data { - let totalLength = 36 + dataLength - let byteRate = sampleRate * 2 + let totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 + let bytesPerSample: UInt32 = 2 // 16位PCM + let byteRate = sampleRate * channels * bytesPerSample // 采样率 * 声道数 * 字节/样本 + let blockAlign = channels * bytesPerSample // 声道数 * 字节/样本 var header = Data() header.append("RIFF".data(using: .ascii)!) @@ -104,20 +146,26 @@ public class RecordFile { header.append("fmt ".data(using: .ascii)!) header.append(Data(bytes: [16, 0, 0, 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: [ UInt8(sampleRate & 0xFF), UInt8((sampleRate >> 8) & 0xFF), UInt8((sampleRate >> 16) & 0xFF), UInt8((sampleRate >> 24) & 0xFF) - ])) + ])) // 采样率 header.append(Data(bytes: [ UInt8(byteRate & 0xFF), UInt8((byteRate >> 8) & 0xFF), UInt8((byteRate >> 16) & 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".data(using: .ascii)!) header.append(Data(bytes: [ diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift index 4f9e3d02d..02db119fd 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift +++ b/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 isRunning = false public var _isWriting = false + public var isRecord = false + public var isContinuousRecognitionActive = false + private var isInitialized = false private let bufferSize: Int = 4096 private var writeThread: DispatchQueue? private var currentRoute: AudioOutputRoute? @@ -63,7 +66,6 @@ public class SimpleAudioReceiver: NSObject { // .audioDataHandler = { data in // self.writeQueue.offer(data) // } - print("初始化了") pushAudioStream = SPXPushAudioInputStream() @@ -74,8 +76,18 @@ public class SimpleAudioReceiver: NSObject { // 创建新的写线程 writeThread = DispatchQueue(label: "audio.stream.writer") 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 { self.audioDataCallback!.onAudio(dataToWrite) } - if self.pushAudioStream != nil && self.audioDataCallback == nil { + if self.pushAudioStream != nil && isContinuousRecognitionActive { try self.pushAudioStream?.write(dataToWrite) } - if self.recordfile != nil { + if self.recordfile != nil && isRecord{ // print("写入recordfile数据长度: \(dataToWrite.count)") recordfile?.saveAudioDataToWav(dataToWrite) } @@ -330,6 +342,10 @@ public func restoreOriginalAudioState() { print("stopMicrophoneCapture") } + /** + * 释放音频资源 + * 安全地清理音频引擎和相关资源 + */ public func releaseAudioResources() { print("释放了") stopMicrophoneCapture() diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index dd3988a04..5bb24ea8a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -90,8 +90,10 @@ object BleConst { const val CODEC_CONTROL_CLOSE = 0x00 /** 音乐或者通话远端声音 */ const val CODEC_CONTROL_DECODE_ON = 0xA1 - /** mic和dac(音乐或者通话远端)声音 */ + /** mic和dac(音乐或者通话远端)声音+翻译后重新编码 */ const val CODEC_CONTROL_A2DP_PLAY = 0xA2 + /** mic和dac(音乐或者通话远端)声音 */ + const val CODEC_CONTROL_CALL_RECORD_PLAY = 0xA3 /** 打开编码指令 */ const val CODEC_CONTROL_ENCODE_ON = 0xB1 /** 左声道 */ diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 4f4660c80..baaed09ec 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -53,9 +53,7 @@ object BleService { fun onConnectionStateChanged(state: Int) // 数据相关回调 - fun onAudioDataReceived(data: ByteArray) - // 数据相关回调 - fun onAudioDataReceivedCall(data: ByteArray) + fun onAudioDataReceived(data: ByteArray,channel:Int) // 唤醒信号相关回调 fun onWakeupSignalReceived() @@ -1054,11 +1052,13 @@ startBytesStatistics() BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码" BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码" BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式" + BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式" BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码" else -> "未知状态($codecStatus)" } if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON || codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || + codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY || codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON ) { recordfile1!!.closeFile() @@ -1276,38 +1276,8 @@ startBytesStatistics() opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { override fun onDecodeStream(data: ByteArray?) { if (data != null) { - // Log.d(TAG, "Opus解码数据: ${data.size} bytes") - if (option!!.getChannel() == 2) { - - //解码数据再重新编码回去 - - - val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节) - val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区 - val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区 - - // 拆分交错的左右声道数据 - for (i in 0 until sampleCount) { - val stereoIndex = i * 4 - val monoIndex = i * 2 - - // 左声道(低位字节在前,高位字节在后) - leftBuffer[monoIndex] = data[stereoIndex] - leftBuffer[monoIndex + 1] = data[stereoIndex + 1] - - // 右声道 - rightBuffer[monoIndex] = data[stereoIndex + 2] - rightBuffer[monoIndex + 1] = data[stereoIndex + 3] - } - - // //重新编码 - // opusManager?.writeEncodeStream(rightBuffer) - //notifyAudioDataReceived1(leftBuffer) - //回调 - notifyAudioDataReceived(rightBuffer)//对方的 - notifyAudioDataReceived1(leftBuffer)//自己的 - } else if (option!!.getChannel() == 1) { - notifyAudioDataReceived(data) + if (option!!.getChannel() >= 1) { + notifyAudioDataReceived(data,option!!.getChannel()) } else { 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播放 @@ -1980,28 +1966,17 @@ startBytesStatistics() /** * 向所有回调监听器分发音频数据 */ - private fun notifyAudioDataReceived(data: ByteArray) { - for (callback in callbacks) { - try { - callback.onAudioDataReceived(data) - } catch (e: Exception) { - Log.e(TAG, "分发音频数据回调异常", e) - } - } - } - /** - * 向所有回调监听器分发音频数据 - */ - private fun notifyAudioDataReceived1(data: ByteArray) { + private fun notifyAudioDataReceived(data: ByteArray,channel:Int) { for (callback in callbacks) { try { - callback.onAudioDataReceivedCall(data) + callback.onAudioDataReceived(data,channel) } catch (e: Exception) { Log.e(TAG, "分发音频数据回调异常", e) } } } + /** * 向所有回调监听器分发唤醒信号 */ diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 80fcb7780..3e2a2677d 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -209,6 +209,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, val success = BleService.openA2DPDecoder() result.success(success) } + "openCallRecordDecoder" -> { + val success = BleService.openCallRecordDecoder() + result.success(success) + } "closeCodec" -> { val success = BleService.closeCodec() result.success(success) @@ -357,11 +361,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, sendEvent(statusEventSink, stateMap, "发送连接状态异常") } - override fun onAudioDataReceived(data: ByteArray) { - // sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") - } - - override fun onAudioDataReceivedCall(data: ByteArray) { + override fun onAudioDataReceived(data: ByteArray,channel:Int) { // sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常") } diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift index f15c40b8b..33d92f331 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift +++ b/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 /** mic和dac(音乐或者通话远端)声音 */ 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 /** 左声道 */ diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 942728de5..3ad24d30f 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/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 onAudioDataReceived(data: Data) - func onAudioDataReceivedCall(data: Data) + func onAudioDataReceived(data: Data, channel: Int32) + // 唤醒信号回调 func onWakeupSignalReceived() @@ -524,15 +524,9 @@ private var cmdReplyType: UInt8 = 0 } /// 通知所有回调接收到音频数据 - private func notifyAudioDataReceived(data: Data) { - for callback in callbacks { - callback.onAudioDataReceived(data: data) - } - } - /// 通知所有回调接收到音频数据 - private func notifyAudioDataReceived1(data: Data) { + private func notifyAudioDataReceived(data: Data, channel: Int32) { 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]) 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: 操作是否成功 @@ -983,6 +983,8 @@ private var cmdReplyType: UInt8 = 0 statusDesc = "已打开解码" case Int(BleConst.CODEC_CONTROL_A2DP_PLAY): statusDesc = "A2DP播放模式" + case Int(BleConst.CODEC_CONTROL_CALL_RECORD_PLAY): + statusDesc = "通话录音播放模式" case Int(BleConst.CODEC_CONTROL_ENCODE_ON): statusDesc = "已打开编码" default: @@ -1184,14 +1186,10 @@ private var cmdReplyType: UInt8 = 0 @available(iOS 13.0, *) extension BleService: SwiftOpusAudioProcessor.AudioDataCallback { /// 接收到解码后的PCM音频数据 - func onAudioDataReceived(data: Data) { - notifyAudioDataReceived(data: data) + func onAudioDataReceived(data: Data, channel: Int32) { + notifyAudioDataReceived(data: data, channel: channel) } - /// 接收到解码后的PCM音频数据 - func onAudioDataReceivedCall(data: Data) { - notifyAudioDataReceived1(data: data) - } /// 接收到编码后的音频数据 /// - Parameter data: 编码后的音频数据 diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift index da70071fe..dd9d07f14 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift @@ -100,6 +100,9 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { case "openA2DPDecoder": result(BleService.shared.openA2DPDecoder()) + case "openCallRecordDecoder": + result(BleService.shared.openCallRecordDecoder()) + case "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 { DispatchQueue.main.async { eventSink([ "type": "audioData", - "data": FlutterStandardTypedData(bytes: data) + "data": FlutterStandardTypedData(bytes: data), + "channel": channel ]) } } } - public func onAudioDataReceivedCall(data: Data) { - - } - + public func onWakeupSignalReceived() { if let eventSink = statusEventSink { DispatchQueue.main.async { diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift index 5030de4ee..87367acd5 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift @@ -8,8 +8,7 @@ class SwiftOpusAudioProcessor: NSObject { /// 音频数据回调协议 protocol AudioDataCallback: AnyObject { - func onAudioDataReceived(data: Data) - func onAudioDataReceivedCall(data: Data) + func onAudioDataReceived(data: Data, channel: Int32) func onEncodedDataReceived(data: Data) // 编码数据回调 } @@ -284,61 +283,61 @@ class SwiftOpusAudioProcessor: NSObject { } print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)") - - if channels == 2 { - processStereoAudioData(data) - } else if channels == 1 { - DispatchQueue.main.async { [weak self] in - self?.callback?.onAudioDataReceived(data: data) + DispatchQueue.main.async { [weak self] in + self?.callback?.onAudioDataReceived(data: data, channel: self?.channels ?? 0) } - } else { - print("不支持的声道数: \(channels)") - } + // if channels == 2 { + // processStereoAudioData(data) + // } else if channels == 1 { + + // } else { + // print("不支持的声道数: \(channels)") + // } } - /// 处理双声道音频数据,分离左右声道 - /// - Parameter stereoData: 交错的双声道PCM数据 - private func processStereoAudioData(_ stereoData: Data) { - let sampleCount = stereoData.count / 4 // 每个样本4字节(左右声道各2字节) + // /// 处理双声道音频数据,分离左右声道 + // /// - Parameter stereoData: 交错的双声道PCM数据 + // private func processStereoAudioData(_ stereoData: Data) { + // let sampleCount = stereoData.count / 4 // 每个样本4字节(左右声道各2字节) - guard sampleCount > 0 else { - print("双声道数据样本数为0") - return - } + // guard sampleCount > 0 else { + // print("双声道数据样本数为0") + // return + // } - var leftBuffer = Data() - var rightBuffer = Data() + // var leftBuffer = Data() + // var rightBuffer = Data() - leftBuffer.reserveCapacity(sampleCount * 2) - rightBuffer.reserveCapacity(sampleCount * 2) + // leftBuffer.reserveCapacity(sampleCount * 2) + // rightBuffer.reserveCapacity(sampleCount * 2) - stereoData.withUnsafeBytes { bytes in - let uint8Ptr = bytes.bindMemory(to: UInt8.self) + // stereoData.withUnsafeBytes { bytes in + // let uint8Ptr = bytes.bindMemory(to: UInt8.self) - for i in 0.. openCallRecordDecoder() async { + try { + final result = + await _methodChannel.invokeMethod('openCallRecordDecoder'); + return result ?? false; + } catch (e) { + print('打开解码器失败: $e'); + return false; + } + } + /// 关闭编解码器 Future closeCodec() async { try { diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index 083ce0f70..2eecf9b5e 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -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) { - - } /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别