Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to create an accurate timer with GCD

Tags:

ios

swift

timer

I'm trying to make a music app that needs a very precise timer (it needs to be synced with background music). And I need to display the timer as a progress bar on the UI too.

I initially started with NSTimer, turned out to be not accurate at all, with above 20ms off. And I turned to GCD. But I seem not to be able to get it work neither.

Here is my code (removed parts that are not related)

This is the first line in my ViewController class

var dispatchQueue = dispatch_queue_t()

And in my override func viewDidLoad() function

dispatchQueue = dispatch_queue_create("myQueueId", nil)

Then the actual functions that use the dispatch queue

// Generate new measure
// In the main dispatch queue because it handles a lot of work
func newMeasure() {
    timerBar.progress = 1.0
    // Reset the timer
    logEnd = NSDate()
    let lapse = logEnd.timeIntervalSinceDate(logStart)
    println("Lapse: \(lapse)")
    logStart = NSDate()

    // measureTimer is set to 1.47, so the newMeasure is supposedly to be called every 1.47 seconds
    timer = measureTimer

    startTime = NSDate()
    dispatch_async(dispatchQueue, {
        self.gameTimer()
    })

    // ... do a lot of stuff that will drag the timer if not put in dispatch queue
}

// Accurate Game Timer
// In the dispacheQueue so the timer can has its own thread
func gameTimer() {
    // Get the time now
    let now = NSDate()
    let adjust = now.timeIntervalSinceDate(self.startTime)
    // Refresh the start time to be now
    self.startTime = now
    self.timer = self.timer - adjust

    // Go back to the main dispatch queue to update UI
    dispatch_async(dispatch_get_main_queue(), {
        self.timerBar.progress = Float(self.timer / self.measureTimer)
    })

    if (self.timer <= 0.2) {
        dispatch_async(dispatch_get_main_queue(), {
            // Going back to the main dispatch queue to start another new measure
            NSTimer.scheduledTimerWithTimeInterval(self.timer, target: self, selector: Selector("newMeasure"), userInfo: nil, repeats: false)
        })
    }
    else {
        dispatch_after(dispatch_time_t(
            100 * NSEC_PER_MSEC), dispatchQueue, {
                self.gameTimer()
        })
    }
}

But the actual log looks like this:

Lapse: 1.47152501344681
Lapse: 1.51420003175735
Lapse: 1.47065001726151
Lapse: 1.47149801254272
Lapse: 1.471755027771
Lapse: 1.47201299667358
Lapse: 1.47167503833771
Lapse: 1.47148901224136
Lapse: 1.47146201133728
Lapse: 1.47289103269577
Lapse: 1.47580003738403
Lapse: 1.47087097167969
Lapse: 1.47286003828049
Lapse: 1.47235900163651
Lapse: 1.47159999608994
Lapse: 1.47144496440887
Lapse: 1.50616401433945
Lapse: 1.51679295301437
Lapse: 1.47380495071411
Lapse: 1.47130501270294
Lapse: 1.50468301773071
Lapse: 1.4718160033226
Lapse: 1.49480104446411
Lapse: 1.50195497274399
Lapse: 1.50018000602722
Lapse: 1.47339296340942
Lapse: 1.47266495227814

They still are not so on time. And because time is so sensitive to music, this is not gonna work. Can someone help me with this accuracy issue? Thank you

like image 753
Jin Wang Avatar asked Jan 08 '23 03:01

Jin Wang


2 Answers

You should consider retiring NSTimer altogether and create a dispatch timer, which has a leeway parameter in which you can specify how much latitude you're giving the timer to run. Also, if you're trying to do timers on a background thread, it's a far simpler and more efficient way of doing that:

private var timer: dispatch_source_t!
private var start: CFAbsoluteTime!

func startTimer() {
    let queue = dispatch_queue_create("com.domain.app.timer", nil)
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, UInt64(1.47 * Double(NSEC_PER_SEC)), 0) // every 1.47 seconds, with no leeway
    dispatch_source_set_event_handler(timer) {
        let current = CFAbsoluteTimeGetCurrent()
        NSLog(String(format: "%.4f", current - self.start))
        self.start = current
    }

    start = CFAbsoluteTimeGetCurrent()
    dispatch_resume(timer)
}

func stopTimer() {
    dispatch_source_cancel(timer)
    timer = nil
}

This isn't going to happen precisely every 1.47 seconds, but it might be closer.

Note, I'm avoid dispatching stuff back and forth and/or scheduling new timers or dispatch_after, though. I create a single dispatch timer and let it go. Also, I'm avoiding NSDate and using CFAbsoluteTime, which should be more efficient.

like image 196
Rob Avatar answered Jan 11 '23 23:01

Rob


You are not actually using a GCD timer. A GCD timer would involve a dispatch source, and there is no dispatch source in your code.

Even then, however, the only really accurate way to get a timer is through CADisplayLink. It is a built-in timer that fires approximately every 1/60 second, because it is tied to the refresh rate of the hardware itself. Even CADisplayLink is not perfectly accurate, but it is close, and it as frequent as you can possibly get, and every firing includes a timestamp so you can compensate quite precisely.

like image 25
matt Avatar answered Jan 11 '23 23:01

matt