Browse Source

1.修复翻译和录音快速点击会卡死通话录音两份音频文件改成一份音频

weicu
fdp 1 year ago
parent
commit
898e8d46e5
  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. 12
      lib/modules/meeting/controllers/meeting_record_controller.dart
  8. 64
      lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart
  9. 58
      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

4
lib/data/services/asr_service.dart

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

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

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

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

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

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

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

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

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

12
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;
}
}

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

@ -35,38 +35,38 @@ class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
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, // 对应中文:导入音频

58
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<void> _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<void> _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<void> 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<void> 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<void> 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<void> pauseRecording() async {
_timerManager.pauseTimer();
_asrService.pauseRecord();
lasyIsRecording.value = false;
Logger.info(' 录音已暂停');
}
@ -538,7 +556,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> 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<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();
},
),
// _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,

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}")
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 onAudioDataReceived(data: Data) {
func onAudioDataReceived(data: Data, channel: Int32) {
pushAudioData(data)
}
func onAudioDataReceivedCall(data: Data) {
}
func onWakeupSignalReceived() {
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 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)
}

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.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<String>("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<String>("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<String>("filePath") ?: ""
// 是否接受音频数据
val acceptAudioData = call.argument<Boolean>("acceptAudioData") ?: false
val useExternalAudio = call.argument<Boolean>("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<Boolean>("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<Int>("sampleRate") ?: 16000
val channels = call.argument<Int>("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<String>("filePath") ?: ""
try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") //
// "enableRecord" -> {
// val filePath = call.argument<String>("filePath") ?: ""
// try {
// FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAstHelper.enableRecord(filePath)
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)
}
}
// //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)
}
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

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

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 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()

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

175
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..<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)
return
}
azureAsrHelper.audioStream?.saveAudioDataTo(data: rightBuffer)
// // 安全解包版本
// guard let audioProcessor = azureAstHelper.audioProcessor else {
// os_log("音频流未初始化", type: .error)
// return
// }
// print("liwei--------------接收到音频数据回调1")
audioStream.saveAudioDataTo(data: data)
azureAstHelper.pushAudioData(audioData: leftBuffer)
}
}
/// 接收到编码数据回调
/// - Parameter 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() {
audioProcessor = SimpleAudioReceiver()
audioProcessor?.initAudioRecord()
// 检查音频配置是否已存在
if audioConfig == nil, let pushStream = audioProcessor?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream)
@ -547,6 +548,7 @@ import os.log
* 开始连续语音翻译
*/
public func startContinuousTranslation() -> 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)

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

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 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: [

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 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()

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_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
/** 左声道 */

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 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)
}
}
}
/**
* 向所有回调监听器分发唤醒信号
*/

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()
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), "发送音频数据异常")
}

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
/** 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
/** 左声道 */

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 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: 编码后的音频数据

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

93
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..<sampleCount {
let stereoIndex = i * 4
// for i in 0..<sampleCount {
// let stereoIndex = i * 4
// 左声道(低位字节在前,高位字节在后)
leftBuffer.append(uint8Ptr[stereoIndex])
leftBuffer.append(uint8Ptr[stereoIndex + 1])
// // 左声道(低位字节在前,高位字节在后)
// 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")
// 使用SpeexKit编码右声道数据
//encodePCMData(rightBuffer)
// 回调分离后的音频数据
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: rightBuffer)
self?.callback?.onAudioDataReceivedCall(data: leftBuffer)
}
}
// // 右声道
// rightBuffer.append(uint8Ptr[stereoIndex + 2])
// rightBuffer.append(uint8Ptr[stereoIndex + 3])
// }
// }
// print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes")
// // 使用SpeexKit编码右声道数据
// //encodePCMData(rightBuffer)
// // 回调分离后的音频数据
// DispatchQueue.main.async { [weak self] in
// self?.callback?.onAudioDataReceived(data: rightBuffer)
// self?.callback?.onAudioDataReceivedCall(data: leftBuffer)
// }
// }
// 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 {
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) {
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

Loading…
Cancel
Save