You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:53:22