Rhythm based game? How to stay in sync

Hi Guys,

I’ve been experimenting with a rhythm based game where a player pulses to the kick drum of a music track.

The way I’ve done it is to work out how many milliseconds a beat is and play an animation every beat.

The problem is that the music track will never play 100% perfectly, so it will go out of sync.

Is there a way to get the current track play position? Or has anyone got any tips?

Cheers

Rob

I’m going to have to disappoint you here: I’ve been trying to do this for three years now, but as great as Corona is, the audio system doesn’t allow for this level of control. I’ve tried all of the workarounds and suggestions in all of the forum posts. I consider myself a serious programmer, and I used to be an audio engineer, so I know my way around this stuff.

Oh dear :frowning:

I managed to get the player to bounce to the exact BPM of the track, but I guess the track is playing very slightly faster or slower at all times. Poo.

Poo indeed! Other people might disagree, but trust me, I very much have a “can do” mentality, especially where Corona is concerned, but after my own experience with rhythm based games, I’d advise to invest time in another project or use Objective-C or Swift. I only got round to fairly solid timing by jumping through so many hoops and having so many restrictions that it was obvious this was the wrong path.

One idea would be splitting the track into bars and make a sequencer that plays 1 bar at a time? Might not be able to play it smoothly though.

Hey, I was thinking about that. But I think there’d be a considerable “pop” when the different samples trigger. No point in not trying though :slight_smile:

I could trigger the next one with the onComplete listener. 

145 BPM for a 3 minute long track is a lot of beats though :smiley:

Maybe I could split it into bars. Then do four animations per bar. 

I’ll give it a go and get back to you :slight_smile:

Are you using a timer to make your player bounce and to play the rythm?

Also, do you know your track is at a constant BPM?

Because if you are, and the BPM is something that can be accurately divided into the correct time, your problem is likely with Corona’s timer system itself. A while ago, I created a library specifically to combat the timer inaccuracies, but it’s probably out of date now and it’s not hard to write one from scratch.

Basically, the way Corona’s timer works is like so:
[lua]
each frame:
if timerStartTime + timerDelayTime >= frameTime then
doTimerListener()
timerStartTime = frameTime
done
done
[/lua]

The problem is in that check. if timerStartTime + timerDelayTime checks for if the time has passed, but, since the FPS is never perfect and is gauged in increments of 16 or 33 ms, and the timer sets the start time to the frame time, each iteration you lose a few milliseconds. That’s fine if all you’re doing is a particle effect, but for more accuracy, it’s unnacceptable. If you have a timer running every 100 ms that starts at time 5 ms, it actually ends up like this (real timer values printed from the simulator):

2015-01-21 07:01:23.076 Corona Simulator[576:10375] 338.858
2015-01-21 07:01:23.197 Corona Simulator[576:10375] 459.242
2015-01-21 07:01:23.329 Corona Simulator[576:10375] 590.924
2015-01-21 07:01:23.461 Corona Simulator[576:10375] 723.044
2015-01-21 07:01:23.592 Corona Simulator[576:10375] 854.252
2015-01-21 07:01:23.725 Corona Simulator[576:10375] 987.251
2015-01-21 07:01:23.862 Corona Simulator[576:10375] 1124.685
2015-01-21 07:01:23.989 Corona Simulator[576:10375] 1251.183
2015-01-21 07:01:24.121 Corona Simulator[576:10375] 1383.132
2015-01-21 07:01:24.252 Corona Simulator[576:10375] 1514.608
2015-01-21 07:01:24.385 Corona Simulator[576:10375] 1647.011
2015-01-21 07:01:24.516 Corona Simulator[576:10375] 1778.199

See the horrific inaccuracies? The first time we ran at 338, the second time should be 438, right? Wrong! It’s 459! We just lost 21 ms in one iteration!

The way you can handle timers so that they’re accurate is by keeping a counter of iterations and checking for startTime + iterations * delay instead.

Here’s a full program with an accurate timer vs the built-in timer, both set to 100 ms delay, and a status check for how many times each one’s run:

local accurateTimer = { delay = 100, startTime = 0, nextTime = 0, iterations = 0, onTimer = function(time) print(time) end } -------------------------------------------------------------------------------- -- Update Timer -------------------------------------------------------------------------------- accurateTimer.update = function(event) if event.time \>= accurateTimer.nextTime then accurateTimer.onTimer(event.time) accurateTimer.iterations = accurateTimer.iterations + 1 accurateTimer.nextTime = accurateTimer.startTime + accurateTimer.iterations \* accurateTimer.delay end end accurateTimer.start = function(delay) accurateTimer.delay = delay accurateTimer.startTime = system.getTimer() accurateTimer.nextTime = accurateTimer.startTime + accurateTimer.delay end local badTimerIterations = 0 local badTimer = timer.performWithDelay(100, function() badTimerIterations = badTimerIterations + 1 end, 0) accurateTimer.start(100) Runtime:addEventListener("enterFrame", accurateTimer.update) timer.performWithDelay(5000, function() print("TIME: " .. (system.getTimer() - accurateTimer.startTime) .. "; ACC: " .. accurateTimer.iterations - 1 .. "; BAD: " .. badTimerIterations) end, 0)

