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

