3

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.

gimbal
  • 63
  • 8
  • AVAudioConverter: https://developer.apple.com/documentation/avfoundation/avaudioconverter should be able to handle the conversion – sbooth Nov 12 '20 at 12:03
  • Thanks for the hint. It looks like to use AVAudioConverter I have to convert Data to AVAudioConverterInputBlock via AVAudioCompressedBuffer. But how can this be done? I can see that I can pick up the format by initialising an AVAudioPlayer with Data and grabbing the format from the AVAudioPlayer, but creating AVAudioCompressedBuffer also needs packet descriptors and I can't see how to get those. – gimbal Nov 12 '20 at 22:23
  • You could also use AudioFileOpenWithCallbacks and then ExtAudioFileWrapAudioFileID. I believe under the hood AVAudioFile uses ExtAudioFile anyway. – sbooth Nov 12 '20 at 22:30

1 Answers1

2

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)
}
sbooth
  • 16,646
  • 2
  • 55
  • 81
  • Thank you. I had started looking at AudioFileOpenWithCallbacks and ExtAudioFileWrapAudioFileID as per your earlier comment and was discovering the difficulties of integrating Swift with C! So your solution will be very helpful and I will look at it more closely over the next day. – gimbal Nov 13 '20 at 22:11
  • I can confirm that it ran successfully on iOS 10.3.3 and iOS 14.0 with the following format argument: AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false). Certainly a non-trivial problem to solve. – gimbal Nov 14 '20 at 20:48