import Foundation import AVFoundation import UIKit /// 统一音频会话管理类 /// 用于统一管理所有组件的音频会话配置 @objc class AudioSessionManager: NSObject { // MARK: - 单例实现 @objc static let shared = AudioSessionManager() // 音频会话 private let audioSession = AVAudioSession.sharedInstance() // 音频会话状态 private var isConfigured = false private var isActive = false // 当前配置类型 private var currentCategory: AVAudioSession.Category? private var currentMode: AVAudioSession.Mode? private var currentOptions: AVAudioSession.CategoryOptions? // 中断监听器 private var interruptionObserver: NSObjectProtocol? // 路由变化监听器 private var routeChangeObserver: NSObjectProtocol? // 私有初始化方法,确保只能通过shared访问 private override init() { super.init() NSLog("[AudioSessionManager] 音频会话管理器已初始化") setupNotifications() } deinit { removeNotifications() } // MARK: - 通知设置 private func setupNotifications() { // 监听中断事件 interruptionObserver = NotificationCenter.default.addObserver( forName: AVAudioSession.interruptionNotification, object: nil, queue: .main) { [weak self] notification in self?.handleInterruption(notification) } // 监听路由变化事件 routeChangeObserver = NotificationCenter.default.addObserver( forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) { [weak self] notification in self?.handleRouteChange(notification) } // 监听应用进入后台/前台 NotificationCenter.default.addObserver( self, selector: #selector(handleAppDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(handleAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } private func removeNotifications() { if let interruptionObserver = interruptionObserver { NotificationCenter.default.removeObserver(interruptionObserver) } if let routeChangeObserver = routeChangeObserver { NotificationCenter.default.removeObserver(routeChangeObserver) } NotificationCenter.default.removeObserver(self) } // MARK: - 通知处理 private func handleInterruption(_ notification: Notification) { guard let userInfo = notification.userInfo, let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } switch type { case .began: // 中断开始 NSLog("[AudioSessionManager] 音频会话被中断") isActive = false case .ended: // 中断结束,尝试恢复 if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) if options.contains(.shouldResume) { NSLog("[AudioSessionManager] 尝试恢复被中断的音频会话") try? activateSession() } } @unknown default: break } } private func handleRouteChange(_ notification: Notification) { guard let userInfo = notification.userInfo, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } switch reason { case .newDeviceAvailable: // 新设备连接 let currentRoute = audioSession.currentRoute for output in currentRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP { NSLog("[AudioSessionManager] 检测到蓝牙音频设备连接: \(output.portName)") // 蓝牙设备连接,可能需要更新配置 if isActive && currentOptions?.contains(.allowBluetooth) == true { try? forceReconfigure() } break } case .oldDeviceUnavailable: // 设备断开连接 if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { for output in previousRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP { NSLog("[AudioSessionManager] 蓝牙音频设备断开连接: \(output.portName)") // 蓝牙设备断开,重新配置到内置扬声器 if isActive { try? forceReconfigure() } break } } default: break } } @objc private func handleAppDidEnterBackground() { NSLog("[AudioSessionManager] 应用进入后台") // 应用进入后台时可能需要调整音频会话 } @objc private func handleAppWillEnterForeground() { NSLog("[AudioSessionManager] 应用即将进入前台") // 应用返回前台,检查并恢复音频会话 if isConfigured && !isActive { try? activateSession() } } // MARK: - 音频会话配置 /// 统一配置方法 - 所有音频配置都通过此方法进行 @objc func configureSession(category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions, force: Bool = false) throws { NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode), 选项=\(options), 强制=\(force)") // 如果已配置且不强制重新配置,则直接返回 if isConfigured && !force && currentCategory == category && currentMode == mode && currentOptions == options { NSLog("[AudioSessionManager] 音频会话已配置,跳过配置") return } do { // NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode)") // 暂时停用当前会话(如果正在活动) if isActive { try audioSession.setActive(false, options: .notifyOthersOnDeactivation) } // 设置新的分类、模式和选项 try audioSession.setCategory(category, mode: mode, options: options) // 设置采样率和缓冲区大小(对于语音识别优化) // if mode == .spokenAudio { // try audioSession.setPreferredSampleRate(16000) // try audioSession.setPreferredIOBufferDuration(0.01) // 10ms // } // 重新激活会话 try audioSession.setActive(true, options: .notifyOthersOnDeactivation) isActive = true // 切换到扬声器 // 记录当前配置 currentCategory = category currentMode = mode currentOptions = options isConfigured = true // 检查并设置首选的输入设备(如果需要) if category == .playAndRecord || category == .record { try configurePreferredInput() } return } catch { NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)") throw error } } /// 配置首选输入设备 private func configurePreferredInput() throws { // 获取可用输入设备 guard let availableInputs = audioSession.availableInputs else { NSLog("[AudioSessionManager] 无法获取可用输入设备") return } // 首选顺序:有线耳机 > 蓝牙耳机 > 内置麦克风 var preferredInput: AVAudioSessionPortDescription? = nil // 先尝试找到有线耳机 if let wiredHeadset = availableInputs.first(where: { $0.portType == .headsetMic }) { preferredInput = wiredHeadset NSLog("[AudioSessionManager] 设置首选输入设备: 有线耳机") } // 如果没有有线耳机,尝试使用蓝牙 else if let bluetooth = availableInputs.first(where: { $0.portType == .bluetoothHFP }) { preferredInput = bluetooth NSLog("[AudioSessionManager] 设置首选输入设备: 蓝牙耳机") } // 如果没有外部设备,使用内置麦克风 else if let builtIn = availableInputs.first(where: { $0.portType == .builtInMic }) { preferredInput = builtIn NSLog("[AudioSessionManager] 设置首选输入设备: 内置麦克风") } // 设置首选输入设备 if let input = preferredInput { try audioSession.setPreferredInput(input) } } /// 激活音频会话(如果已配置) @objc func activateSession() throws { if !isConfigured { NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话,应用默认配置") // 未配置,应用默认配置 // try configureSession( // category: .playAndRecord, // mode: .spokenAudio, // options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers, .defaultToSpeaker], // force: true // ) try configureSession( category: .playAndRecord, // 允许同时录音和播放 mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 options: [ .allowBluetooth, // 允许蓝牙设备 .defaultToSpeaker, // 默认使用扬声器 .mixWithOthers // 允许与其他应用混音 ], force: false // 是否强制重新配置 ) } else if !isActive { NSLog("[AudioSessionManager] 激活已配置的音频会话") try audioSession.setActive(true, options: .notifyOthersOnDeactivation) isActive = true } } /// 停用音频会话(保留配置) @objc func deactivateSession() throws { if isActive { NSLog("[AudioSessionManager] 停用音频会话") try audioSession.setActive(false, options: .notifyOthersOnDeactivation) isActive = false } } /// 重新配置当前会话 @objc func forceReconfigure() throws { NSLog("[AudioSessionManager] 强制重新配置音频会话") // try configureSession( // category: currentCategory ?? .playAndRecord, // mode: currentMode ?? .default, // options: currentOptions ?? [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker], // force: true // ) try configureSession( category: .playAndRecord, // 允许同时录音和播放 mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 options: [ .allowBluetooth, // 允许蓝牙设备 .defaultToSpeaker, // 默认使用扬声器 .mixWithOthers // 允许与其他应用混音 ], force: false // 是否强制重新配置 ) try audioSession.overrideOutputAudioPort(.speaker) } // MARK: - 状态检查 /// 检查麦克风权限 @objc func checkMicrophonePermission(completion: @escaping (Bool) -> Void) { switch AVAudioSession.sharedInstance().recordPermission { case .granted: completion(true) case .denied: NSLog("[AudioSessionManager] 麦克风权限被拒绝") completion(false) case .undetermined: NSLog("[AudioSessionManager] 请求麦克风权限") AVAudioSession.sharedInstance().requestRecordPermission { granted in DispatchQueue.main.async { completion(granted) } } @unknown default: completion(false) } } /// 获取当前音频会话的配置信息 @objc func getCurrentConfiguration() -> [String: Any] { return [ "isConfigured": isConfigured, "isActive": isActive, "category": String(describing: currentCategory), "mode": String(describing: currentMode), "bluetoothEnabled": currentOptions?.contains(.allowBluetooth) == true, "currentInputs": audioSession.currentRoute.inputs.map { [$0.portType.rawValue: $0.portName] }, "currentOutputs": audioSession.currentRoute.outputs.map { [$0.portType.rawValue: $0.portName] } ] } /// 检查当前是否有蓝牙耳机连接 @objc func hasBluetoothHeadsetConnected() -> Bool { let outputs = audioSession.currentRoute.outputs return outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE } } /// 检查蓝牙是否启用 @objc func isBluetoothEnabled() -> Bool { // 在iOS中无法直接检查蓝牙是否启用,我们可以通过检查音频会话配置来推断 // 检查音频会话是否配置了允许蓝牙 if let options = currentOptions { return options.contains(.allowBluetooth) || options.contains(.allowBluetoothA2DP) } // 如果尚未配置音频会话,尝试检查当前路由中是否有蓝牙设备 let outputs = audioSession.currentRoute.outputs let hasBluetooth = outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE } return hasBluetooth } /// 获取当前音频路由 @objc func getCurrentAudioRoute() -> String { let outputs = audioSession.currentRoute.outputs if let output = outputs.first { return "\(output.portType.rawValue) (\(output.portName))" } return "unknown" } /// 获取当前输入设备 @objc func getCurrentInputDevice() -> String { let inputs = audioSession.currentRoute.inputs if let input = inputs.first { return "\(input.portType.rawValue) (\(input.portName))" } return "unknown" } // MARK: - 路由管理和监听 /// 获取当前路由信息 @objc func getCurrentRoute() -> AVAudioSessionRouteDescription { return audioSession.currentRoute } /// 添加路由变更监听器 /// - Parameters: /// - observer: 监听对象 /// - selector: 监听方法选择器 @objc func addRouteChangeListener(_ observer: Any, selector: Selector) { NotificationCenter.default.addObserver( observer, selector: selector, name: AVAudioSession.routeChangeNotification, object: nil ) } /// 移除路由变更监听器 /// - Parameter observer: 要移除的监听对象 @objc func removeRouteChangeListener(_ observer: Any) { NotificationCenter.default.removeObserver( observer, name: AVAudioSession.routeChangeNotification, object: nil ) } /// 获取设备类型描述 private func getDeviceType(portType: AVAudioSession.Port) -> String { switch portType { case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE: return "bluetooth" case .headphones, .headsetMic: return "wired" case .airPlay: return "airplay" default: return "unknown" } } // /// 为Azure语音识别专门配置的音频会话 // /// 此函数优化Azure语音识别的音频处理配置,提供更强的回音消除能力 // /// - Parameters: // /// - force: 是否强制重新配置 // /// - configureAdditionalSettings: 配置完成后进行额外的音频设置 // /// - Returns: 配置是否成功 // @objc func configureForAzureSpeechRecognition(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool { // NSLog("[AudioSessionManager] 配置Azure ASR语音识别的音频会话") // do { // // 配置音频会话以优化语音识别 // try configureSession( // category: .playAndRecord, // 允许同时录音和播放 // mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 // options: [ // .allowBluetooth, // 允许蓝牙设备 // .defaultToSpeaker, // 默认使用扬声器 // .mixWithOthers // 允许与其他应用混音 // ], // force: force // 是否强制重新配置 // ) // // 额外的优化配置 // if configureAdditionalSettings { // // 获取当前是否使用耳机 // let currentRoute = audioSession.currentRoute // let hasHeadphones = currentRoute.outputs.contains { // $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP // } // // 设置采样率为16kHz(Azure Speech API推荐) // try audioSession.setPreferredSampleRate(16000.0) // // 设置较小的缓冲区大小以减少延迟 // try audioSession.setPreferredIOBufferDuration(0.01) // // 根据是否有耳机连接调整输入增益 // if !hasHeadphones { // // 无耳机时降低输入增益以减少回音 // try audioSession.setInputGain(0.8) // NSLog("[AudioSessionManager] 启用扬声器回音消除优化") // } else { // // 使用耳机时可以使用较高增益 // try audioSession.setInputGain(1.0) // NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式") // } // } // NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话") // return true // } catch { // NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)") // return false // } // } @objc func configureForVoiceInteraction(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool { NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话") do { // 配置音频会话以优化语音识别 try configureSession( category: .playAndRecord, // 允许同时录音和播放 mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 options: [ .allowBluetooth, // 允许蓝牙设备 .defaultToSpeaker, // 默认使用扬声器 .mixWithOthers // 允许与其他应用混音 ], force: force // 是否强制重新配置 ) try audioSession.overrideOutputAudioPort(.speaker) // 额外的优化配置 if configureAdditionalSettings { // 获取当前是否使用耳机 let currentRoute = audioSession.currentRoute let hasHeadphones = currentRoute.outputs.contains { $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP } // 设置采样率为16kHz(Azure Speech API推荐) try audioSession.setPreferredSampleRate(16000.0) // 设置较小的缓冲区大小以减少延迟 try audioSession.setPreferredIOBufferDuration(0.01) // 根据是否有耳机连接调整输入增益 if !hasHeadphones { // 无耳机时降低输入增益以减少回音 try audioSession.setInputGain(0.8) NSLog("[AudioSessionManager] 启用扬声器回音消除优化") } else { // 使用耳机时可以使用较高增益 try audioSession.setInputGain(1.0) NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式") } } NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话") return true } catch { NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)") return false } } /// 配置用于语音交互(同时支持ASR和TTS)的音频会话 /// 此函数综合优化语音识别和语音合成,适用于需要双向交互的场景 /// - Returns: 配置是否成功 // @objc func configureForVoiceInteraction(force: Bool = false) -> Bool { // do { // // 配置音频会话以支持语音交互 // // 使用playAndRecord类别允许同时录音和播放 // // 使用spokenAudio模式优化语音交互 // try configureSession( // category: .playAndRecord, // mode: .voiceChat, // options: [ // // .allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出 // .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出 // .defaultToSpeaker, // 默认使用扬声器输出 // .mixWithOthers // ], // force: force // 强制重新配置,确保设置生效 // ) // // 设置首选输入设备 // try configurePreferredInput() // NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话") // return true // } catch { // NSLog("[AudioSessionManager] 配置语音交互(ASR+TTS)的音频会话失败: \(error.localizedDescription)") // return false // } // } }