如何将Float数组转换为AVAudioPCMBuffer?修警告后输出音频异常
问题背景
我持有一个存储音频采样数据的Float类型数组,需要将其转换为AVAudioPCMBuffer实例,传入AVAudioFile的write(from:)方法完成音频文件写入。
初始实现与触发警告
最初参考公开代码片段实现如下:
var floats: [Float] = ... // 外部传入的音频采样数据 let audioBuffer = AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(floats.count * MemoryLayout<Float>.size), mData: &floats) var bufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer) let outputAudioBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, bufferListNoCopy: &bufferList)! try self.renderedAudioFile?.write(from: outputAudioBuffer)
该实现可以正常输出符合预期的音频,但在Xcode 13.4.1版本中,&floats位置会抛出指针生命周期警告:
Cannot use inout expression here; argument 'mData' must be a pointer that outlives the call to 'init(mNumberChannels:mDataByteSize:mData:)'
错误改写导致的乱码问题
为消除上述警告,我尝试通过withUnsafeMutablePointer限定指针作用域改写代码:
var floats: [Float] = ... // 外部传入的音频采样数据 try withUnsafeMutablePointer(to: &floats) { bytes in let audioBuffer = AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(bytes.pointee.count * MemoryLayout<Float>.size), mData: bytes) var bufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer) let outputAudioBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, bufferListNoCopy: &bufferList)! try self.renderedAudioFile?.write(from: outputAudioBuffer) }
改写后警告消失,但输出的音频为完全无效的乱码。已确认floats.count与bytes.pointee.count数值完全一致,无法定位问题原因。
问题根因
改写后的代码用错了指针获取方法:withUnsafeMutablePointer(to: &floats)返回的是指向数组结构体本身的指针(类型为UnsafeMutablePointer<[Float]>),并非指向数组底层连续存储的Float采样数据的指针,将该指针传给mData参数后,音频系统读取的是完全错误的内存地址,自然会输出乱码。
正确实现方案
方案1:拷贝数据初始化(推荐,无生命周期风险)
直接初始化对应容量的AVAudioPCMBuffer,将数组内的采样数据拷贝到buffer的通道内存中,不需要手动管理指针生命周期,稳定性最高:
var floats: [Float] = ... // 外部传入的音频采样数据 // 替换为你实际的音频参数:采样率、声道数必须和原始采样数据匹配 guard let audioFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1) else { // 格式初始化失败处理 return } guard let frameCount = AVAudioFrameCount(exactly: floats.count), let outputBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: frameCount) else { // buffer初始化失败处理 return } outputBuffer.frameLength = frameCount // 将采样数据拷贝到buffer的浮点通道 if let channelData = outputBuffer.floatChannelData { floats.withUnsafeMutableBufferPointer { floatPtr in channelData[0].assign(from: floatPtr.baseAddress!, count: Int(frameCount)) } } // 写入音频文件 try self.renderedAudioFile?.write(from: outputBuffer)
方案2:零拷贝初始化(性能更高,需注意指针生命周期)
如果需要避免内存拷贝,可以使用数组的withUnsafeMutableBufferPointer方法直接拿到元素存储区的指针,在闭包作用域内完成buffer初始化和写入操作,保证指针在使用期间全程有效:
var floats: [Float] = ... // 外部传入的音频采样数据 // 替换为你实际的音频参数 guard let audioFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1) else { return } try floats.withUnsafeMutableBufferPointer { floatPtr in let audioBuffer = AudioBuffer( mNumberChannels: 1, mDataByteSize: UInt32(floatPtr.count * MemoryLayout<Float>.size), mData: floatPtr.baseAddress ) var bufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer) guard let outputAudioBuffer = AVAudioPCMBuffer( pcmFormat: audioFormat, bufferListNoCopy: &bufferList ) else { return } // 必须手动设置帧长度,否则buffer默认长度为0 outputAudioBuffer.frameLength = AVAudioFrameCount(floatPtr.count) try self.renderedAudioFile?.write(from: outputAudioBuffer) }
关键注意事项
- 不要使用
withUnsafeMutablePointer(to: &array)获取数组元素指针,该方法仅能拿到数组结构体本身的指针,无法访问元素连续存储区 - 无论用哪种方式初始化
AVAudioPCMBuffer,都需要显式设置frameLength为实际采样帧数,否则会出现写入长度异常 AVAudioFormat的参数(采样率、声道数、存储格式)必须和原始Float采样数据完全匹配,否则会出现播放速率异常、音高错误、内容乱码等问题
内容的提问来源于stack exchange,提问作者Robert Atkins

