1

I have an array of Float (representing audio samples) and I want to turn it into an AVAudioPCMBuffer so I can pass it to AVAudioFile's write(from:). There's an obvious way (actually not obvious at all, I cribbed it from this gist):

var floats: [Float] = ... // this comes from somewhere else
let audioBuffer = AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(floats.count * MemoryLayout<Float>.size), mData: &floats)
var bufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer)
let outputAudioBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, bufferListNoCopy: &bufferList)!
try self.renderedAudioFile?.write(from: outputAudioBuffer)

This works (I get the audio output I expect) but in Xcode 13.4.1 this gives me a warning on the &floats: Cannot use inout expression here; argument 'mData' must be a pointer that outlives the call to 'init(mNumberChannels:mDataByteSize:mData:)'

Ok, scope the pointer then:

var floats: [Float] = ... // this comes from somewhere else
try withUnsafeMutablePointer(to: &floats) { bytes in
    let audioBuffer = AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(bytes.pointee.count * MemoryLayout<Float>.size), mData: bytes)
    var bufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer)
    let outputAudioBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, bufferListNoCopy: &bufferList)!
    try self.renderedAudioFile?.write(from: outputAudioBuffer)
}

The warning goes away, but now the output is garbage. I really don't understand this as floats.count and bytes.pointee.count are the same number. What am I doing wrong?

Robert Atkins
  • 23,528
  • 15
  • 68
  • 97

1 Answers1

1

This solution is somewhat hackish but should work:

@implementation AVAudioFile (FloatArrayWriting)

- (BOOL)writeFloatArray:(const float *)data count:(NSInteger)count format:(AVAudioFormat *)format error:(NSError **)error
{
    NSParameterAssert(data != NULL);
    NSParameterAssert(count >= 0);
    NSParameterAssert(format != nil);

    AudioBufferList abl;
    abl.mNumberBuffers = 1;
    abl.mBuffers[0].mData = (void *)data;
    abl.mBuffers[0].mNumberChannels = 1;
    abl.mBuffers[0].mDataByteSize = count * sizeof(float);

    AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:format bufferListNoCopy:&abl deallocator:NULL];
    return [self writeFromBuffer:buffer error:error];
}

@end

By implementing the method in Objective-C you can sidestep the pointer gymnastics needed for AVFAudio in Swift.

Here is a possible Swift solution:

try floats.withUnsafeMutableBufferPointer { umrbp in
    let audioBuffer = AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(umrbp.count * MemoryLayout<Float>.size), mData: umrbp.baseAddress)
    var bufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer)
    let outputAudioBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, bufferListNoCopy: &bufferList)!
    try self.renderedAudioFile?.write(from: outputAudioBuffer)
}
Robert Atkins
  • 23,528
  • 15
  • 68
  • 97
sbooth
  • 16,646
  • 2
  • 55
  • 81
  • Thanks for taking the effort to think this through, but _surely_ the "pointer gymnastics" are at least **possible** in Swift? If they aren't, what's the underlying reason and how big a hole is this in Swift's capabilities? Or is it just an edge-case compiler bug? – Robert Atkins Jun 20 '22 at 07:32
  • I added a possible Swift solution but I haven't had a chance to test it for correctness. – sbooth Jun 20 '22 at 13:47
  • YES! The magic spell was `.baseAddress`, thank you. You also need `mDataByteSize: UInt32(bytes.count * MemoryLayout.size)`, so I edited that in to your answer, hope you don't mind. – Robert Atkins Jun 21 '22 at 09:28
  • Glad it's working for you. I believe that `umrbp.count` is the count of bytes, not array elements, so multiplying shouldn't be necessary. – sbooth Jun 21 '22 at 11:37
  • The multiplication is definitely necessary, without it I got little chunks of my samples, with it, it works properly. – Robert Atkins Jun 21 '22 at 14:10
  • https://developer.apple.com/documentation/swift/unsafemutablerawbufferpointer/count-95usp - perhaps a different value is off by a factor of 4? – sbooth Jun 21 '22 at 18:57
  • Ah—another subtle difference, I'm using `.withUnsafeMutableBufferPointer` (rather than `.withUnsafeMutableBytes`), whose `count` property is "The number of elements in the buffer". Edited the above again so it's consistent. – Robert Atkins Jun 22 '22 at 07:24