Swift AVFoundation AudioEngine模拟器切换AirPods时崩溃问题排查
问题:切换AirPods时音频录制程序崩溃,报错
required condition is false: format.sampleRate == hwFormat.sampleRate 我正在开发一款通过WebSocket传输数据的类Siri应用,但在模拟器中切换使用/不使用AirPods时,程序会崩溃,报错信息如下:
Thread 1: "required condition is false: format.sampleRate == hwFormat.sampleRate"
我尝试调整commonFormat(如将.pcmFormatFloat32改为其他格式)以及总线设置,但仍未找到崩溃的根本原因,希望能得到排查思路。
实现代码
final class AudioRecorder: Recorder, ObservableObject { enum RecordingState { case recording, paused, stopped } private var engine: AVAudioEngine! private var mixerNode: AVAudioMixerNode! @Published private(set) var state: RecordingState = .stopped private let audioBus = 0 private let bufferSize: AVAudioFrameCount = 1024 private let sampleRate = 44100.0 private var audioBuffers = [AVAudioPCMBuffer]() @Published private(set) var combinedBuffer: AudioRecordResult? func createEngine() { setupSession() setupEngine() } fileprivate func setupSession() { let session = AVAudioSession.sharedInstance() try? session.setCategory(.record) try? session.setActive(true, options: .notifyOthersOnDeactivation) } fileprivate func setupEngine() { engine = AVAudioEngine() mixerNode = AVAudioMixerNode() mixerNode.volume = 0 engine.attach(mixerNode) makeConnections() engine.prepare() } fileprivate func makeConnections() { let inputNode = engine.inputNode let inputFormat: AVAudioFormat = inputNode.outputFormat(forBus: 0) engine.connect(inputNode, to: mixerNode, format: inputFormat) let mainMixerNode = engine.mainMixerNode let mixerFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: inputFormat.sampleRate, channels: 1, interleaved: false) engine.connect(mixerNode, to: mainMixerNode, format: mixerFormat) } func startRecording() throws { let tapNode: AVAudioNode = mixerNode let format = tapNode.outputFormat(forBus: 0) let startTime = DispatchTime.now() DispatchQueue.global(qos: .background).async { tapNode.installTap(onBus: 0, bufferSize: 4096, format: format, block: { [weak self] (buffer, time) in guard let self = self else { return } self.audioBuffers.append(buffer) let currentTime = DispatchTime.now() let elapsedTime = currentTime.uptimeNanoseconds - startTime.uptimeNanoseconds let isFinal = elapsedTime > 5 * 1_000_000_000 self.combineBuffers(isFinal: isFinal) if isFinal { stopRecording() } }) } try engine.start() state = .recording } private func combineBuffers(isFinal: Bool) { guard !audioBuffers.isEmpty else { return } let format = audioBuffers[0].format let totalFrameLength = audioBuffers.reduce(0) { $0 + $1.frameLength } guard let combinedBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: totalFrameLength) else { return } combinedBuffer.frameLength = totalFrameLength var frameOffset: AVAudioFrameCount = 0 for buffer in audioBuffers { for channel in 0..<Int(buffer.format.channelCount) { let src = buffer.floatChannelData![channel] let dst = combinedBuffer.floatChannelData![channel] + Int(frameOffset) memcpy(dst, src, Int(buffer.frameLength) * MemoryLayout<Float>.size) } frameOffset += buffer.frameLength } self.combinedBuffer = .success(buffer: combinedBuffer, final: isFinal) } func stopRecording() { mixerNode.removeTap(onBus: 0) engine.stop() if case .success(let buffer, _) = self.combinedBuffer { self.combinedBuffer = .success(buffer: buffer, final: true) } self.audioBuffers = [] state = .stopped } }
排查思路与修复方案
核心原因
切换AirPods时,音频硬件设备发生变更,AVAudioEngine的输入格式(采样率、通道数等)会随之改变,但当前代码未处理设备变更事件,原有的引擎连接和Tap仍绑定旧硬件格式,新旧格式不匹配触发断言崩溃。
具体修复步骤
- 监听音频路由变更:注册
AVAudioSession.routeChangeNotification通知,在设备切换时销毁并重建音频引擎 - 避免缓存旧格式:每次启动录制时,获取节点最新的输出格式,不要提前固定格式
- 严格清理资源:设备变更时先停止引擎、移除Tap,再重新初始化引擎和连接
修改后的关键代码
- 添加路由变更监听
init() { NotificationCenter.default.addObserver( self, selector: #selector(handleAudioRouteChange), name: AVAudioSession.routeChangeNotification, object: nil ) } @objc private func handleAudioRouteChange(notification: Notification) { if state == .recording { stopRecording() } // 销毁旧引擎资源 engine = nil mixerNode = nil // 重建引擎 createEngine() }
- 完善会话设置的错误处理
fileprivate func setupSession() { let session = AVAudioSession.sharedInstance() do { try session.setCategory(.record) try session.setActive(true, options: .notifyOthersOnDeactivation) } catch { print("音频会话配置失败: \(error.localizedDescription)") } }
- 修正录制时的格式获取与Tap安装
func startRecording() throws { guard let tapNode = mixerNode else { return } // 每次启动都获取最新格式,避免使用旧设备的格式 let format = tapNode.outputFormat(forBus: 0) let startTime = DispatchTime.now() DispatchQueue.global(qos: .background).async { tapNode.installTap(onBus: 0, bufferSize: 4096, format: format, block: { [weak self] (buffer, time) in guard let self = self else { return } self.audioBuffers.append(buffer) let currentTime = DispatchTime.now() let elapsedTime = currentTime.uptimeNanoseconds - startTime.uptimeNanoseconds let isFinal = elapsedTime > 5 * 1_000_000_000 self.combineBuffers(isFinal: isFinal) if isFinal { self.stopRecording() } }) } try engine.start() state = .recording }
额外注意事项
- 模拟器的音频设备切换逻辑和真机存在差异,修复后必须在真机上验证
- 不要忽略
AVAudioSession的错误返回,这些信息能快速定位权限或配置问题 - 录制过程中切换设备时,建议给用户显示状态提示,提升体验
内容的提问来源于stack exchange,提问作者Farhandika
相关产品推荐
相关产品推荐

