AVAudioFile.write(from:) fails when buffer contains interleaved audio

Viewed 2078

I'm trying to write out an audio file after doing some processing, and am getting an error. I've reduced the error to this simple standalone case:

import Foundation
import AVFoundation

do {
    let inputFileURL = URL(fileURLWithPath: "/Users/andrewmadsen/Desktop/test.m4a")
    let file = try AVAudioFile(forReading: inputFileURL, commonFormat: .pcmFormatFloat32, interleaved: true)
    guard let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(file.length)) else {
        throw NSError()
    }
    buffer.frameLength = buffer.frameCapacity
    try file.read(into: buffer)

    let tempURL =
    URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent("com.openreelsoftware.AudioWriteTest")
        .appendingPathComponent(UUID().uuidString)
        .appendingPathExtension("caf")
    let fm = FileManager.default
    let dirURL = tempURL.deletingLastPathComponent()
    if !fm.fileExists(atPath: dirURL.path, isDirectory: nil) {
        try fm.createDirectory(at: dirURL, withIntermediateDirectories: true, attributes: nil)
    }

    var settings = buffer.format.settings
    settings[AVAudioFileTypeKey] = kAudioFileCAFType
    let tempFile = try AVAudioFile(forWriting: tempURL, settings: settings)
    try tempFile.write(from: buffer)

} catch {
    print(error)
}

When this code runs, the tempFile.write(from: buffer) call throws an error:

Error Domain=com.apple.coreaudio.avfaudio Code=-50 "(null)" UserInfo={failed call=ExtAudioFileWrite(_imp->_extAudioFile, buffer.frameLength, buffer.audioBufferList)}

test.m4a is a stereo, 44.1 KHz AAC file (from the iTunes store), though the failure occurs with other stereo files in other formats (AIFF and WAV) as well.

The code does not fail, and instead correctly saves the original audio out to a new file if I change the interleaved parameter to false when creating the original input AVAudioFile (file). However, in this case, the following message is logged to the console:

Audio files cannot be non-interleaved. Ignoring setting AVLinearPCMIsNonInterleaved YES.

It seems strange and confusing that writing a non-interleaved buffer works fine, despite a message saying that files must be interleaved, while writing an interleaved buffer fails. This is the opposite of what I expected.

I'm aware that reading a file using the plain AVAudioFile(forReading:) initializer without specifying a format defaults to using non-interleaved (ie. the "standard" AVAudioFormat at the file's actual sample rate and channel count). Does this mean that I really do have to convert interleaved audio to non-interleaved before trying to write it?

Notably, in the actual program where this problem came up, I'm doing something much more complex than simply reading a file in and writing it back out again, and I do need to handle interleaved audio. I have confirmed however that that original, more complex code is also failing only for interleaved stereo audio.

Is there something tricky I need to do to get AVAudioFile to write out a buffer containing interleaved PCM audio?

2 Answers

The mixup here is that there are TWO formats in play: the format of the output file, and the format of the buffers you will write (the processing format). The initializer AVAudioFile(forWriting: settings:) does not let you choose the processing format and defaults to de-interleaved, hence your error.

This opens the file for writing using the standard format (deinterleaved floating point).

You need to use the other initializer: AVAudioFile(forWriting:settings: commonFormat:interleaved:) whose last two arguments specify the processing format (the argument names could have been clearer about that tbh).

var settings: [String : Any] = [:]

settings[AVFormatIDKey] = kAudioFormatMPEG4AAC
settings[AVAudioFileTypeKey] = kAudioFileCAFType
settings[AVSampleRateKey] = buffer.format.sampleRate
settings[AVNumberOfChannelsKey] = 2
settings[AVLinearPCMIsFloatKey] = (buffer.format.commonFormat == .pcmFormatInt32)

let tempFile = try AVAudioFile(forWriting: tempURL, settings: settings, commonFormat: buffer.format.commonFormat, interleaved: buffer.format.isInterleaved)
try tempFile.write(from: buffer)

p.s. passing the buffer format setting directly to AVAudioFile gets you an LPCM caf file, which you may not want, hence I reconstruct the file settings.

Not positive here, but maybe since you're making the outputFile settings the same as the processing format, it's possible that the processing format has an inflexible policy on interleaving, whereas the file settings format will be fine with it - or vice versa.

Here's what I'd try first. Incomplete example, but should be enough to illustrate the areas to test.

let sourceFile: AVAudioFile
let format: AVAudioFormat

do {
    // for the moment, try this without any specific format and see what it gives you
    let sourceFile = try AVAudioFile(forReading: inputFileURL)
    format = sourceFile.processingFormat
    print(format) // let's see what we're getting so far, maybe some clues
} catch {
    fatalError("Unable to load the source audio file: \(error.localizedDescription).")
}

let sourceSettings = sourceFile.fileFormat.settings
var outputSettings = sourceSettings // start with the settings of the original file rather than the buffer format settings
outputSettings[AVAudioFileTypeKey] = kAudioFileCAFType

// etc...
Related