In my previous post, I have come up with a simplistic implementation of an AVAudioEffectNode with a custom render block. At that time, I assumed a fixed setup with two channels and a sample rate of 44,100 Hz. However, a reader recently reached out, asking how to adapt this for an audio effect that converts stereo input to mono output. This inspired me to modify the AVAudioEffectNode
class for greater flexibility.
Key Changes
To accommodate different input and output configurations, I made the following updates:
- Dynamic Bus Arrays: Instead of using fixed input and output busses, I implemented lazy-loaded bus arrays. This allows for more dynamic configuration:
private lazy var inputBusArray: AUAudioUnitBusArray = {
let defaultAudioFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)!
let inputBus = try! AUAudioUnitBus(format: defaultAudioFormat)
return AUAudioUnitBusArray(audioUnit: self, busType: .input, busses: [inputBus])
}()
private lazy var outputBusArray: AUAudioUnitBusArray = {
let defaultAudioFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)!
let outputBus = try! AUAudioUnitBus(format: defaultAudioFormat)
return AUAudioUnitBusArray(audioUnit: self, busType: .output, busses: [outputBus])
}()
- Overriding Bus Properties needed for AUV3 implementation
public override var inputBusses: AUAudioUnitBusArray {
return inputBusArray
}
public override var outputBusses: AUAudioUnitBusArray {
return outputBusArray
}
For the full implementation, check out this gist.
Usage
You can now easily change the input and output formats using:
avAudioEffectNode.auAudioUnit.inputBusses.replaceBusses
avAudioEffectNode.auAudioUnit.outputBusses.replaceBusses
Here's an example using the symclip node from my previous example:
print(symClipNode. inputFormat (forBus: 0))
// prints: <AVAudioFormat 0x600002a85180: 2 ch, 44100 Hz, Float32, deinterleaved>
let audioFormat = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1)!
let inputBuss = try AUAudioUnitBus(format: audioFormat)
symClipNode.auAudioUnit.inputBusses.replaceBusses([inputBuss])
print(symClipNode.inputFormat (forBus: 0))
// prints: <AVAudioFormat 0x600002a84d0: 1 ch, 48000 Hz, Float32, deinterleaved>
Bonus: BYOB (Bring your own buffers) input pulling
When the input and output have different buffer counts (2 in and 1 out in our reader's case) you have to BYOB.
Here's a sane (and not very Swifty) way of doing this:
let sumNode = AVAudioEffectNode(renderBlock: { actionFlags, timestamp, frameCount, outputBusNumber,
outputData, renderEvent, pullInputBlock -> AUAudioUnitStatus in
let bufferSizeBytes = MemoryLayout<Float>.size * Int (frameCount)
var inputBufferlist = AudioBufferList.allocate(maximumBuffers: 2)
inputBufferList[0] = AudioBuffer (mNumberChannels: 1,
mDataByteSize: UInt32(bufferSizeBytes),
mData: malloc(bufferSizeBytes))
inputBufferList[1] = AudioBuffer (mNumberChannels: 1,
mDataByteSize: UInt32(bufferSizeBytes),
mData: malloc(bufferSizeBytes))
// Pull the audio from the input
let inputStatus = pullInputBlock?(actionFlags, timestamp, frameCount, 0, inputBufferList.unsafeMutablePointer)
if inputStatus != noErr {
return inputStatus ?? KAudioUnitErr_FailedInitialization
}
let ablPointer = UnsafeMutableAudioBufferListPointer(outputData)
for buffer in ablPointer {
// do your summing here
let input = UnsafePointer<Float>(inputBufferList[0].mData!.assumingMemoryBound(to: Float.self))
...
}
}
Since you allocated you are now also responsible for freeing when you are done with the block:
for buffer in inputBufferList {
free(buffer.mData)
}
free(inputBufferList.unsafeMutablePointer)
Unfortunately some of those C-APIs don't translate well to Swift and can be hard to use even when you know what you are doing.
Feel free to contact me for tips, feedback, opinions or sending me your cool audio effects.
Thank you for reading!