Accurate Timing in Corona

If you're a forum responder who says non-going-out-of-sync timing is impossible, please don't take offense at this. I'm not meaning to make fun of you or disparage you, I'm just meaning to point out in a normal argument that that statement isn't correct. I have two forum posters in mind whom I respect very much, but who both have erred with their response to non-going-out-of-sync timing. I'm not trying to put you down. Permit me some rhetoric humor. Please (please, please, please!) don't take this the wrong way.

Also, as of the writing date of this post, Corona timers (in the timer.* namespace) have trouble going out of sync. If Corona fixes this sometime in the future, this post will become unnecessary.

Many's the day I've come across a six-month-old forum post, too late to save, consisting of one person saying "My [pulse/sound/graphic/timer, pick one] is going out of sync! Help!", and another person saying "Sorry, dude. That's how things are. Can't be fixed. Use [Swift/Java/Objective-C/C/C++, pick one]." If either of those people sounds like you, great! If you're the person with the problem, take heart - this can be fixed. If you're the there-ain't-no-way responder, take heart - you can learn the truth.

First off, let me answer the "why." Why do Corona's timers go out of sync? Aren't they accurate enough?

Yes and no. Corona's timers are accurate in a sense. They fire your callbacks within about 40 ms of the target time, which is really quite good, all things considered. For most purposes (particle effects, AI behaviors, state checking, etc.), this is plenty accurate. It's whenever a developer requires something more that Corona's timers become the wrong tool for the job. For anything that requires an exact iteration period - for example, rythm games, where a timer has to fire exactly, say, 120 times a minute - Corona's timers cannot be used. Corona's timers are unsuitable for this because of an inherent flaw in their implementation. Following is a simple overview of how a timer using Corona's approach is made:

local bpm = 120
local bpmMilliseconds = 60000 / bpm

print("The BPM is " .. bpm .. ", so the delay should be " .. bpmMilliseconds)

local simpleTimer
simpleTimer = {
  startTime = 0,
  delay = bpmMilliseconds,
  nextTime = 0,
  lastTime = 0,
  callback = function()
    print("Delay was " .. (system.getTimer() - simpleTimer.lastTime))
  end
}

simpleTimer.startTime = system.getTimer()
simpleTimer.nextTime = simpleTimer.startTime + simpleTimer.delay

local function updateTimer()
  local time = system.getTimer()
  
  if time >= simpleTimer.nextTime then
    simpleTimer.callback()
    simpleTimer.lastTime = time
    simpleTimer.nextTime = time + simpleTimer.delay
  end
end

Runtime:addEventListener("enterFrame", updateTimer)

The code is fairly simple. All we do is create a table with the fields startTime, for the time the timer was started, delay, for the delay between timer "firings", nextTime, the next time the timer will fire, and lastTime, the last time the timer was fired. When we update the timer (typically in an enterFrame listener), we check for if the current time is past the timer's next firing time. If it is, we call the callback and set the last firing time to the current time, then set the next firing time to a delay of delay ms from now. Makes sense, right? The problem isn't in the code, the problem is in the system.

Let's think theoretically for a minute. Most devices lock a game's framerate to either 30 FPS or 60 FPS. That means that the time of each frame on a device (and, thus, the time between updates of the timer) is either 33.333... milliseconds or 16.666... milliseconds. So in an ideal situation, with each frame taking exactly the time it should, the highest timing accuracy we can get is to the 30th or 60th of a second. In reality, framerate is never exact, which means our accuracy will usually be even lower. What, practically, does this mean? Well, let's mentally step through the code in an ideal environment. Instead of 120 BPM as we have in the example above, assume the delay is set to 100.

Theoretically, this sequence should repeat every three frames, resulting in a perfectly accurate timer that never goes out of sync. So didn't I just prove myself wrong? Didn't I just demonstrate that perfectly accurate timers can be done with this naive approach? Absolutely not. You see, here we have a delay of 100. Any value that's not a multiple of 33.333... ends up like this like this:

