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

Swift AVAudioFile.read(into:) 真机报Foundation._GenericObjCError error 0

问题说明

开发通过录制西瓜敲击声预测西瓜甜度的项目时,使用AVAudioRecorder录制并保存音频,读取已保存的音频文件时触发错误:

Foundation._GenericObjCError error 0

该错误仅在真机iPhone Xr测试时触发,Xcode模拟器运行无法复现,错误触发位置为readAudioIntoFloats函数内的file.read(into:buf)调用处。

相关业务代码

录音与音频列表获取逻辑

private func startRecording() {
        do {
            
            if self.record{
                // recording already started
                
                recorder.stop()
                self.record.toggle()
                // updating data for every recording
                self.getAudios()
                return
            }
            // record audio
            
            // store audio in document directory
//            var highPassFilter = AVAudioUnitEQFilterType.highPass
//            var lowPassFilter = AVAudioUnitEQFilterType.lowPass
            
            let fileURL: URL = {
                        let src = "thump.wav"
                        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(src)
                   }()
            
//            let fileURL : URL = {
//                return Bundle.main.url(forResource: "thump", withExtension: "wav")
//            }()!
            
            let recordSettings: [String : Any] = [AVFormatIDKey: Int(kAudioFormatLinearPCM),
                                                  AVSampleRateKey: 44100.0,
                                                  AVNumberOfChannelsKey: 1,
                                                  AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ]
            
            self.recorder = try AVAudioRecorder(url: fileURL, settings: recordSettings)
            print(fileURL)
            self.recorder.record()
            print("Recording started")
            self.record.toggle()
        } catch {
            print(error.localizedDescription)
        }
    } 

func getAudios() {
        
        do {
            
            let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            
            // Fetch all data from document directory
            
            let result = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .producesRelativePathURLs)
            
            self.audios.removeAll()
            
            for i in result {
                
                self.audios.append(i)
                
            }
            
        }
        catch {
            print(error.localizedDescription)
        }
        
    } 

音频读取逻辑

func readAudioIntoFloats(fname: String, ext: String) -> [Float] {
            let interleaved: Bool = false
            
            let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let fileName = url.appendingPathComponent("\(fname).\(ext)")
//            let fileName = Bundle.main.url(forResource: "thump", withExtension: "wav")
            print(fileName)
            let file = try! AVAudioFile(forReading: fileName)
            let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: file.fileFormat.sampleRate, channels: file.fileFormat.channelCount, interleaved: interleaved)!

            let buf = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 4096*24)!
            do {
                try file.read(into: buf)
            } catch {
                print(error.localizedDescription)
            }
            
            var floatArray: [Float] = []

            if (Int(file.fileFormat.channelCount) == 2) {
                let leftArray = Array(UnsafeBufferPointer(start: buf.floatChannelData?[0], count:Int(buf.frameLength)))
                let rightArray = Array(UnsafeBufferPointer(start: buf.floatChannelData?[1], count:Int(buf.frameLength)))
                
                for i in 0..<leftArray.count {
                    floatArray.append((leftArray[i]+rightArray[i])/Float(2))
                }
            } else {
                
                floatArray = Array(UnsafeBufferPointer(start: buf.floatChannelData?[0], count:Int(buf.frameLength)))
                
            }
            
            print(floatArray.count)
            return floatArray

        }
错误原因
  1. 录音停止后立即操作文件:调用recorder.stop()不会同步完成文件写入,模拟器运行在Mac端磁盘IO速度快,文件写入几乎无延迟不会触发问题;真机磁盘IO有延迟,stop调用后音频文件头、音频数据还未完全写入磁盘,此时直接读取文件会触发无明确提示的通用Foundation错误。
  2. 音频缓冲区大小写死:代码中固定给PCM缓冲区分配4096*24的帧容量,如果录制的音频总帧数超过这个值,读取时会发生缓冲区溢出触发错误;如果音频长度小于该值,真机上未初始化的缓冲区内存也可能导致读取失败。
  3. 固定文件名覆盖逻辑缺失:每次录音都写入固定文件名thump.wav,但写入前未删除已有旧文件,真机沙盒的文件权限校验比模拟器严格,旧文件未被正确覆盖时会触发权限类错误。
  4. 强制解包+错误信息不全:代码中用try!强制解包AVAudioFile初始化结果,且捕获错误时仅打印localizedDescription,无法拿到具体错误码定位根因。
修复方案
  • 遵守AVAudioRecorderDelegate协议,在audioRecorderDidFinishRecording(_:successfully:)代理回调中确认录音写入成功后,再调用getAudios()刷新列表、执行后续读取逻辑,不要在stop()调用后直接操作文件。
  • 创建PCM缓冲区时不要写死固定容量,先读取音频文件的总帧长度file.length,用该值作为缓冲区的frameCapacity,读取前显式设置缓冲区的frameLength和总帧数一致,避免溢出或脏数据问题。
  • 每次初始化AVAudioRecorder开始录音前,先检查目标路径是否存在旧文件,存在则先删除旧文件再写入,避免文件覆盖权限问题。
  • 替换所有强制解包的try!为do-catch逻辑,捕获错误时打印完整的error信息而非仅localizedDescription,方便真机调试定位问题。

关键修复代码

  1. 录音完成回调处理
// 初始化recorder后设置代理
self.recorder.delegate = self

// 实现代理方法
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
    DispatchQueue.main.async {
        self.record.toggle()
        if flag {
            self.getAudios()
        } else {
            print("录音文件写入失败")
        }
    }
}

// 原stop逻辑去掉直接调用getAudios的代码
if self.record{
    recorder.stop()
    return
}
  1. 录音前清理旧文件
// 创建AVAudioRecorder前新增
if FileManager.default.fileExists(atPath: fileURL.path) {
    try? FileManager.default.removeItem(at: fileURL)
}
  1. 音频读取缓冲区适配
// 替换原固定容量buffer创建逻辑
let totalFrames = AVAudioFrameCount(file.length)
guard let buf = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: totalFrames) else {
    return []
}
buf.frameLength = totalFrames
try file.read(into: buf)

内容的提问来源于stack exchange,提问作者Rafael Bustillo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 11:39:20