swiftavfoundationavaudioengineavaudiopcmbuffer

How can mp3 data in memory be loaded into an AVAudioPCMBuffer in Swift?


I have a class method to read an mp3 file into an AVAudioPCMBuffer as follows:

private(set) var fullAudio: AVAudioPCMBuffer?

func initAudio(audioFileURL: URL) -> Bool {
    var status = true
    
    do {
        let audioFile = try AVAudioFile(forReading: audioFileURL)
        let audioFormat = audioFile.processingFormat
        let audioFrameLength = UInt32(audioFile.length)

        fullAudio = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameLength)

        if let fullAudio = fullAudio {
            try audioFile.read(into: fullAudio)

            // processing of full audio
        }
    } catch {
        status = false
    }
    
    return status
}

However, I now need to be able to read the same mp3 info from memory (rather than a file) into the AVAudioPCMBuffer without using the file system, where the info is held in the Data type, for example using a function declaration of the form

func initAudio(audioFileData: Data) -> Bool {
    // some code setting up fullAudio
}

How can this be done? I've looked to see whether there is a route from Data holding mp3 info to AVAudioPCMBuffer (e.g. via AVAudioBuffer or AVAudioCompressedBuffer), but haven't seen a way forward.


Solution

  • I went down the rabbit hole on this one. Here is what probably amounts to a Rube Goldberg-esque solution:

    A lot of the pain comes from using C from Swift.

    func data_AudioFile_ReadProc(_ inClientData: UnsafeMutableRawPointer, _ inPosition: Int64, _ requestCount: UInt32, _ buffer: UnsafeMutableRawPointer, _ actualCount: UnsafeMutablePointer<UInt32>) -> OSStatus {
        let data = inClientData.assumingMemoryBound(to: Data.self).pointee
        let bufferPointer = UnsafeMutableRawBufferPointer(start: buffer, count: Int(requestCount))
        let copied = data.copyBytes(to: bufferPointer, from: Int(inPosition) ..< Int(inPosition) + Int(requestCount))
        actualCount.pointee = UInt32(copied)
        return noErr
    }
    
    func data_AudioFile_GetSizeProc(_ inClientData: UnsafeMutableRawPointer) -> Int64 {
        let data = inClientData.assumingMemoryBound(to: Data.self).pointee
        return Int64(data.count)
    }
    
    extension Data {
        func convertedTo(_ format: AVAudioFormat) -> AVAudioPCMBuffer? {
            var data = self
    
            var af: AudioFileID? = nil
            var status = AudioFileOpenWithCallbacks(&data, data_AudioFile_ReadProc, nil, data_AudioFile_GetSizeProc(_:), nil, 0, &af)
            guard status == noErr, af != nil else {
                return nil
            }
    
            defer {
                AudioFileClose(af!)
            }
    
            var eaf: ExtAudioFileRef? = nil
            status = ExtAudioFileWrapAudioFileID(af!, false, &eaf)
            guard status == noErr, eaf != nil else {
                return nil
            }
    
            defer {
                ExtAudioFileDispose(eaf!)
            }
    
            var clientFormat = format.streamDescription.pointee
            status = ExtAudioFileSetProperty(eaf!, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout.size(ofValue: clientFormat)), &clientFormat)
            guard status == noErr else {
                return nil
            }
    
            if let channelLayout = format.channelLayout {
                var clientChannelLayout = channelLayout.layout.pointee
                status = ExtAudioFileSetProperty(eaf!, kExtAudioFileProperty_ClientChannelLayout, UInt32(MemoryLayout.size(ofValue: clientChannelLayout)), &clientChannelLayout)
                guard status == noErr else {
                    return nil
                }
            }
    
            var frameLength: Int64 = 0
            var propertySize: UInt32 = UInt32(MemoryLayout.size(ofValue: frameLength))
            status = ExtAudioFileGetProperty(eaf!, kExtAudioFileProperty_FileLengthFrames, &propertySize, &frameLength)
            guard status == noErr else {
                return nil
            }
    
            guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(frameLength)) else {
                return nil
            }
    
            let bufferSizeFrames = 512
            let bufferSizeBytes = Int(format.streamDescription.pointee.mBytesPerFrame) * bufferSizeFrames
            let numBuffers = format.isInterleaved ? 1 : Int(format.channelCount)
            let numInterleavedChannels = format.isInterleaved ? Int(format.channelCount) : 1
            let audioBufferList = AudioBufferList.allocate(maximumBuffers: numBuffers)
            for i in 0 ..< numBuffers {
                audioBufferList[i] = AudioBuffer(mNumberChannels: UInt32(numInterleavedChannels), mDataByteSize: UInt32(bufferSizeBytes), mData: malloc(bufferSizeBytes))
            }
    
            defer {
                for buffer in audioBufferList {
                    free(buffer.mData)
                }
                free(audioBufferList.unsafeMutablePointer)
            }
    
            while true {
                var frameCount: UInt32 = UInt32(bufferSizeFrames)
                status = ExtAudioFileRead(eaf!, &frameCount, audioBufferList.unsafeMutablePointer)
                guard status == noErr else {
                    return nil
                }
    
                if frameCount == 0 {
                    break
                }
    
                let src = audioBufferList
                let dst = UnsafeMutableAudioBufferListPointer(pcmBuffer.mutableAudioBufferList)
    
                if src.count != dst.count {
                    return nil
                }
    
                for i in 0 ..< src.count {
                    let srcBuf = src[i]
                    let dstBuf = dst[i]
                    memcpy(dstBuf.mData?.advanced(by: Int(dstBuf.mDataByteSize)), srcBuf.mData, Int(srcBuf.mDataByteSize))
                }
    
                pcmBuffer.frameLength += frameCount
            }
    
            return pcmBuffer
        }
    }
    

    A more robust solution would probably read the sample rate and channel count and give the option to preserve them.

    Tested using:

    let url = URL(fileURLWithPath: "/tmp/test.mp3")
    let data = try! Data(contentsOf: url)
    
    let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 1, interleaved: false)!
    if let d = data.convertedTo(format) {
        let avf = try! AVAudioFile(forWriting: URL(fileURLWithPath: "/tmp/foo.wav"), settings: format.settings, commonFormat: format.commonFormat, interleaved: format.isInterleaved)
        try! avf.write(from: d)
    }