Error -50 writing to AVAudioFile

I'm trying to write 16-bit interleaved 2-channel data captured from a LiveSwitch audio source to a AVAudioFile. The buffer and file formats match but I get a bad parameter error from the API. Does this API not support the specified format or is there some other issue?

Here is the debugger output.

(lldb) po audioFile.url
▿ file:///private/var/mobile/Containers/Data/Application/1EB14379-0CF2-41B6-B742-4C9A80728DB3/tmp/Heart%20Sounds%201
  - _url : file:///private/var/mobile/Containers/Data/Application/1EB14379-0CF2-41B6-B742-4C9A80728DB3/tmp/Heart%20Sounds%201
  - _parseInfo : nil
  - _baseParseInfo : nil
(lldb) po error
Error Domain=com.apple.coreaudio.avfaudio Code=-50 "(null)" UserInfo={failed call=ExtAudioFileWrite(_impl->_extAudioFile, buffer.frameLength, buffer.audioBufferList)}
(lldb) po buffer.format
<AVAudioFormat 0x302a12b20:  2 ch,  44100 Hz, Int16, interleaved>
(lldb) po audioFile.fileFormat
<AVAudioFormat 0x302a515e0:  2 ch,  44100 Hz, Int16, interleaved>
(lldb) po buffer.frameLength
882
(lldb) po buffer.audioBufferList
▿ 0x0000000300941e60
  - pointerValue : 12894608992

This code handles the details of converting the Live Switch frame into an AVAudioPCMBuffer.

extension FMLiveSwitchAudioFrame {
    func convertedToPCMBuffer() -> AVAudioPCMBuffer {
        Self.convertToAVAudioPCMBuffer(from: self)!
    }

    static func convertToAVAudioPCMBuffer(from frame: FMLiveSwitchAudioFrame) -> AVAudioPCMBuffer? {
        // Retrieve the audio buffer and format details from the FMLiveSwitchAudioFrame
        guard
            let buffer = frame.buffer(),
            let format = buffer.format() as? FMLiveSwitchAudioFormat else { return nil }

        // Extract PCM format details from FMLiveSwitchAudioFormat
        let sampleRate = Double(format.clockRate())
        let channelCount = AVAudioChannelCount(format.channelCount())

        // Determine bytes per sample based on bit depth
        let bitsPerSample = 16
        let bytesPerSample = bitsPerSample / 8
        let bytesPerFrame = bytesPerSample * Int(channelCount)
        let frameLength = AVAudioFrameCount(Int(buffer.dataBuffer().length()) / bytesPerFrame)

        // Create an AVAudioFormat from the FMLiveSwitchAudioFormat
        guard let avAudioFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: sampleRate, channels: channelCount, interleaved: true) else {
            return nil
        }

        // Create an AudioBufferList to wrap the existing buffer
        let audioBufferList = UnsafeMutablePointer<AudioBufferList>.allocate(capacity: 1)
        audioBufferList.pointee.mNumberBuffers = 1
        audioBufferList.pointee.mBuffers.mNumberChannels = channelCount
        audioBufferList.pointee.mBuffers.mDataByteSize = UInt32(buffer.dataBuffer().length())
        audioBufferList.pointee.mBuffers.mData = buffer.dataBuffer().data().mutableBytes // Directly use LiveSwitch buffer

        // Transfer ownership of the buffer to AVAudioPCMBuffer
        let pcmBuffer = AVAudioPCMBuffer(pcmFormat: avAudioFormat, bufferListNoCopy: audioBufferList) /* { buffer in
            // Ensure the buffer is freed when AVAudioPCMBuffer is deallocated
            buffer.deallocate() // Only call this if LiveSwitch allows manual deallocation
        } */

        pcmBuffer?.frameLength = frameLength
        return pcmBuffer
    }
}

This is the handler that is invoked with every frame in order to convert it for use with AVAudioFile and optionally update a scrolling signal display on the screen.

    private func onRaisedFrame(obj: Any!) -> Void {
        // Bail out early if no one is interested in the data.
        guard isMonitoring else { return }

        // Convert LS frame to AVAudioPCMBuffer (no-copy)
        let frame = obj as! FMLiveSwitchAudioFrame
        let buffer = frame.convertedToPCMBuffer()

        // Hand subscribers a reference to the buffer for rendering to display.
        bufferPublisher?.send(buffer)

        // If we have and output file, store the data there, as well.
        guard let audioFile = self.audioFile else { return }
        do {
            try audioFile.write(from: buffer) // FIXME: This call is throwing error -50
        } catch {
            FMLiveSwitchLog.error(withMessage: "Failed to write buffer to audio file at \(audioFile.url): \(error)")
            self.audioFile = nil
        }
    }

This is how the audio file is being setup.

static var recordingFormat: AVAudioFormat = {
        AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 44_100, channels: 2, interleaved: true)!
    }()

let audioFile = try AVAudioFile(forWriting: outputURL, settings: Self.recordingFormat.settings)
Answered by statemachinejunkie in 890482022

I've been told that the internal processing format of AVFoundation is always 32-bit float and that the buffers I'm handing to the write(from:) function must be in that format. Using a settings dictionary, Apple will automatically convert the 32-bit floats to the format in the settings dict.

let settings: [String: Any] = [
        AVFormatIDKey: kAudioFormatLinearPCM,
        AVSampleRateKey: sampleRate,
        AVNumberOfChannelsKey: channels,
        AVLinearPCMBitDepthKey: 16,
        AVLinearPCMIsFloatKey: false,
        AVLinearPCMIsBigEndianKey: false,
        AVLinearPCMIsNonInterleaved: false
    ]

    let audioFile = try AVAudioFile(forWriting: url, settings: settings)
    let format = audioFile.processingFormat

I never pasted the code I use to create the audio file but the settings dictionary is supposed to guarantee the conversion I need so that 16-bit samples are indeed written to the disk. I've not tried this because I moved beyond this issue months ago by bypassing AVFoundation and writing the data directly to disk myself. The alternative seems to be to convert from the third-party format of Int16 interleaved buffers to Float32 non-interleaved buffers and then back again when writing to disk. If you need to guarantee a lossless solution, I don't think this is it, though Apple may disagree with me. If anyone wants to correct my thinking or perhaps contradict some bad information reflected in the post, please do.

Accepted Answer

I've been told that the internal processing format of AVFoundation is always 32-bit float and that the buffers I'm handing to the write(from:) function must be in that format. Using a settings dictionary, Apple will automatically convert the 32-bit floats to the format in the settings dict.

let settings: [String: Any] = [
        AVFormatIDKey: kAudioFormatLinearPCM,
        AVSampleRateKey: sampleRate,
        AVNumberOfChannelsKey: channels,
        AVLinearPCMBitDepthKey: 16,
        AVLinearPCMIsFloatKey: false,
        AVLinearPCMIsBigEndianKey: false,
        AVLinearPCMIsNonInterleaved: false
    ]

    let audioFile = try AVAudioFile(forWriting: url, settings: settings)
    let format = audioFile.processingFormat

I never pasted the code I use to create the audio file but the settings dictionary is supposed to guarantee the conversion I need so that 16-bit samples are indeed written to the disk. I've not tried this because I moved beyond this issue months ago by bypassing AVFoundation and writing the data directly to disk myself. The alternative seems to be to convert from the third-party format of Int16 interleaved buffers to Float32 non-interleaved buffers and then back again when writing to disk. If you need to guarantee a lossless solution, I don't think this is it, though Apple may disagree with me. If anyone wants to correct my thinking or perhaps contradict some bad information reflected in the post, please do.

Error -50 writing to AVAudioFile
 
 
Q