Browse Source

refactor(audio): 重构音频处理逻辑,简化模式管理和设备检测

移除冗余的音频模式设置方法,统一使用currentRecognitionMode管理识别模式
优化音频会话配置逻辑,根据模式和音频源类型动态调整参数
新增耳机设备检测功能,自动适配最佳音频路由
newdev_shunjiawei
liwei1dao 9 months ago
parent
commit
0cda1ec293
  1. 25
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  2. 32
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  3. 63
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  4. 290
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

25
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
}

32
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 }
}

63
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

290
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? {

Loading…
Cancel
Save