I am currently wrapping my head around Core Audio and I was hit with the question of how to update the GUI the AudioQueueInputCallback. To begin with I want to update a label with the Level Meter reading from the mic.
In my code I am storing the current level meter value in a struct on each callback.
func MyAudioQueueInputCallback(inUserData: UnsafeMutablePointer<Void>, inAQ: AudioQueueRef, inBuffer: AudioQueueBufferRef, inStartTime: UnsafePointer<AudioTimeStamp>, var inNumberPacketDesc: UInt32, inPacketDesc: UnsafePointer<AudioStreamPacketDescription>){
var error: OSStatus
if (inNumberPacketDesc > 0){
error = AudioFileWritePackets(MyRecorder.recordFile, false, inBuffer.memory.mAudioDataByteSize, inPacketDesc, MyRecorder.recordPacket, &inNumberPacketDesc, inBuffer.memory.mAudioData)
checkError(error, operation: "AudioFileWritePackets Failed ")
// Increment the packet index
MyRecorder.recordPacket += Int64(inNumberPacketDesc)
if (MyRecorder.running){
error = AudioQueueEnqueueBuffer(inAQ, inBuffer, inNumberPacketDesc, inPacketDesc)
checkError(error, operation: "AudioQueueEnqueueBuffer Failed BAHHHH")
var level: Float32 = 0
var levelSize = UInt32(sizeof(level.dynamicType))
error = AudioQueueGetProperty(inAQ, kAudioQueueProperty_CurrentLevelMeter, &level, &levelSize)
checkError(error, operation: "AudioQueueGetProperty Failed... Get help!")
MyRecorder.meterLevel = level // meter level stored in public struct
}
}
}
//MARK: User Data Struct / Class
struct MyRecorder {
static var recordFile: AudioFileID = nil
static var recordPacket: Int64 = 0
static var running: Bool = false
static var queue: AudioQueueRef = nil
static var meterLevel: Float32 = 0.00
}
From here the meterLevel variable is polled using an NSTimer on the Main thread.
func setMeterLabel() -> Void{
meter.text = String(MyRecorder.meterLevel)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
main()
var timer = NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: "setMeterLabel", userInfo: nil, repeats: true)
}
This code works just fine but I feel as though using the NSTimer may not be the best way to go about it. Is there a way for my callback to directly update the label?
Looks like everything is happening within the same controller, so you should be able to access everything you need from within the callback function.
You probably won't want to update the GUI on every callback, but you could set up a counter which you increment every time, and make the GUI updates at regular intervals.
The only thing you have to change is to ensure that you call the
setMeterLabel
on the main queue - you can't make GUI changes from a background queue.The callback rate might be too fast so consider creating an average filter ( or box average). Let the audio callback update a simple primitive (int, float, etc.) in your controller. You can make a delegate if you want. Store them in a queue and keep only say the last 20 items. Get rid of the oldest and keep the newest.
Let your UI calculate the average of the latest n items you chose and show it on the ui at a periodic rate.
Using a repeating NSTimer or CADisplayLink is a good and standard way to do this. Any other method incurs the possibility of blocking your audio callback for too long.
You know your audio sample rate and the size of your audio buffers. Therefore you know approximately how often new data buffers will appear. Therefore, at the slowest of the 2 rates (the new audio buffer rate or your desired frame animation rate, usually 60 or 30 Hz or slower) you can set a repeating timer callback (or a CADisplayLink timer callback) to poll whether a new audio data has been logged, and do something, such as call a setNeedsDisplay on some view which needs to be updated. (The UIView drawRect update will always be called on the main UI thread).
One good way to pass status or data from the audio callbacks to your timer callbacks is using a lock-free circular FIFO/buffer. Then you are guaranteed never to block your audio callback thread.
And if you run your timer callback on the UI thread, there is no need to do an async dispatch to the same thread.