Wait. We just lost 13 1/3 ms. Each time we fire the timer, we lose 13 1/3 ms. That means for every 75 firings of the timer, we'll lose an entire second of accuracy. And this is in a theoretical situation with a perfectly exact framerate.

Ok, enough with the theoretical part. In real life, the framerate is nowhere near as accurate as a mathematically exact fraction. You're much more likely to get 16 ms on the first frame, then 18, then 15, then 14, then maybe a lag and get 24, etc. To demonstrate this, here's what I got from running the original code at 60 FPS (with the timer at 120 BPM or 500 ms delay):

The BPM is 120, so the delay should be 500
Delay was 509.951
Delay was 517.941
Delay was 511.132
Delay was 512.742
Delay was 511.877

Here, we're adding anywhere from 10 to 18 extra ms each time the timer fires. This means that, instead of a BPM of 120, we have a BPM of approximately 117 or 118. And that "approximately" is the big problem - our BPM can stutter anywhere thereabouts. There's no way this timer is acceptable for anything requiring accuracy.

A slight pause whilst I point out something important. This inaccuracy is a fundamental issue in every device. Whether you're using Lua, another scripting language, or are doing native programming in Swift, Java, or C++, this framerate problem is still there. Switching from Corona to native or getting Corona Enterprise will not help you here, no matter what people may say. Even doing multi-threaded magic will have the same problem, because it is fundamentally impossible to get an exact framerate, due to time being a continuous quantity.

What needs to change to get an accurate timer isn't the accuracy of the time reporting, it's the mechanism behind the timer itself. The kind of accuracy we need isn't to-the-nanosecond accuracy, it's long-term accuracy. A rythm game can deal with an event firing 1/60th of a second late (for reference, that's over 10 times faster than a typical blink of the eye!). It can't deal with the BPM sliding away from the target and losing sync with the audio or visual cues.

So how is this done? I present: The Accurate Timer Method of Goodness. Instead of using the naive approach and setting next firing time to the current time plus the delay when the timer fires, keep a count of times the timer has fired. Then, multiply the count by the delay to get the precise time the timer should fire next. The end result looks like this:

local bpm = 120
local bpmMilliseconds = 60000 / bpm

print("The BPM is " .. bpm .. ", so the delay should be " .. bpmMilliseconds)

local accurateTimer
accurateTimer = {
  startTime = 0,
  delay = bpmMilliseconds,
  lastTime = 0,
  iterations = 1,
  callback = function()
    print("Delay was " .. (system.getTimer() - accurateTimer.lastTime))
  end
}

accurateTimer.startTime = system.getTimer()

local function updateTimer()
  local time = system.getTimer()
  
  if time >= accurateTimer.startTime + accurateTimer.iterations * accurateTimer.delay then
    accurateTimer.callback()
    accurateTimer.lastTime = time
    accurateTimer.iterations = accurateTimer.iterations + 1
  end
end

Runtime:addEventListener("enterFrame", updateTimer)

At first glance, it doesn't look so very different. We have the same setup and overall approach. What about when we run it?

The BPM is 120, so the delay should be 500
Delay was 492.517
Delay was 493.888
Delay was 511.457
Delay was 496.504
Delay was 496.235

At first glance here, too, it looks like the accurate timer has the same issue the simple one does. The difference is that this timer compensates for error. The first two firings, we lose 11 ms, but then - here's the important part - we gain it back in the next tick. This system of losing, then gaining goes on with the accurate timer, resulting in perfect precision in the long term. Sure, we're off by a few ms each time it fires, but that's necessary in any timer locked in a 60 FPS environment (read: every normal timer, even in native code). Plus, we're talking about 8 or 9 milliseconds of difference each tick. That's less than a hundredth of a second. If the people playing your rythm game have better reflexes than that, I take my hat off to them. And that's really quite a statement, given that I'm not even wearing a hat.