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 8914a89bb..0f35547e7 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 @@ -346,8 +346,7 @@ class AgentServiceImpl: NSObject { ttsAppId: "", ttsAppToken: azureSpeechKey, ttsResource: azureSpeechRegion, - language: primaryLanguage, - isExtaudioSource: BleService.shared.isConnected() + language: primaryLanguage ), ttsSuccess else { sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR") return false @@ -392,7 +391,7 @@ class AgentServiceImpl: NSObject { private func checkAdnSetAudioSession() { let audioSourceType: AzureAsrHelper.AudioSourceType = BleService.shared.isConnected() ? .external : .microphone - azureAsrHelper?.performAudioStart(audioSourceType: audioSourceType, audioDataCallback: nil) + azureAsrHelper?.performAudioStart(mod: currentRecognitionMode, audioSourceType: audioSourceType, audioDataCallback: nil) } private func startIdleCheckForMode(mode: String) { @@ -402,8 +401,6 @@ class AgentServiceImpl: NSObject { // 根据模式决定空闲检测策略 switch mode { case "phone_call": - _ = azureAsrHelper?.setPhoneCallMode(enabled: true) - _ = azureTtsHelper?.setPhoneCallMode(enabled: true) // 通话模式:禁用空闲检测,保持持续激活 os_log("通话模式:禁用空闲检测", log: logger, type: .info) return @@ -524,9 +521,6 @@ class AgentServiceImpl: NSObject { // 设置当前识别模式 currentRecognitionMode = mode os_log("设置语音识别模式: %{public}@", log: logger, type: .info, mode) - _ = azureAsrHelper?.setPhoneCallMode(enabled: mode == "phone_call") - _ = azureTtsHelper?.setPhoneCallMode(enabled: mode == "phone_call") - _ = azureAsrHelper?.setPreferBuiltInMic(enabled: mode == "phone_call" || mode == "push_to_talk") let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone @@ -583,11 +577,6 @@ class AgentServiceImpl: NSObject { os_log("停止语音识别,当前模式: %{public}@", log: logger, type: .info, currentRecognitionMode) let success = azureAsrHelper?.stopContinuousRecognition() ?? false - if currentRecognitionMode == "phone_call" { - _ = azureAsrHelper?.setPhoneCallMode(enabled: false) - _ = azureTtsHelper?.setPhoneCallMode(enabled: false) - } - _ = azureAsrHelper?.setPreferBuiltInMic(enabled: false) if !success && isStartingRecognition { // 启动尚未完成,先记录一次待停止请求,onSessionStarted 到来后立即 stop @@ -639,7 +628,7 @@ class AgentServiceImpl: NSObject { } _ = azureTtsHelper?.setSpeechParams(rate: ttsRatePercent, pitch: 0, volume: 100) - _ = azureTtsHelper?.setAudioSourceType(isExtaudioSource: BleService.shared.isConnected()) + _ = azureTtsHelper?.setPhoneCallMode(mod: currentRecognitionMode) return azureTtsHelper?.speakOnce(sessionid:sessionid,text) ?? false } @@ -1798,7 +1787,7 @@ class ChatApiStreamCallback: StreamCallback { responseBuilder += token if speakResponse && reply && broadcast{ - _ = agentService.azureTtsHelper?.setAudioSourceType(isExtaudioSource: BleService.shared.isConnected()) + _ = agentService.azureTtsHelper?.setPhoneCallMode(mod:agentService.currentRecognitionMode) try agentService.azureTtsHelper?.speakStream(sessionid:sessionid,token) // 在开始流式TTS时立即停止气泡音 } @@ -1824,7 +1813,7 @@ class ChatApiStreamCallback: StreamCallback { } if speakResponse && reply && broadcast && sessionid == agentService.currsessionId{ - _ = agentService.azureTtsHelper?.setAudioSourceType(isExtaudioSource: BleService.shared.isConnected()) + _ = agentService.azureTtsHelper?.setPhoneCallMode(mod: agentService.currentRecognitionMode) agentService.azureTtsHelper?.flushStream(sessionid:sessionid) } @@ -1876,7 +1865,7 @@ class ChatApiStreamCallback: StreamCallback { "message": message ]) if(code == 2001){ - _ = agentService.azureTtsHelper?.setAudioSourceType(isExtaudioSource: BleService.shared.isConnected()) + _ = agentService.azureTtsHelper?.setPhoneCallMode(mod: agentService.currentRecognitionMode) agentService.azureTtsHelper?.speakStream(sessionid: sessionid,agentService.insufficientIntegralText) } agentService.isAiStreaming = false @@ -1920,7 +1909,7 @@ class ChatApiStreamCallback: StreamCallback { if (iscallingTool) { os_log("收到函数调用: callingToolText:%{public}@", log: agentService.logger, type: .info, agentService.callingToolText) - _ = agentService.azureTtsHelper?.setAudioSourceType(isExtaudioSource: BleService.shared.isConnected()) + _ = agentService.azureTtsHelper?.setPhoneCallMode(mod: agentService.currentRecognitionMode) agentService.azureTtsHelper?.speakStream(sessionid: sessionid,agentService.callingToolText) iscallingTool = false } 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 a69b0c385..f6f89fffa 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 @@ -81,22 +81,9 @@ public class AzureAsrHelper: NSObject { // 音频处理 public var audioStream: SimpleAudioReceiver? - /** - * 设置 AI 电话/按住说话场景下是否优先使用手机内置麦克风 - * @param enabled 是否启用 - * @return 是否设置成功 - * @throws 无 - */ - public func setPreferBuiltInMic(enabled: Bool) -> Bool { - audioStream?.setPreferBuiltInMic(enabled) - return true - } - public func setPhoneCallMode(enabled: Bool) -> Bool { - isPhoneCallMode = enabled - audioStream?.setPhoneCallMode(enabled) - return true - } + + public func asrProvider() -> String { return useXunfei ? "xunfei" : "azure" @@ -255,7 +242,6 @@ public class AzureAsrHelper: NSObject { if audioStream == nil { audioStream = SimpleAudioReceiver() audioStream?.initAudioRecord() - audioStream?.setPhoneCallMode(isPhoneCallMode) } // 预创建音频配置 if let pushStream = audioStream?.pushAudioStream { @@ -283,6 +269,7 @@ public class AzureAsrHelper: NSObject { /// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效 /// 2) 任务执行前二次校验:必须是当前挂起任务且 audioStream?.isContinuousRecognitionActive 仍为 true 才执行启动 private func startAudioRecordAsync( + mod: String, audioSourceType: AudioSourceType, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, completion: @escaping (Bool) -> Void @@ -325,7 +312,7 @@ public class AzureAsrHelper: NSObject { // 准备启动 self.isAudioStarting = true self.pendingStopRequest = false // 启动前清空旧的停止请求 - let success = self.performAudioStart(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) + let success = self.performAudioStart(mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) self.isAudioStarting = false self.isAudioStarted = success @@ -358,12 +345,14 @@ public class AzureAsrHelper: NSObject { * 执行音频启动操作 */ public func performAudioStart( + mod: String, audioSourceType: AudioSourceType, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? ) -> Bool { do { if(!audioStream!.isContinuousRecognitionActive){ audioStream?.startAudioRecord( + mod: mod, audioSourceType: audioSourceType == .microphone ? .microphone : .external, audioDataCallback: audioDataCallback ) @@ -430,6 +419,7 @@ public class AzureAsrHelper: NSObject { * 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。 */ public func startContinuousRecognition( + mod: String = "normal", audioSourceType: AudioSourceType = .microphone, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, isRemoveFirstPunctuation: Bool = true @@ -487,7 +477,7 @@ public class AzureAsrHelper: NSObject { } // 异步启动音频处理 - startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: proxyCallback) { [weak self] (success: Bool) in + startAudioRecordAsync(mod: mod, audioSourceType: audioSourceType, audioDataCallback: proxyCallback) { [weak self] (success: Bool) in guard let self = self else { return } if success { @@ -516,7 +506,7 @@ public class AzureAsrHelper: NSObject { // self.audioStream?.isContinuousRecognitionActive = true // 异步启动音频处理 - startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in + startAudioRecordAsync(mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in guard let self = self else { return } if success { @@ -787,7 +777,7 @@ public class AzureAsrHelper: NSObject { if audioStream == nil { audioStream = SimpleAudioReceiver() audioStream?.initAudioRecord() // 确保调用初始化 - audioStream?.setPhoneCallMode(isPhoneCallMode) + } // 检查音频配置是否已存在 @@ -1012,7 +1002,7 @@ public class AzureAsrHelper: NSObject { // 修复:添加类型转换 if(!audioStream!.isContinuousRecognitionActive){ // 异步启动音频处理 - startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in + startAudioRecordAsync(mod: "normal", audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in guard let self = self else { return } } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift index aee7d4930..181fc90c4 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift @@ -77,30 +77,12 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate { private var pushStreamChunkCount = 0 private var pushStreamCallbackCount = 0 private var synthEventAudioCount = 0 - private var isExtaudioSource = false - private var isPhoneCallMode = false + public var isHeadphones = true + public var currentRecognitionMode: String = "normal" + - /** - * 初始化语音合成服务 - * - Parameters: - * - ttsAppId: 服务应用ID(Azure 场景未使用,可为空) - * - ttsAppToken: Azure 语音服务订阅密钥 - * - ttsResource: Azure 语音服务区域 - * - language: 语言代码,如 "zh-CN" - * - Returns: 是否初始化成功 - * - Throws: 无(内部捕获 SDK 异常并记录日志) - */ - public func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool { - return initialize( - ttsAppId: ttsAppId, - ttsAppToken: ttsAppToken, - ttsResource: ttsResource, - language: language, - isExtaudioSource: false - ) - } /** * 初始化语音合成服务(支持外部音频源场景) @@ -109,11 +91,10 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate { * - ttsAppToken: Azure 语音服务订阅密钥 * - ttsResource: Azure 语音服务区域 * - language: 语言代码,如 "zh-CN" - * - isExtaudioSource: 是否为外部音频源(例如 BLE 输入) * - Returns: 是否初始化成功 * - Throws: 无(内部捕获 SDK 异常并记录日志) */ - public func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String, isExtaudioSource: Bool) -> Bool { + public func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool { do { micCapture = MicrophoneCapture.shared // 创建语音配置 @@ -131,7 +112,6 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate { // 创建合成器 recreateSynthesizer() - self.isExtaudioSource = isExtaudioSource // 设置初始化完成 isInitialized = true @@ -433,22 +413,31 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate { } } - public func setAudioSourceType(isExtaudioSource: Bool) -> Bool { - self.isExtaudioSource = isExtaudioSource + + public func setPhoneCallMode(mod: String) -> Bool { + self.currentRecognitionMode = mod + self.isHeadphones = detectHeadphonesOutput() if isInitialized { safeConfigureAudioSessionForTTS() } return true } - - public func setPhoneCallMode(enabled: Bool) -> Bool { - isPhoneCallMode = enabled - if isInitialized { - safeConfigureAudioSessionForTTS() + /** + * 检测是否存在耳机/蓝牙输出设备(用于决定是否启用A2DP而非扬声器) + * 参数:无 + * 返回值:存在返回true,否则返回false + * 异常:无 + */ + private func detectHeadphonesOutput() -> Bool { + let session = AVAudioSession.sharedInstance() + let outputs = session.currentRoute.outputs + return outputs.contains { out in + out.portType == .bluetoothA2DP || + out.portType == .bluetoothHFP || + out.portType == .bluetoothLE || + out.portType == .headphones } - return true } - /** * 设置音频输出设备 */ @@ -1376,15 +1365,15 @@ private func applyAudioSessionForTTS() -> Bool { desiredOptions.insert(.duckOthers) } else if isRecording { desiredCategory = .playAndRecord - if self.isPhoneCallMode { + if self.currentRecognitionMode == "phone_call" { desiredMode = .voiceChat desiredOptions.insert(.allowBluetooth) } else { - desiredMode = self.isExtaudioSource ? .default : .videoChat + desiredMode = self.isHeadphones ? .default : .videoChat desiredOptions.insert(.allowBluetooth) desiredOptions.insert(.allowBluetoothA2DP) } - if self.isExtaudioSource { + if self.isHeadphones { if otherAudioPlaying { desiredOptions.insert(.duckOthers) } @@ -1417,7 +1406,7 @@ private func applyAudioSessionForTTS() -> Bool { _ = try? audioSession.setPreferredIOBufferDuration(0.02) } try audioSession.setActive(true) - if self.isPhoneCallMode, desiredCategory == .playAndRecord, !hasHeadphones { + if self.currentRecognitionMode == "phone_call", desiredCategory == .playAndRecord, !hasHeadphones { _ = try? audioSession.overrideOutputAudioPort(.speaker) } break 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 b5a0a8a5f..3944b0b0a 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 @@ -54,10 +54,8 @@ public class SimpleAudioReceiver: NSObject { private var currentRoute: AudioOutputRoute? //public var onAudioData: ((Data) -> Void)? public var recordfile: RecordFile? + private var currentRecognitionMode = "normal" // 当前识别模式:normal, ble_wakeup, phone_call, push_to_talk public var isHeadphones = true - public var isReceiverHFP = false // 是否是接收HFP音频 - public var isPhoneCallMode = false // AI电话模式:录音+播放强制走电话链路(voiceChat/HFP) - public var preferBuiltInMic = false // 耳机连接时优先使用手机内置麦克风录音 /** * 初始化 @@ -125,39 +123,118 @@ public class SimpleAudioReceiver: NSObject { /** * 开始音频输入 - * 参数:audioSourceType 音频源类型(microphone 或 external);audioDataCallback 音频数据回调 - * 返回值:无 - * 异常:无(内部捕获并打印错误,不抛出) + * - Parameter mod: 识别模式,如 "normal", "phone_call", "push_to_talk", "ble_wakeup"。 + * - Parameter audioSourceType: 音频源类型(.microphone 或 .external)。 + * - Parameter audioDataCallback: 音频数据回调。 */ - public func startAudioRecord(audioSourceType: AudioSourceType = .microphone, audioDataCallback: AudioDataCallback?) { - _isWriting=true - self.audioSourceType = audioSourceType; - self.audioDataCallback = audioDataCallback - print("音源设制---------- startAudioRecord=audioSourceType:\(audioSourceType)") - switch audioSourceType { - case .microphone: - isReceiverHFP = isPhoneCallMode - // 动态检测是否存在耳机/蓝牙输出,用于后续路由选项计算 - isHeadphones = detectHeadphonesOutput() + public func startAudioRecord(mod: String = "normal", audioSourceType: AudioSourceType = .microphone, audioDataCallback: AudioDataCallback?) { + self.currentRecognitionMode = mod + self.audioSourceType = audioSourceType + self.audioDataCallback = audioDataCallback + self.isHeadphones = detectHeadphonesOutput() + + _isWriting = true + + print("音源设置---------- startAudioRecord=mod:\(mod), audioSourceType:\(audioSourceType), isHeadphones:\(isHeadphones)") + + do { + try configureAudioSession() + + let isHybridExternalMode = audioSourceType == .external && (currentRecognitionMode == "phone_call" || currentRecognitionMode == "push_to_talk") + + if audioSourceType == .microphone || isHybridExternalMode { runMicrophoneCapture() - case .external: - isReceiverHFP = false + } else { // True external mode runExternalCapture() } - } - - public func setPhoneCallMode(_ enabled: Bool) { - isPhoneCallMode = enabled + } catch { + print("配置或启动音频捕获失败: \(error.localizedDescription)") + } } /** - * 设置是否优先使用手机内置麦克风录音 - * @param enabled 是否启用 - * @return 无 - * @throws 无 + * 根据当前模式和设备状态配置音频会话 */ - public func setPreferBuiltInMic(_ enabled: Bool) { - preferBuiltInMic = enabled + private func configureAudioSession() throws { + let audioSession = self.audioSession + + var desiredCategory: AVAudioSession.Category + var desiredMode: AVAudioSession.Mode + var desiredOptions: AVAudioSession.CategoryOptions + + let isHybridExternalMode = audioSourceType == .external && (currentRecognitionMode == "phone_call" || currentRecognitionMode == "push_to_talk") + + if audioSourceType == .microphone || isHybridExternalMode { + // --- 麦克风输入逻辑 (包括混合模式) --- + desiredCategory = .playAndRecord + + if currentRecognitionMode == "phone_call" { + desiredMode = .voiceChat + desiredOptions = isHeadphones ? [.allowBluetooth] : [.allowBluetooth, .defaultToSpeaker] + } else { // normal, push_to_talk, ble_wakeup 等 + desiredMode = .videoChat + if isHeadphones { + desiredOptions = [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers] + } else { + desiredOptions = [.mixWithOthers, .defaultToSpeaker] + } + } + + // 如果在真实通话中,强制使用更适合的录音设置 + if isTelephonyActive() { + desiredCategory = .record + desiredMode = .voiceChat + desiredOptions = [.allowBluetooth] + } + + } else if audioSourceType == .external { + // --- 纯外部源 (仅播放) 逻辑 --- + desiredCategory = .playback + desiredMode = .videoChat + desiredOptions = isHeadphones ? [.allowBluetoothA2DP, .mixWithOthers] : [.mixWithOthers, .defaultToSpeaker] + } else { + // 兜底,理论上不会执行 + desiredCategory = .playAndRecord + desiredMode = .default + desiredOptions = [] + } + + // 仅在需要时更新会话,避免不必要的中断 + if audioSession.category != desiredCategory || audioSession.mode != desiredMode || audioSession.categoryOptions != desiredOptions { + try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions) + } + + // 激活会话 + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + + // --- 路由设置 --- + if audioSourceType == .microphone || isHybridExternalMode { + // 为需要麦克风的场景设置输入输出 + if isHeadphones { + try audioSession.overrideOutputAudioPort(.none) // 允许耳机/蓝牙输出 + } else { + try audioSession.overrideOutputAudioPort(.speaker) // 强制外放 + } + + // 设置首选输入设备 + if let inputs = audioSession.availableInputs { + if isHybridExternalMode { + // 混合模式: 强制使用内置麦克风 + if let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { + try audioSession.setPreferredInput(builtInMic) + print("混合外部模式: 首选输入已设置为内置麦克风") + } + } else if let btInput = inputs.first(where: { $0.portType == .bluetoothHFP }), isHeadphones { + // 标准麦克风模式: 优先使用蓝牙耳机麦克风 + try audioSession.setPreferredInput(btInput) + } else if let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { + // 兜底: 使用内置麦克风 + try audioSession.setPreferredInput(builtInMic) + } + } + } else { // 纯外部源 + try audioSession.overrideOutputAudioPort(.none) + } } /** @@ -255,9 +332,6 @@ public class SimpleAudioReceiver: NSObject { // 确保正在写入状态 guard self._isWriting else { return } - // 根据通话状态进行安全的音频会话配置 - try safeConfigureAudioSessionForMic() - // 启动麦克风采集 try micCapture.startCapture() @@ -282,154 +356,8 @@ public class SimpleAudioReceiver: NSObject { */ private func runExternalCapture() { if audioSourceType == .external { - do { - // 停止麦克风采集,确保不走通话音道 - micCapture.stopCapture() - // 外部模式会话配置:禁止HFP,仅允许A2DP/耳机输出 - try safeConfigureAudioSessionForExternal() - } catch { - print("设置音频会话失败: \(error.localizedDescription)") - } - //pushAudioData(data: Data()) - } - } - - /** - * 外部音频源的安全音频会话配置(禁止HFP通话音道) - * 参数:无 - * 返回值:无 - * 异常:可能抛出AVAudioSession设置错误(内部捕获并兜底) - */ - private func safeConfigureAudioSessionForExternal() throws { - let audioSession = self.audioSession - do { - // 外部模式不录麦,仅做播放路由;有耳机时只允许A2DP,禁止HFP - let outputs = audioSession.currentRoute.outputs - let hasHeadphones = outputs.contains { out in - out.portType == .bluetoothA2DP || - out.portType == .bluetoothLE || - out.portType == .headphones - } - let desiredCategory: AVAudioSession.Category = .playback - let desiredMode: AVAudioSession.Mode = .videoChat - let desiredOptions: AVAudioSession.CategoryOptions = hasHeadphones - ? [.allowBluetoothA2DP, .mixWithOthers] - : [.mixWithOthers, .defaultToSpeaker] - - let needsSetCategory = (audioSession.category != desiredCategory) || (audioSession.mode != desiredMode) - if needsSetCategory { - try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions) - } - // 取消扬声器强制,确保耳机/A2DP优先 - try audioSession.overrideOutputAudioPort(.none) - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - } catch { - print("外部模式音频会话配置失败,兜底到playback: \(error)") - try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers, .defaultToSpeaker]) - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - } - } - /** - * 在通话/非通话场景下安全配置音频会话用于麦克风采集 - * - 通话中:使用 .record + .voiceChat + [.allowBluetooth],避免 A2DP 和强制外放,兼容电话路由 - * - 非通话:使用 .playAndRecord + .videoChat + [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] - * - 失败兜底:退回到最简单的 .record + .default,确保录音尽可能可用 - */ - private func safeConfigureAudioSessionForMic() throws { - let audioSession = self.audioSession - do { - // 计算目标配置 - let desiredCategory: AVAudioSession.Category - let desiredMode: AVAudioSession.Mode - let desiredOptions: AVAudioSession.CategoryOptions - if isPhoneCallMode { - desiredCategory = .playAndRecord - desiredMode = .voiceChat - if isHeadphones { - desiredOptions = [.allowBluetooth] - } else { - desiredOptions = [.allowBluetooth, .defaultToSpeaker] - } - } else if isReceiverHFP { - if isTelephonyActive() { - // 通话中:录音优先,避免 A2DP(仅播放),改用 HFP(.allowBluetooth) - desiredCategory = .record - desiredMode = .voiceChat - desiredOptions = [.allowBluetooth] - } else { - if(isHeadphones){ - // 非通话:保持原有逻辑,但使用 .allowBluetooth 以支持录音(而不是 A2DP) - desiredCategory = .playAndRecord - desiredMode = .videoChat - desiredOptions = [.allowBluetooth, .mixWithOthers] - //desiredOptions = [.mixWithOthers, .defaultToSpeaker] - }else{ - desiredCategory = .playAndRecord - desiredMode = .videoChat - desiredOptions = [.mixWithOthers, .defaultToSpeaker] - } - } - }else{ - if isTelephonyActive() { - // 通话中:录音优先,避免 A2DP(仅播放),改用 HFP(.allowBluetooth) - desiredCategory = .record - desiredMode = .voiceChat - desiredOptions = [.allowBluetooth] - } else { - if(isHeadphones){ - // 非通话:保持原有逻辑,但使用 .allowBluetooth 以支持录音(而不是 A2DP) - desiredCategory = .playAndRecord - desiredMode = .videoChat - desiredOptions = [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers] - //desiredOptions = [.mixWithOthers, .defaultToSpeaker] - }else{ - desiredCategory = .playAndRecord - desiredMode = .videoChat - desiredOptions = [.mixWithOthers, .defaultToSpeaker] - } - } - } - - - // 仅在类别或模式变化时才调用 setCategory,避免触发不必要的 categoryChange - let needsSetCategory = (audioSession.category != desiredCategory) || (audioSession.mode != desiredMode) - if needsSetCategory { - try audioSession.setCategory(desiredCategory, - mode: desiredMode, - options: desiredOptions) - } else { - // 类别与模式一致时,也尽量避免重复 setCategory;如需确保 options,可在确有必要时再设置 - // 如果你确实需要校正 options,可取消下一行注释,但这可能再次触发 categoryChange - // try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions) - } - - // 保持激活;这里重复 setActive(true) 一般安全,但如果你观察到重复激活也触发路由回调,可在外层控制频率 - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - - // 路由校正:有蓝牙输出时不强制外放,并优先选择蓝牙输入(HFP)或内置麦克风 - let outputs = audioSession.currentRoute.outputs - let hasBtOutput = outputs.contains { out in - out.portType == .bluetoothA2DP || out.portType == .bluetoothHFP || out.portType == .bluetoothLE - } - if hasBtOutput { - try audioSession.overrideOutputAudioPort(.none) - } else if isPhoneCallMode, !isHeadphones { - try audioSession.overrideOutputAudioPort(.speaker) - } - if let inputs = audioSession.availableInputs { - if preferBuiltInMic, let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { - try audioSession.setPreferredInput(builtInMic) - } else if isReceiverHFP, let btInput = inputs.first(where: { $0.portType == .bluetoothHFP }) { - try audioSession.setPreferredInput(btInput) - } else if let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { - try audioSession.setPreferredInput(builtInMic) - } - } - } catch { - // 兜底:尽量保证能开始录音 - print("音频会话配置失败,尝试兜底配置: \(error)") - try audioSession.setCategory(.record, mode: .default, options: []) - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + // 停止麦克风采集,确保不走通话音道 + micCapture.stopCapture() } } @@ -451,12 +379,12 @@ public class SimpleAudioReceiver: NSObject { * 继续麦克风捕获 */ public func resumeRecord() { - guard _isWriting==false else { return } + guard _isWriting==false else { return } _isWriting = true // 启动录音引擎 do { print("resumeRecord") - try safeConfigureAudioSessionForMic() + try configureAudioSession() try micCapture.startCapture() } catch { if let nsError = error as NSError? {