iOS Audiokit high precision timer, possible?

Does AudioKit provide a high precision timer?

I had a look and found about AKSheduledAction, which is based on Timer and seems to be the only default option:

AKScheduledAction

Alternatively, to the usage of Timer, I wrote a scheduler that runs in the audio thread (* it’s not see bellow), correct me if wrong. I’ve tested and it’s not very precise, as the resulting time difference varies:

func schedule(timeOut: Double, onComplete: @escaping () -> Void) -> Void {
    do {
        let file = try AKAudioFile.silent(samples: Int64(defaultSampleRate * timeOut))
        let audioPlayer = try AKAudioPlayer(file: file)
        audioPlayer >>> self.mainMixer
        audioPlayer.completionHandler = {
            onComplete()
        }
        audioPlayer.play()
    } catch {
        print(error.localizedDescription)
    }
}

Output:

elapsedTime 4.715054468018934
elapsedTime 4.712334129028022
elapsedTime 4.71259418700356
elapsedTime 4.712263747991528

The test was run in the main thread by the following method:

func test() {
    let info = ProcessInfo.processInfo
    let begin = info.systemUptime
    self.schedule(timeOut: 4.666667) {
        let elapsedTime = info.systemUptime - begin
        print("elapsedTime \(elapsedTime)")
    }
}

I’ll have to do a bit more research into what the .completionHandler is, but I started by assuming as AVAudioPlayerNode.scheduleBuffer:

AVAudioPlayerNode.scheduleBuffer(AVAudioPCMBuffer, at: nil, options: .interruptsAtLoop, completionHandler: nil)

Looking at the source of completionHandler, which is a AKCallback, we can see it runs in the main thread (Link ):

/// Triggered when the player reaches the end of its playing range
fileprivate func internalCompletionHandler() {
    DispatchQueue.main.async {
        if self.isPlaying {
            self.stop()
            self.completionHandler?()
        }
    }
}

So, I’ll try to rewrite it to use AVAudioPlayerNode.scheduleBuffer, which hopefully will help.

Ref:

https://developer.apple.com/documentation/foundation/timer https://developer.apple.com/library/archive/technotes/tn2169/_index.html

I’ve completed my research and unfortunately didn’t or couldn’t find much information about a method providing a precise timer.

The best results I found is based on the following technique that holds the best result so far*, the audioFile has a length of 4.666667:

let info = ProcessInfo.processInfo
let begin = info.systemUptime
self.audioBufferPlayerNode.scheduleBuffer(self.file.pcmBuffer, at: nil, options: .interruptsAtLoop, completionHandler: {
    let elapsedTime = info.systemUptime - begin
    print("elapsedTime \(elapsedTime)")
})
self.audioBufferPlayerNode.play()

The output:

elapsedTime 4.691985241952352
elapsedTime 4.662765832967125
elapsedTime 4.664656096952967
elapsedTime 4.664366245968267
elapsedTime 4.666537321987562
elapsedTime 4.666480115964077
elapsedTime 4.659072204027325
elapsedTime 4.665851002908312

Have in mind that you need to compute the initialisation process for AVAudioPlayerNode and connect it to the mixer ahead of call time, otherwise if you compute all in a single call you get:

elapsedTime 4.701177543029189
elapsedTime 4.7050989009439945
elapsedTime 4.702830816968344
elapsedTime 4.695075194002129
elapsedTime 4.708125164033845
elapsedTime 4.700082497904077
elapsedTime 4.695926539017819
elapsedTime 4.711938992026262

You may want to look into the .prepare method to improve timings (https://developer.apple.com/documentation/avfoundation/avaudioplayernode )

Also, beware that this runs on the thread:

AVAudioPlayerNodeImpl.CompletionHandlerQueue

Hope this helps somebody else in the future!

comments powered by Disqus