I'm trying to figure out how to use the new outputProvider API on AVAssetReader. Specifically, I want to open a media file (video or audio), extract its audio, and write it back out as PCM. I have the following code:
import Foundation
import AVFoundation
enum AudioExtractorError: Error {
case noAudio
case bufferAllocationFailed
}
@available(macOS 26.0, iOS 26.0, *)
class AudioExtractor {
init(fileURL: URL) {
self.fileURL = fileURL
}
func extractAudio(from fileURL: URL, to outputFolder: URL) async throws -> URL {
let asset = AVAsset(url: fileURL)
guard let audioTrack = try await asset.loadTracks(withMediaType: .audio).first else {
throw AudioExtractorError.noAudio
}
let reader = try AVAssetReader(asset: asset)
let outputSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatLinearPCM,
AVLinearPCMIsBigEndianKey: false,
AVLinearPCMBitDepthKey: 32,
AVLinearPCMIsFloatKey: true,
AVLinearPCMIsNonInterleaved: false,
AVNumberOfChannelsKey: 2
]
let trackOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: outputSettings)
let outputProvider = reader.outputProvider(for: trackOutput)
try reader.start()
let outputURL = outputFolder
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("caf")
let outputFile = try AVAudioFile(forWriting: outputURL, settings: outputSettings)
while let sampleBuffer = try await outputProvider.next() {
guard case let CMSampleBuffer.DynamicContent.dataBuffer(blockBuffer) = sampleBuffer.content else {
continue
}
let blockSampleBuffer = CMReadySampleBuffer(dataBuffer: blockBuffer,
formatDescription: sampleBuffer.formatDescription!,
sampleProperties: sampleBuffer.sampleProperties)
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: outputFile.processingFormat, frameCapacity: AVAudioFrameCount(sampleBuffer.sampleCount)) else {
throw AudioExtractorError.bufferAllocationFailed
}
try blockSampleBuffer.copyPCMData(fromRange: 0..<sampleBuffer.sampleCount, into: pcmBuffer.mutableAudioBufferList)
try outputFile.write(from: pcmBuffer)
}
return outputURL
}
let fileURL: URL
}
When I run this, the call to blockSampleBuffer.copyPCMData() fails with error code -12731 which is kCMSampleBufferError_RequiredParameterMissing. Has anyone managed to get this API working?
1 Answer 1
It's great to see a new question on here!
There are two sources of copyPCMData() returning -12731 = kCMSampleBufferError_RequiredParameterMissing in your code:
- The
AVAudioFileinitializer you used gives you an interleavedfileFormatbut a deinterleavedprocessingFormatwhich does not matchAVAssetReaderTrackOutput's configuration (it's not obvious, I've mentioned this before) - You're not setting the
AVAudioPCMBuffer.frameLengthwhich results in a 0AudioBuffer.mDataByteSize
With the following changes, the code works for me:
- let outputFile = try AVAudioFile(forWriting: outputURL, settings: outputSettings)
+ let outputFile = try AVAudioFile(forWriting: outputURL, settings: outputSettings, commonFormat: .pcmFormatFloat32, interleaved: true) // note the inverted senses of interleaved vs AVLinearPCMIsNonInterleaved!
...
+ pcmBuffer.frameLength = AVAudioFrameCount(sampleBuffer.sampleCount)
try blockSampleBuffer.copyPCMData(fromRange: 0..<sampleBuffer.sampleCount,
into: pcmBuffer.mutableAudioBufferList)
6 Comments
AVAudioFile has some funny API corner cases, some inherited fromExtAudioFile, some not. My favourite is that it lacked a close method for a decade or so: stackoverflow.com/a/52122691/22147 Explore related questions
See similar questions with these tags.
CMSampleBufferCopyPCMDataIntoAudioBufferList(). I guess those structs and enums go a little beyond the usual syntactic sugar. TIL!kCMSampleBufferError_RequiredParameterMissingwas completely misleading.