diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 4cde8e5c9..1087023b6 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -102,6 +102,47 @@
NSAllowsArbitraryLoadsInWebContent
+
+ NSExceptionDomains
+
+ music.126.net
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSExceptionMinimumTLSVersion
+ TLSv1.0
+ NSIncludesSubdomains
+
+
+ p1.music.126.net
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSExceptionMinimumTLSVersion
+ TLSv1.0
+
+ p2.music.126.net
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSExceptionMinimumTLSVersion
+ TLSv1.0
+
+ p3.music.126.net
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSExceptionMinimumTLSVersion
+ TLSv1.0
+
+ p4.music.126.net
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSExceptionMinimumTLSVersion
+ TLSv1.0
+
+
NSBluetoothAlwaysUsageDescription
Bluetooth permission is required to connect and manage audio devices
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 954c2b61c..4d7ce9fcb 100644
--- a/lib/data/services/ble_manager.dart
+++ b/lib/data/services/ble_manager.dart
@@ -857,6 +857,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/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart
index 4fabc7cc6..78fa49473 100644
--- a/lib/modules/agent/controllers/agent_controller.dart
+++ b/lib/modules/agent/controllers/agent_controller.dart
@@ -498,7 +498,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
switch (event.type) {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
- isTextInputMode.value = false;
+ //isTextInputMode.value = false;
Logger.i(TAG, '订阅代理服务事件识别开始');
// isTextInputMode.refresh();
// 只标记识别开始,不添加消息
@@ -937,7 +937,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 注意:这里不需要调用startVoiceInput(),因为原生层已经启动了语音识别
// 我们只需要同步Flutter层的状态
- isTextInputMode.value = false;
+ //isTextInputMode.value = false;
Logger.i(TAG, 'BLE唤醒状态已设置,等待原生层语音识别结果');
}
diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart
index 96bc62d21..792618710 100644
--- a/lib/modules/meeting/controllers/meeting_record_controller.dart
+++ b/lib/modules/meeting/controllers/meeting_record_controller.dart
@@ -70,7 +70,7 @@ class MeetingRecordController extends GetxController
// 性能优化:音频处理采样控制
int _audioProcessCounter = 0;
- static const int _audioProcessInterval = 3; // 每3个音频包处理1次
+ static const int _audioProcessInterval = 1; // 每1个音频包处理1次
// 波形流动动画控制器
late AnimationController waveformAnimationController;
@@ -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..f20d097ac 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;
// ==================== 语言相关 ====================
@@ -128,6 +128,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
if (mode == 'call') {
await _initializeCallModeTranslationService();
+ } else {
+ _reinitializeAsrService();
}
}
@@ -186,8 +188,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 +208,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;
@@ -444,6 +456,19 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future _initializeCallModeTranslationService() async {
try {
Logger.info('开始初始化通话模式语音翻译服务');
+
+ // 初始化 ASR 服务时,明确指定需要支持的语言
+ final List asrSupportedLanguages = [targetLanguageCode.value];
+ Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages');
+
+ await _asrService.initialize(supportedLanguages: asrSupportedLanguages);
+
+ // 获取识别事件流
+ var recognitionStream = await _asrService.recognizeCallback();
+ _recognitionSubscription?.cancel();
+ _recognitionSubscription =
+ recognitionStream.listen(_handleRecognitionEvent);
+
final List callModeLanguages = [
sourceLanguageCode.value,
targetLanguageCode.value
@@ -498,7 +523,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 +535,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 +563,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future pauseRecording() async {
_timerManager.pauseTimer();
_asrService.pauseRecord();
- lasyIsRecording.value = false;
+
Logger.info(' 录音已暂停');
}
@@ -538,7 +571,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future continueRecording() async {
_timerManager.resumeTimer();
_asrService.resumeRecord();
- lasyIsRecording.value = true;
+
Logger.info(' 录音已继续');
}
@@ -552,8 +585,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 +657,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 fccbf1bf4..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
@@ -917,19 +917,19 @@ private func jsonToString(_ json: [String: Any]) -> String? {
// 处理音乐卡片
private func processMusicCard(_ cardMusic: [String: Any]) {
let id = cardMusic["id"] as? Int ?? 0
- let url = cardMusic["url"] as? String ?? ""
+// let url = cardMusic["url"] as? String ?? ""
let title = cardMusic["name"] as? String ?? ""
let artist = cardMusic["sgener"] as? String ?? ""
let coverUrl = cardMusic["image"] as? String ?? ""
- let ts = cardMusic["ts"] as? Int ?? 0
+// let ts = cardMusic["ts"] as? Int ?? 0
processMusicPlay([
"id": id,
- "url": url,
+// "url": url,
"title": title,
"artist": artist,
"coverUrl": coverUrl,
- "ts": ts
+// "ts": ts
])
}
@@ -939,19 +939,19 @@ private func jsonToString(_ json: [String: Any]) -> String? {
for item in musics {
let id = item["id"] as? Int ?? 0
- let url = item["url"] as? String ?? ""
+// let url = item["url"] as? String ?? ""
let title = item["name"] as? String ?? ""
let artist = item["sgener"] as? String ?? ""
let coverUrl = item["image"] as? String ?? ""
- let ts = item["ts"] as? Int ?? 0
+// let ts = item["ts"] as? Int ?? 0
playlist.append([
"id": id,
- "url": url,
+// "url": url,
"title": title,
"artist": artist,
"coverUrl": coverUrl,
- "ts": ts
+// "ts": ts
])
}
@@ -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..6348732c2 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 == 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 == 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 == AzureAsrHelper.AudioSourceType.EXTERNAL){
+ isRecord = false
+ }else{
+ azureAsrHelper.pauseRecord()
+
+
azureAsrHelper.pauseRecord()
+ }
result.success(true)
}
"resumeRecord" -> {
- azureAsrHelper.resumeRecord()
+ if(azureAsrHelper.audioSourceType == 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 == 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/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
index b500773cf..e8e70f9a7 100644
--- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
+++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
@@ -48,41 +48,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
),
QQMusicSearchHandler(context)
)
-
- // QQ音乐播放歌曲
-// client.registerLocalFunction(
-// "qqmusic_play_songs",
-// "播放QQ音乐歌曲列表",
-// mapOf(
-// "type" to "object",
-// "properties" to mapOf(
-// "songMids" to mapOf(
-// "type" to "array",
-// "description" to "歌曲ID列表",
-// "items" to mapOf("type" to "string")
-// ),
-// "index" to mapOf(
-// "type" to "integer",
-// "description" to "开始播放的歌曲索引,默认为0"
-// )
-// ),
-// "required" to listOf("songMids")
-// ),
-// QQMusicPlaySongsHandler(context)
-// )
-
- //QQ音乐播放控制
-// client.registerLocalFunction(
-// "qqmusic_play",
-// "播放QQ音乐",
-// mapOf(
-// "type" to "object",
-// "properties" to emptyMap(),
-// "required" to emptyList()
-// ),
-// QQMusicPlayHandler(context)
-// )
-
+
client.registerLocalFunction(
"qqmusic_pause",
"暂停QQ音乐",
@@ -137,40 +103,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
),
QQMusicPreviousHandler(context)
)
-
- // QQ音乐状态查询
- client.registerLocalFunction(
- "qqmusic_get_playback_state",
- "获取QQ音乐播放状态",
- mapOf(
- "type" to "object",
- "properties" to emptyMap(),
- "required" to emptyList()
- ),
- QQMusicGetPlaybackStateHandler(context)
- )
-
- client.registerLocalFunction(
- "qqmusic_get_current_song",
- "获取QQ音乐当前播放歌曲信息",
- mapOf(
- "type" to "object",
- "properties" to emptyMap(),
- "required" to emptyList()
- ),
- QQMusicGetCurrentSongHandler(context)
- )
-
- client.registerLocalFunction(
- "qqmusic_get_play_time",
- "获取QQ音乐播放时间信息",
- mapOf(
- "type" to "object",
- "properties" to emptyMap(),
- "required" to emptyList()
- ),
- QQMusicGetPlayTimeHandler(context)
- )
+
Log.d(TAG, "QQ音乐所有功能注册完成")
} catch (e: Exception) {
Log.e(TAG, "注册QQ音乐功能失败: ${e.message}", e)
@@ -179,31 +112,6 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
}
-private class QQMusicLoginHandler(private val context: Context?) : FunctionHandler {
- override suspend fun handle(arguments: Map): String {
- return try {
- val qqMusicSingleton = QQMusicSingleton.getInstance()
- var result = ""
-
- qqMusicSingleton.login { loginResult ->
- result = if (loginResult.isSuccess) {
- val loginData = loginResult.getOrNull()
- "{\"success\": true, \"message\": \"登录成功\", \"data\": {\"openId\": \"${loginData?.openId}\", \"openToken\": \"${loginData?.openToken}\", \"expireTime\": \"${loginData?.expireTime}\"}}"
- } else {
- val error = loginResult.exceptionOrNull()
- "{\"success\": false, \"message\": \"登录失败: ${error?.message}\"}"
- }
- }
-
- // 等待回调完成
- delay(3000)
- result.ifEmpty { "{\"success\": false, \"message\": \"登录超时\"}" }
- } catch (e: Exception) {
- Log.e("QQMusicLoginHandler", "QQ音乐登录失败", e)
- "{\"success\": false, \"message\": \"登录异常: ${e.message}\"}"
- }
- }
-}
private class QQMusicSearchHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map): String {
return try {
@@ -537,27 +445,3 @@ private class QQMusicGetCurrentSongHandler(private val context: Context?) : Func
}
}
-private class QQMusicGetPlayTimeHandler(private val context: Context?) : FunctionHandler {
- override suspend fun handle(arguments: Map): String {
- return try {
- val qqMusicSingleton = QQMusicSingleton.getInstance()
- var result = ""
-
- qqMusicSingleton.getPlayTime { timeResult ->
- result = if (timeResult.isSuccess) {
- val timeInfo = timeResult.getOrNull()
- "{\"success\": true, \"message\": \"获取播放时间成功\", \"data\": {\"totalTime\": ${timeInfo?.totalTime}, \"currentTime\": ${timeInfo?.currentTime}}}"
- } else {
- val error = timeResult.exceptionOrNull()
- "{\"success\": false, \"message\": \"获取播放时间失败: ${error?.message}\"}"
- }
- }
-
- delay(2000)
- result.ifEmpty { "{\"success\": false, \"message\": \"获取播放时间超时\"}" }
- } catch (e: Exception) {
- Log.e("QQMusicLoginHandler", "获取QQ音乐播放时间失败", e)
- "{\"success\": false, \"message\": \"获取播放时间异常: ${e.message}\"}"
- }
- }
-}
\ No newline at end of file
diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
index 79e54376b..4e6c098ea 100644
--- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
+++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
@@ -16,6 +16,7 @@ public class MusicService: NSObject {
private var player: AVPlayer?
private var playlist: [[String: Any]] = []
private var currentIndex = 0
+ private var currentsongid = Int64(0)
public var isPlaying = false
private var flutterChannel: FlutterMethodChannel?
private var progressTimer: Timer?
@@ -34,7 +35,7 @@ public class MusicService: NSObject {
private override init() {
super.init()
setupRemoteCommandCenter()
- //setupAudioSession()
+ setupAudioSession()
}
public func initialize(serviceUrl: String, userToken: String) {
@@ -44,15 +45,21 @@ public class MusicService: NSObject {
}
- // // MARK: - 音频会话设置
- // private func setupAudioSession() {
- // do {
- // try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth])
- // try AVAudioSession.sharedInstance().setActive(true)
- // } catch {
- // print("设置音频会话失败: \(error)")
- // }
- // }
+ // MARK: - 音频会话设置
+ private func setupAudioSession() {
+ do {
+ let audioSession = AVAudioSession.sharedInstance()
+ try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP])
+ try audioSession.setActive(true)
+
+ // 确保媒体控制中心可以接收到我们的应用
+ setupRemoteCommandCenter()
+
+ print("音频会话和媒体控制设置成功")
+ } catch {
+ print("设置音频会话失败: \(error)")
+ }
+ }
// MARK: - 基础播放控制
func getCurrentUrlString() -> String? {
@@ -144,9 +151,7 @@ public class MusicService: NSObject {
"id": songObj["id"] as? Int64,
"name": songObj["name"] as? String,
"singer": songObj["singer"] as? String,
- "url": songObj["url"] as? String,
"image": songObj["image"] as? String,
- "ts": songObj["ts"] as? Int64
])
}
@@ -154,17 +159,203 @@ public class MusicService: NSObject {
}
- public func play(song: [String: Any]) {
- guard let urlString = song["url"] as? String, let url = URL(string: urlString) else { return }
+ // MARK: - 获取歌曲URL
+ public func getSongUrl(id: Int64,curragnt:Int,allagent:Int, completion: @escaping (Result<[String: Any?], Error>) -> Void) {
+ guard !serviceUrl.isEmpty, let url = URL(string: "\(serviceUrl)/api/home/music_url") else {
+ completion(.failure(NSError(domain: "MusicService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Service URL is not configured"])))
+ return
+ }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.addValue(userToken, forHTTPHeaderField: "Authorization")
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.timeoutInterval = 30.0
+
+ let body: [String: Any] = [
+ "id": id
+ ]
+
+ do {
+ request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
+ } catch {
+ if (allagent > 0 && curragnt < allagent){
+ getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
+ }else{
+ completion(.failure(error))
+ }
+
+ return
+ }
+
+ print("发送获取URL请求: id=\(id)")
+
+ let task = URLSession.shared.dataTask(with: request) { data, response, error in
+ if let error = error {
+ print("获取URL请求失败: \(error.localizedDescription)")
+ if (allagent > 0 && curragnt < allagent){
+ self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
+ }else{
+ completion(.failure(error))
+ }
+ return
+ }
+
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
+ print("获取URL失败,HTTP状态码: \(statusCode)")
+ if (allagent > 0 && curragnt < allagent){
+ self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
+ }else{
+ completion(.failure(NSError(domain: "MusicService", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "获取URL失败,HTTP状态码: \(statusCode)"])))
+ }
+ return
+ }
+
+ guard let data = data else {
+ print("获取URL响应数据为空")
+ if (allagent > 0 && curragnt < allagent){
+ self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
+ }else{
+ completion(.failure(NSError(domain: "MusicService", code: -2, userInfo: [NSLocalizedDescriptionKey: "响应数据为空"])))
+ }
+ return
+ }
+
+ do {
+ if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
+ let urlData = self.parseUrlFromResponse(jsonResponse: jsonResponse)
+ print("获取URL成功: \(urlData)")
+ if (!urlData.isEmpty){
+ completion(.success(urlData))
+ }else{
+ if (allagent > 0 && curragnt < allagent){
+ self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
+ }else{
+ completion(.failure(NSError(domain: "MusicService", code: -3, userInfo: [NSLocalizedDescriptionKey: "获取连接失败!"])))
+ }
+ }
+ } else {
+ if (allagent > 0 && curragnt < allagent){
+ self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
+ }else{
+ completion(.failure(NSError(domain: "MusicService", code: -3, userInfo: [NSLocalizedDescriptionKey: "解析JSON响应失败"])))
+ }
+ }
+ } catch {
+ print("解析获取URL响应时发生异常: \(error.localizedDescription)")
+ if (allagent > 0 && curragnt < allagent){
+ self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
+ }else{
+ completion(.failure(error))
+ }
+ }
+ }
+
+ task.resume()
+ }
+
+ private func parseUrlFromResponse(jsonResponse: [String: Any]) -> [String: Any?] {
+ var urlData: [String: Any?] = [:]
+
+ guard let data = jsonResponse["data"] as? [String: Any] else {
+ return urlData
+ }
+ urlData["id"] = data["id"] as? Int64
+ urlData["url"] = data["url"] as? String
+ urlData["ts"] = data["ts"] as? Int64
+
+ return urlData
+ }
+
+ public func playPlaylist(songs: [[String: Any]]) {
+ playlist = songs
+ currentIndex = 0
+ if !playlist.isEmpty {
+ play(song: playlist[currentIndex])
+ }
+ }
+
+
+ public func play(song: [String: Any]) {
// 获取歌曲ID
- if let id = song["id"] as? NSNumber {
- currentIdInternal = id.int64Value
+ let id: Int64
+ if let songId = song["id"] as? NSNumber {
+ id = songId.int64Value
+ currentIdInternal = id
} else {
+ id = 0
currentIdInternal = 0
}
+ currentsongid = id
+ print("开始播放play id: \(id)")
+
+ // 检查 URL 和时间戳
+ let url = song["url"] as? String
+ let ts = song["ts"] as? Int64
+ let currentTime = Int64(Date().timeIntervalSince1970) // 当前时间戳(秒)
+
+ // 判断是否需要重新获取 URL
+ let needNewUrl = url?.isEmpty != false || ts == nil || ts! <= currentTime
+
+ if needNewUrl {
+ print("URL 不存在或已过期,重新获取 URL")
+ // 调用 getSongUrl 获取新的 URL
+ getSongUrl(id: id,curragnt: 0,allagent: 3) { [weak self] result in
+ DispatchQueue.main.async {
+ switch result {
+ case .success(let urlData):
+ if let newUrl = urlData["url"] as? String, !newUrl.isEmpty {
+ // 更新 song 数据
+ var updatedSong = song
+ updatedSong["url"] = newUrl
+ updatedSong["ts"] = urlData["ts"] ?? 0
+
+ // 更新 playlist 中对应的歌曲数据
+ self?.updatePlaylistSong(songId: id, updatedSong: updatedSong)
+
+ if (self?.currentsongid == id) {
+ // 使用新 URL 开始播放
+ self?.startPlayback(song: updatedSong, url: newUrl)
+ }
+
+ } else {
+ print("获取到的 URL 为空")
+ }
+ case .failure(let error):
+ print("获取 URL 失败: \(error.localizedDescription)")
+ }
+ }
+ }
+ } else {
+ // URL 有效,直接播放
+ startPlayback(song: song, url: url!)
+ }
+ }
+
+ // 新增方法:更新 playlist 中指定 ID 的歌曲数据
+ private func updatePlaylistSong(songId: Int64, updatedSong: [String: Any]) {
+ for i in 0.. {
// 已授权,直接请求登录信息
Log.d(TAG, "已授权,直接获取登录信息")
- callback.onSuccess("","","")
-// requestAuth(callback)
+ isBindQQMusicService = true
+ callback.onSuccess()
}
ErrorCodes.ERROR_API_NO_PERMISSION -> {
// 需要授权
@@ -110,12 +115,14 @@ class QQMusicLoginManager(private val context: Context) {
startAuthProcess(callback)
}
else -> {
+ isBindQQMusicService = false
Log.e(TAG, "检查授权状态失败: $code")
callback.onError("AUTH_CHECK_FAILED", "检查授权状态失败")
}
}
} catch (e: Exception) {
Log.e(TAG, "检查授权状态异常", e)
+ isBindQQMusicService = false
callback.onError("AUTH_CHECK_ERROR", "检查授权状态异常: ${e.message}")
}
}
@@ -172,7 +179,7 @@ class QQMusicLoginManager(private val context: Context) {
},
failed = { errorCode, errorMsg ->
Log.e(TAG, "授权失败: code=$errorCode, msg=$errorMsg")
-
+ isBindQQMusicService = false
val errorMessage = when (errorCode) {
LOGIN_FAILED -> "登录失败: $errorMsg"
else -> "授权失败: $errorMsg"
@@ -197,22 +204,23 @@ class QQMusicLoginManager(private val context: Context) {
// 检查签名
if (OpenIDHelper.checkQMSign(sign, returnNonce)) {
- val openId = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_ID)
- val openToken = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_TOKEN)
- val expireTime = appDecryptJson.getString(Keys.API_PARAM_KEY_SDK_EXPIRETIME)
-
- Log.d(TAG, "登录成功: openId=$openId")
-
+// val openId = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_ID)
+// val openToken = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_TOKEN)
+// val expireTime = appDecryptJson.getString(Keys.API_PARAM_KEY_SDK_EXPIRETIME)
+// Log.d(TAG, "登录成功: openId=$openId")
+ isBindQQMusicService = true
handler.post {
- callback.onSuccess(openId, openToken, expireTime)
+ callback.onSuccess()
}
} else {
+ isBindQQMusicService = false
Log.e(TAG, "签名验证失败")
handler.post {
callback.onError("SIGN_ERROR", "签名验证失败")
}
}
} else {
+ isBindQQMusicService = false
Log.e(TAG, "解密失败")
handler.post {
callback.onError("DECRYPT_ERROR", "解密失败")
@@ -220,6 +228,7 @@ class QQMusicLoginManager(private val context: Context) {
}
} catch (e: Exception) {
Log.e(TAG, "解析登录结果失败", e)
+ isBindQQMusicService = false
handler.post {
callback.onError("PARSE_ERROR", "解析登录结果失败: ${e.message}")
}
diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
index 822a6cc06..414d46856 100644
--- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
+++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
@@ -39,38 +39,27 @@ class QQMusicManager(private val context: Context) {
fun onSuccess(data: Map)
fun onError(errorCode: Int, errorMessage: String)
}
-
- private val serviceConnection = object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
- Log.d(TAG, "QQ音乐服务连接成功")
- qqmusicApi = IQQMusicApi.Stub.asInterface(service)
- isBindQQMusicService = true
-
- // 初始化SDK
- CommonCmd.init(Config.BIND_PLATFORM)
-
- // 延迟发送hi消息,确保Binder连接稳定
- Handler(Looper.getMainLooper()).postDelayed({
- sayHi()
- }, 500) // 延迟500ms
- }
-
- override fun onServiceDisconnected(name: ComponentName?) {
- Log.d(TAG, "QQ音乐服务连接断开")
- qqmusicApi = null
- isBindQQMusicService = false
- //重连绑定
- bindQQMusicApiService();
- }
+
+ // 新增:绑定服务的回调接口
+ interface BindCallback {
+ fun onSuccess()
+ fun onError(errorCode: Int, errorMessage: String)
}
+
init {
- bindQQMusicApiService()
+ // bindQQMusicApiService()
}
-
+
+ //是否连接
+ public val isConnected: Boolean
+ get() = isBindQQMusicService && qqmusicApi != null
+
+
+
private fun sayHi() {
if (!isBindQQMusicService || qqmusicApi == null) {
- Log.w(TAG, "QQ音乐服务未连接,无法发送hi消息")
+ Log.w(TAG, "连接QQ音乐失败,可能是QQ音乐帐号状态异常,请打开QQ音乐检查帐号状态是否异常?")
return
}
@@ -97,9 +86,9 @@ class QQMusicManager(private val context: Context) {
}
}
- fun bindQQMusicApiService() {
+ fun bindQQMusicApiService(callback: BindCallback? = null) {
Log.d(TAG, "开始绑定QQ音乐服务,平台类型: ${Config.BIND_PLATFORM}")
-
+
val intent = when (Config.BIND_PLATFORM) {
CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> {
Intent("com.tencent.qqmusic.third.api.QQMusicApiService").apply {
@@ -118,6 +107,7 @@ class QQMusicManager(private val context: Context) {
}
else -> {
Log.e(TAG, "不支持的平台类型: ${Config.BIND_PLATFORM}")
+ callback?.onError(-1, "不支持的平台类型: ${Config.BIND_PLATFORM}")
return
}
}
@@ -125,22 +115,71 @@ class QQMusicManager(private val context: Context) {
Log.d(TAG, "绑定Intent: action=${intent.action}, package=${intent.`package`}")
try {
- val bindResult = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
+ val bindResult = context.bindService(intent, object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.d(TAG, "QQ音乐服务连接成功")
+ qqmusicApi = IQQMusicApi.Stub.asInterface(service)
+ isBindQQMusicService = true
+
+ // 初始化SDK
+ CommonCmd.init(Config.BIND_PLATFORM)
+
+ // 延迟发送hi消息,确保Binder连接稳定
+ Handler(Looper.getMainLooper()).postDelayed({
+ sayHi()
+ // 绑定成功后调用回调
+ callback?.onSuccess()
+ }, 500) // 延迟500ms
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.d(TAG, "QQ音乐服务连接断开")
+ qqmusicApi = null
+ isBindQQMusicService = false
+ // 服务断开时通知错误
+ callback?.onError(-2, "QQ音乐服务连接断开")
+ //重连绑定
+ bindQQMusicApiService(callback)
+ }
+ }, Context.BIND_AUTO_CREATE)
+
Log.d(TAG, "bindService结果: $bindResult")
-
+
if (!bindResult) {
Log.e(TAG, "绑定QQ音乐服务失败,尝试启动QQ音乐进程")
// 尝试启动QQ音乐进程
CommonCmd.startQQMusicProcess(context, context.packageName)
-
+
// 延迟重试绑定
Handler(Looper.getMainLooper()).postDelayed({
- Log.d(TAG, "重试绑定QQ音乐服务")
- context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
+ val retryResult = context.bindService(intent, object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.d(TAG, "QQ音乐服务连接成功")
+ qqmusicApi = IQQMusicApi.Stub.asInterface(service)
+ isBindQQMusicService = true
+ // 初始化SDK
+ CommonCmd.init(Config.BIND_PLATFORM)
+ // 延迟发送hi消息,确保Binder连接稳定
+ Handler(Looper.getMainLooper()).postDelayed({
+ sayHi()
+ callback?.onSuccess()
+ }, 500) // 延迟500ms
+ }
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.d(TAG, "QQ音乐服务连接断开")
+ qqmusicApi = null
+ isBindQQMusicService = false
+ }
+ }, Context.BIND_AUTO_CREATE)
+ Log.d(TAG, "重试绑定QQ音乐服务 $retryResult")
+ if (!retryResult) {
+ callback?.onError(-3, "重试绑定QQ音乐服务失败")
+ }
}, 2000)
}
} catch (e: Exception) {
Log.e(TAG, "绑定QQ音乐服务异常", e)
+ callback?.onError(-4, "绑定QQ音乐服务异常: ${e.message}")
}
}
@@ -659,7 +698,6 @@ class QQMusicManager(private val context: Context) {
fun cleanup() {
try {
if (isBindQQMusicService) {
- context.unbindService(serviceConnection)
isBindQQMusicService = false
Log.d(TAG, "QQ音乐服务已解绑")
}
diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
index 53ff7b3e8..9b9762381 100644
--- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
+++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
@@ -8,8 +8,6 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import com.tencent.qqmusic.third.api.contract.*
-import java.text.SimpleDateFormat
-import java.util.*
/**
* QQ音乐单例管理器
@@ -23,12 +21,7 @@ class QQMusicSingleton private constructor() {
private const val KEY_OPEN_ID = "open_id"
private const val KEY_OPEN_TOKEN = "open_token"
private const val KEY_EXPIRE_TIME = "expire_time"
- private const val LOGIN_WAIT_DELAY_MS = 30000L
- // 添加搜索重试相关常量
- private const val SEARCH_TIMEOUT_MS = 10000L
- private const val MAX_RETRY_COUNT = 1
- private const val RETRY_DELAY_MS = 1000L
-
+
@Volatile
private var INSTANCE: QQMusicSingleton? = null
@@ -53,9 +46,9 @@ class QQMusicSingleton private constructor() {
*/
fun initialize(context: Context) {
this.context = context
- this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- loadSavedLoginInfo()
- Log.d(TAG, "QQ音乐单例初始化完成")
+ // this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ // loadSavedLoginInfo()
+ // Log.d(TAG, "QQ音乐单例初始化完成")
}
/**
* 检查QQ音乐App是否已安装
@@ -87,58 +80,54 @@ class QQMusicSingleton private constructor() {
* 检查是否已登录且未过期
*/
fun isLoggedIn(): Boolean {
- if (currentLoginResult == null){
- loadSavedLoginInfo()
- if (currentLoginResult == null){
- return false
- }
- return true
- }else{
- return true
- }
+ return loginManager != null && loginManager!!.isLogined
}
-
- /**
- * 获取当前登录信息
- */
- fun getCurrentLoginInfo(): LoginResult? {
- return if (isLoggedIn()) currentLoginResult else null
- }
-
+
/**
* 登录接口
*/
- fun login(callback: (Result) -> Unit) {
+ fun login(callback: (Result) -> Unit) {
if (!isQQMusicInstalled()){
callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件")))
return
}
- val currentActivity = context
+ val currentContext = context
// 先检查是否已经登录且未过期
if (isLoggedIn()) {
Log.d(TAG, "用户已登录且未过期,直接返回登录信息")
- ensureMusicManager(currentActivity!!)
- callback(Result.success(currentLoginResult!!))
+ ensureMusicManager(currentContext!!,object: QQMusicManager. BindCallback{
+ override fun onSuccess() {
+ callback(Result.success(Unit))
+ }
+
+ override fun onError(errorCode: Int, errorMessage: String) {
+ callback(Result.failure(Exception("no logined $errorMessage")))
+ }
+ })
+
return
}
- if (currentActivity == null) {
+ if (currentContext == null) {
callback(Result.failure(Exception("Activity not available")))
return
}
Log.d(TAG, "开始新的登录流程")
- loginManager = QQMusicLoginManager(currentActivity)
+ loginManager = QQMusicLoginManager(currentContext)
loginManager?.startLogin(object : QQMusicLoginManager.LoginCallback {
- override fun onSuccess(openId: String, openToken: String, expireTime: String) {
+ override fun onSuccess() {
handler.post {
- ensureMusicManager(currentActivity!!)
- val result = LoginResult(openId, openToken, expireTime)
- currentLoginResult = result
- saveLoginInfo(result)
- Log.d(TAG, "登录成功并已保存: $result")
- callback(Result.success(result))
+ ensureMusicManager(currentContext,object: QQMusicManager. BindCallback{
+ override fun onSuccess() {
+ callback(Result.success(Unit))
+ }
+
+ override fun onError(errorCode: Int, errorMessage: String) {
+ callback(Result.failure(Exception("no logined $errorMessage")))
+ }
+ })
}
}
@@ -164,8 +153,16 @@ class QQMusicSingleton private constructor() {
// 先检查是否已经登录且未过期
if (isLoggedIn()) {
Log.d(TAG, "用户已登录且未过期,直接返回登录信息")
- ensureMusicManager(currentActivity!!)
- callback(Result.success(currentLoginResult!!))
+ ensureMusicManager(currentActivity!!,object: QQMusicManager. BindCallback{
+ override fun onSuccess() {
+ callback(Result.success(currentLoginResult!!))
+ }
+
+ override fun onError(errorCode: Int, errorMessage: String) {
+ callback(Result.failure(Exception("no logined $errorMessage")))
+ }
+ })
+
return
}else{
callback(Result.failure(Exception("no logined")))
@@ -190,129 +187,12 @@ class QQMusicSingleton private constructor() {
searchType: Int = Data.SearchType.SEARCH_TYPE_SONG,
callback: (Result>>) -> Unit
) {
- if (!isQQMusicInstalled()){
- callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件")))
- return
- }
- searchWithRetry(keyword, searchType, 1, callback)
- }
-
- /**
- * 带重试的搜索实现
- */
- private fun searchWithRetry(
- keyword: String,
- searchType: Int,
- retryCount: Int,
- callback: (Result>>) -> Unit
- ) {
- val currentActivity = context
- if (currentActivity == null) {
- callback(Result.failure(Exception("Activity not available")))
- return
- }
-
- // 检查登录状态
- if (!isLoggedIn()) {
- Log.d(TAG, "搜索前检测到未登录,开始自动登录")
- login { loginResult ->
- if (loginResult.isSuccess) {
- Log.d(TAG, "自动登录成功,等待QQ音乐服务连接...")
- handler.postDelayed({
- Log.d(TAG, "等待完成,开始搜索")
- performSearchWithTimeout(keyword, searchType, retryCount, callback)
- }, LOGIN_WAIT_DELAY_MS)
- } else {
- callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}")))
- }
- }
- } else {
- performSearchWithTimeout(keyword, searchType, retryCount, callback)
- }
- }
-
- /**
- * 执行带超时的搜索操作
- */
- private fun performSearchWithTimeout(
- keyword: String,
- searchType: Int,
- retryCount: Int,
- callback: (Result>>) -> Unit
- ) {
- val currentActivity = context ?: return
- val searchKey = "${keyword}_${System.currentTimeMillis()}"
- var completed = false
-
- Log.d(TAG, "开始搜索: $keyword (第${retryCount + 1}次)")
-
- // 设置超时
- val timeoutRunnable = Runnable {
- if (!completed) {
- completed = true
- searchTimeouts.remove(searchKey)
- Log.w(TAG, "搜索超时: $keyword")
-
- if (retryCount < MAX_RETRY_COUNT) {
- Log.d(TAG, "准备重试: ${retryCount + 1}/$MAX_RETRY_COUNT")
- handler.postDelayed({
- searchWithRetry(keyword, searchType, retryCount + 1, callback)
- }, RETRY_DELAY_MS)
- } else {
- callback(Result.failure(Exception("搜索超时,已重试${MAX_RETRY_COUNT}次")))
- }
- }
- }
-
- searchTimeouts[searchKey] = timeoutRunnable
- handler.postDelayed(timeoutRunnable, SEARCH_TIMEOUT_MS)
-
- ensureMusicManager(currentActivity)
- musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback {
- override fun onSuccess(songs: List