By the way, in the above example, I used a timer.performWithDelay to do the status check, but that’s fine because I also print out the true time.

  • Caleb

Once again, let me save you a lot of time and tell you up front that, because Corona handles audio on a best-effort basis, all your wonderfully crafted code will stil result in the need to create lots of samples with varying amounts of silence spliced before your true audio data and in the end still sloppy or choppy rhythm.

How so? If you use an accurate timer, you can play the track in the background and the rhythm/bouncing/whatever with the accurate timer and only be off by at most 16 ms, which will be regained the next frame. 16 ms is negligible in a rhythm game - it’s over ten times faster than a blink of an eye!

  • Caleb

I’ve tried using your timer method (thanks!!) however it still drops out of sync. I’m guessing that the music must be playing at a fluctuating rate.

I tell a lie! I was using it all kinds of incorrectly!! 

It seems to be working pretty well. I’ll just need to test on devices.

Celeb P, you may have saved my project :slight_smile:

To cut a long story short, try the attached project and let me know if you find this solid or stuttering timing. I don’t think you’ll be able to make anything simpler than this and it’s still sloppy timing, and definitely noticeable (on an iPhone 5 and iPad 3).

Add anything else to this and you’ll timing will only suffer.

That example stutters because you’re using an enterFrame listener and a count. Each frame may or may not be exactly 16.666666 or 33.3333333 milliseconds, so the time will have to stutter. But if you use an accurate timer (like the sample code I posted), not bothered by the lack of frame exactness, you’ll hear minimal or even zero stuttering. Add in a background track and event noises and you’ll likely hear nothing at all.

[EDIT:] Even with an engine other than Corona, games on any device usually cap their framerate at 60 fps. So Corona isn’t really the issue here. Even if you try moving to Obj-C/Swift with SpriteKit and some special Cocoa timer class, it’ll still be the same level of un-exactness, even if you have more control over sound playing. It all goes down to how you work things out, which will be about the same regardless of what engine you use.

  • Caleb

Hi Caleb, let me try your code - I’ll keep you posted. Thanks!

Hi Caleb. I just tried your code, and as I expected it stutters just as much.

Your code is an interesting approach and does fix some issues with Corona or apps in general, but the problem for me is that your code only makes the long-term total time passed more acurate. However, it does nothing to make short term timing tighter. Your code will probably (and hopefully) help out Rob’s project, but it does not help to create non-stuttering audio for more sequence-based audio apps.

Try out this code:

[lua]

local time = system.getTimer()

Runtime:addEventListener(“tap”, function(event)
    print(event.time - time)
    time = event.time
end)

[/lua]

Start the Corona Simulator, then click or tap the screen as regularly as you possibly can. The time between clicks/taps doesn’t matter. I’m very musical and have a great sense of rhythm, and the delay for me varies by at worst 30 ms between taps.

So if the audio stutters by one frame time (16.666 ms), again, I don’t think, with a background track, sounds playing, etc., you’ll be able to notice. The issue here was that the time was going out of sync. As long as it’s in sync and very close to the correct time, it’ll be fine.

  • Caleb

It is an old game I made:

https://itunes.apple.com/us/app/free-the-beat-free-edition/id536885804

I was using the bpm code that is the code exchange

I didn’t play the sounds directly, I had several tables like this {0,0,0,0,0,0,0,0,0} and I changed those values …and the bpm check for the values to play the sounds…

I’m going to have to disappoint you here: I’ve been trying to do this for three years now, but as great as Corona is, the audio system doesn’t allow for this level of control. I’ve tried all of the workarounds and suggestions in all of the forum posts. I consider myself a serious programmer, and I used to be an audio engineer, so I know my way around this stuff.

Oh dear :frowning:

I managed to get the player to bounce to the exact BPM of the track, but I guess the track is playing very slightly faster or slower at all times. Poo.

Poo indeed! Other people might disagree, but trust me, I very much have a “can do” mentality, especially where Corona is concerned, but after my own experience with rhythm based games, I’d advise to invest time in another project or use Objective-C or Swift. I only got round to fairly solid timing by jumping through so many hoops and having so many restrictions that it was obvious this was the wrong path.