AudioToolbox录音报错求助:kAudioFileNotOpenError等问题排查
我来帮你梳理下这些AudioToolbox的录音错误,咱们一个个拆解问题、找原因,再给出针对性的修复方案:
核心错误分析与修复
1. kAudioFileNotOpenError (-38) 错误(占比50%)
这个错误说白了就是你用来写文件的AudioFileID失效了,结合你的代码来看,主要有两个问题:
- 每次调用
record(atURL:)启动新录音时,旧的AudioFileID没有被关闭就被新的覆盖了,而音频回调线程还在拿着旧的失效ID写数据,自然触发-38错误; - 音频回调是在非主线程跑的,而
record(atURL:)可能在主线程调用,audioFile和startingByte这两个变量没有线程同步保护,多线程读写时很容易出现竞态,导致写文件时audioFile已经变成nil或者无效实例。
修复要点:
- 每次启动新录音前,必须调用
AudioFileClose关闭旧的文件实例,释放资源; - 用串行队列把
audioFile和startingByte的读写操作都包起来,避免多线程冲突。
2. kAudioUnitErr_CannotDoInCurrentContext (-10863) 错误(占比5%)
这个错误通常是AudioUnitRender调用时机不对——要么RemoteIO还没完全初始化好,要么它处于暂停/不可用状态。另外你之前让系统自动分配缓冲区(mData = nil),偶尔也会因为系统资源紧张导致这个错误。
修复要点:
- 确保
prepareAudioToolbox()完全成功后再启动录音; - 如果遇到-10863错误,直接跳过当前帧返回
noErr,别让整个录音流程中断; - 手动分配缓冲区内存,减少系统自动分配带来的不确定性。
3. kAudioFileDoesNotAllow64BitDataSizeError (1868981823) 错误(占比4%)
WAV格式默认是32位的文件大小字段,理论上只有当文件超过4GB才会触发这个错误,但你录音过程中很快就出现,说明是startingByte被意外篡改了——大概率是多线程竞态导致的数值异常。
修复要点:
- 用串行队列保护
startingByte的读写; - 如果需要支持大文件,换成CAF格式(
kAudioFileCAFType),它原生支持64位文件大小。
4. AudioUnitRender返回-1错误(占比3%)
这个是通用错误,基本是前面问题的衍生问题——比如RemoteIO状态异常、缓冲区设置错误等,把前面的问题修复后,这个错误大概率会消失。
修改后的完整代码
我给你调整了代码,加上了线程同步、资源清理、错误处理这些关键点:
import AudioToolbox class Recorder { static let shared = Recorder() private static let sampleRate: Float64 = 16000 var processAudioData: ((Data) -> ())? fileprivate var remoteIOUnit: AudioComponentInstance? private var audioFile: AudioFileID? private var startingByte: Int64 = 0 // 用串行队列处理音频相关操作,避免多线程竞态 private let audioQueue = DispatchQueue(label: "com.yourapp.recorder.audioQueue") // 录音参数设置 private let formatId: AudioFormatID = kAudioFormatLinearPCM private let bitsPerChannel: UInt32 = 16 private let channelsPerFrame: UInt32 = 1 private let bytesPerFrame: UInt32 = 2 private let framesPerPacket: UInt32 = 1 private let formatFlags: AudioFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked func record(atURL url: URL) { audioQueue.sync { // 先清理旧的录音资源 self.cleanupExistingResources() // 打开新文件 let fileStatus = self.openFileForWriting(fileURL: url) guard fileStatus == noErr else { print("打开录音文件失败:\(fileStatus)") return } self.startingByte = 0 // 准备RemoteIO let prepareStatus = self.prepareAudioToolbox() guard prepareStatus == noErr else { print("初始化AudioToolbox失败:\(prepareStatus)") return } // 开始录音 let startStatus = self.startAudioToolboxRecording() if startStatus != noErr { print("启动录音失败:\(startStatus)") } } } func stopRecording() { audioQueue.sync { self.cleanupExistingResources() } } private func cleanupExistingResources() { // 停止并释放RemoteIO if let remoteIOUnit = self.remoteIOUnit { AudioOutputUnitStop(remoteIOUnit) AudioUnitUninitialize(remoteIOUnit) AudioComponentInstanceDispose(remoteIOUnit) self.remoteIOUnit = nil } // 关闭音频文件 if let audioFile = self.audioFile { AudioFileClose(audioFile) self.audioFile = nil } self.startingByte = 0 } private func openFileForWriting(fileURL: URL) -> OSStatus { var asbd = AudioStreamBasicDescription() memset(&asbd, 0, MemoryLayout<AudioStreamBasicDescription>.size) asbd.mSampleRate = Recorder.sampleRate asbd.mFormatID = formatId asbd.mFormatFlags = formatFlags asbd.mBitsPerChannel = bitsPerChannel asbd.mChannelsPerFrame = channelsPerFrame asbd.mFramesPerPacket = framesPerPacket asbd.mBytesPerFrame = bytesPerFrame asbd.mBytesPerPacket = framesPerPacket * bytesPerFrame var audioFile: AudioFileID? let audioErr = AudioFileCreateWithURL(fileURL as CFURL, AudioFileTypeID(kAudioFileWAVEType), &asbd, .eraseFile, &audioFile) if audioErr == noErr { self.audioFile = audioFile } return audioErr } private func prepareAudioToolbox() -> OSStatus { var status = noErr // 查找RemoteIO组件 var audioComponentDescription = AudioComponentDescription() audioComponentDescription.componentType = kAudioUnitType_Output audioComponentDescription.componentSubType = kAudioUnitSubType_RemoteIO audioComponentDescription.componentManufacturer = kAudioUnitManufacturer_Apple audioComponentDescription.componentFlags = 0 audioComponentDescription.componentFlagsMask = 0 guard let remoteIOComponent = AudioComponentFindNext(nil, &audioComponentDescription) else { return -1 } var ioUnit: AudioComponentInstance? status = AudioComponentInstanceNew(remoteIOComponent, &ioUnit) guard status == noErr, let remoteIOUnit = ioUnit else { return status } self.remoteIOUnit = remoteIOUnit // 启用输入总线(麦克风) let bus1: AudioUnitElement = 1 var oneFlag: UInt32 = 1 status = AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, bus1, &oneFlag, UInt32(MemoryLayout<UInt32>.size)); guard status == noErr else { return status } // 设置输入格式 var asbd = AudioStreamBasicDescription() asbd.mSampleRate = Recorder.sampleRate asbd.mFormatID = formatId asbd.mFormatFlags = formatFlags asbd.mBitsPerChannel = bitsPerChannel asbd.mChannelsPerFrame = channelsPerFrame asbd.mFramesPerPacket = framesPerPacket asbd.mBytesPerFrame = bytesPerFrame asbd.mBytesPerPacket = framesPerPacket * bytesPerFrame status = AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, bus1, &asbd, UInt32(MemoryLayout<AudioStreamBasicDescription>.size)) guard status == noErr else { return status } // 设置录音回调 var callbackStruct = AURenderCallbackStruct() callbackStruct.inputProc = recordingCallback callbackStruct.inputProcRefCon = Unmanaged.passUnretained(self).toOpaque() status = AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, bus1, &callbackStruct, UInt32(MemoryLayout<AURenderCallbackStruct>.size)); guard status == noErr else { return status } // 初始化RemoteIO return AudioUnitInitialize(remoteIOUnit) } private func startAudioToolboxRecording() -> OSStatus { guard let remoteIOUnit = remoteIOUnit else { return 656783 } return AudioOutputUnitStart(remoteIOUnit) } private func writeDataToFile(audioBuffers: UnsafeMutableBufferPointer<AudioBuffer>) -> OSStatus { return audioQueue.sync { guard let audioFile = self.audioFile else { return 176136 } var currentByte = self.startingByte var status = noErr for audioBuffer in audioBuffers { var numBytes = audioBuffer.mDataByteSize guard let mData = audioBuffer.mData else { continue } // 写入音频数据 let audioErr = AudioFileWriteBytes(audioFile, false, currentByte, &numBytes, mData) if audioErr != noErr { status = audioErr break } let data = Data(bytes: mData, count: Int(numBytes)) // 把数据传递出去,注意如果有耗时操作要异步处理 DispatchQueue.global().async { self.processAudioData?(data) } currentByte += Int64(numBytes) } if status == noErr { self.startingByte = currentByte } return status } } } private func recordingCallback( inRefCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>, inTimeStamp: UnsafePointer<AudioTimeStamp>, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer<AudioBufferList>? ) -> OSStatus { let recorder = Unmanaged<Recorder>.fromOpaque(inRefCon).takeUnretainedValue() guard let remoteIOUnit = recorder.remoteIOUnit else { return 656783 } var status = noErr let channelCount: UInt32 = 1 var bufferList = AudioBufferList() bufferList.mNumberBuffers = channelCount let buffers = UnsafeMutableBufferPointer<AudioBuffer>(start: &bufferList.mBuffers, count: Int(bufferList.mNumberBuffers)) // 手动分配缓冲区,避免系统自动分配的潜在问题 let bufferSize = inNumberFrames * 2 buffers[0].mNumberChannels = 1 buffers[0].mDataByteSize = bufferSize buffers[0].mData = malloc(Int(bufferSize)) defer { // 记得释放内存 free(buffers[0].mData) } // 尝试获取录音数据,处理-10863错误 status = AudioUnitRender(remoteIOUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, UnsafeMutablePointer<AudioBufferList>(&bufferList)) if status == kAudioUnitErr_CannotDoInCurrentContext { // 跳过当前帧,返回noErr避免录音中断 return noErr } guard status == noErr else { return status } status = recorder.writeDataToFile(audioBuffers: buffers) return status }
额外提醒
- 一定要调用
stopRecording()来清理资源,不然会内存泄漏,还会留下无效的AudioUnit/AudioFile实例; - 如果需要录制长时间音频,建议换成CAF格式(把
kAudioFileWAVEType改成kAudioFileCAFType),它没有WAV的4GB大小限制; - 所有AudioToolbox的API调用都要检查返回状态,加上详细的日志,方便后续排查问题;
processAudioData如果有耗时操作(比如网络上传、复杂处理),一定要放到异步队列里,别阻塞音频回调线程——这个线程优先级很高,阻塞它会导致录音卡顿甚至系统崩溃。
内容的提问来源于stack exchange,提问作者derpoliuk
相关产品推荐
相关产品推荐

