Skip to content

Instantly share code, notes, and snippets.

@Priva28
Last active October 8, 2022 04:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Priva28/ffd76d11c87eb265a58add47ab5d140b to your computer and use it in GitHub Desktop.
Save Priva28/ffd76d11c87eb265a58add47ab5d140b to your computer and use it in GitHub Desktop.
This is a basic Swift function that allows for incoming system audio to be played to the system output. It uses the modern Swift syntax for setting up and using RemoteIO Audio Units. I hope someone can use it to avoid the pains of having to use AVAudioEngine and understand how to setup RemoteIO Audio Units in Swift just a little bit easier :)
// This is a basic Swift function that allows for incoming system audio (like the microphone or USB input) to be
// played to the system output. It uses the modern Swift syntax for setting up and using RemoteIO Audio Units.
// Of course, before running this function, you would have to setup your AVAudioSession with the playAndRecord category,
// as well as whatever other options you might like. This is just a barebones function that demonstrates the capability
// and you should implement some better error handling and respond to audio session route changes or interruptions.
// I've only created this gist because even though it seems like it would be a simple thing, there is essentially
// zero documentation on anyone trying to do this on the entire internet, and if there was any, it was basically
// ancient and using Objective-C and outdated APIs. Now with this I hope someone can use it to avoid the pains of
// having to use AVAudioEngine and understand how to setup RemoteIO Audio Units in Swift just a little bit easier :)
private var audioUnit: AUAudioUnit!
private var renderBlock: AURenderBlock?
func startAudioPlayThrough() {
// Adjust this to whatever format you want your input and output to be.
// Getting the native sample rate from your audio session is a good idea and will reduce bugs and errors (but make sure to stop and reset your audio units format whenever the audio session route changes!)
guard let audioFormat = AVAudioFormat(
commonFormat: AVAudioCommonFormat.pcmFormatFloat32,
sampleRate: audioSession.sampleRate,
channels: 2,
interleaved: true
) else { return }
// Note that an output unit provides both output AND input! (this confused me a lot at first)
let audioComponentDescription = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_RemoteIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
do {
try audioUnit = AUAudioUnit(componentDescription: audioComponentDescription)
// bus 0 is always the default system output
// bus 1 is always the default system input
// okay so this might also seem confusing but how i interperet is that the output busses are those from the audio unit that output data. aka input from mic or external device. so it gets stuff from the input, then outputs it to our code ig.
let bus1 = audioUnit.outputBusses[1]
try bus1.setFormat(audioFormat)
// same thing here, the input bus is where the audio unit accepts input from our code and then passes that to the system output.
let bus0 = audioUnit.inputBusses[0]
try bus0.setFormat(audioFormat)
} catch {
print(error)
return
}
// Apple: Before invoking an audio unit’s rendering functionality, a host should fetch this block and cache the result. The block can then be called from a realtime context without the possibility of blocking and causing an overload at the Core Audio HAL level.
// is this right? i dont know? it works.
renderBlock = audioUnit.renderBlock
// the audio unit output provider allows us to render the incoming ioData which is then passed to the output.
audioUnit.outputProvider = { actionFlags, timestamp, frameCount, inputBusNumber, ioData in
if let renderBlock = self.renderBlock {
// render the incoming data from bus 1 (input bus remember!)
let inputBus = 1
let error = renderBlock(
actionFlags,
timestamp,
frameCount,
inputBus,
ioData,
.none
)
// Create an AVAudioPCMBuffer from the ioData buffer list so we can do some easier swifty stuff like Shazam recognition or whatever you'd like.
guard let pcmBuffer = AVAudioPCMBuffer(
pcmFormat: audioFormat,
frameCapacity: frameCount
) else { return 0 }
pcmBuffer.frameLength = frameCount
memcpy(pcmBuffer.mutableAudioBufferList.pointee.mBuffers.mData, ioData.pointee.mBuffers.mData, Int(ioData.pointee.mBuffers.mDataByteSize))
pcmBuffer.mutableAudioBufferList.pointee.mBuffers.mDataByteSize = ioData.pointee.mBuffers.mDataByteSize
// pass the pcm buffer to some other function! done!
// you don't have to do this, it's just if you want to inspect the pcm buffer.
self.processInput(pcmBuffer: pcmBuffer)
}
return noErr
}
audioUnit.isInputEnabled = true
audioUnit.isOutputEnabled = true
do {
try audioUnit.allocateRenderResources()
try audioUnit.startHardware()
} catch {
print(error)
return
}
}
@Priva28
Copy link
Author

Priva28 commented Oct 8, 2022

Also, if you wanted to just get incoming buffers and play them some other way or not feed them to the output and just do your own processing, you can simply not setup the output bus, and use inputHandler rather than outputProvider.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment