I am looking at the 'Metronome' sample code from the iOS SDK (http://developer.apple.com/library/ios/#samplecode/Metronome/Introduction/Intro.html). I am running the metronome at 60 BPM, which means a tick every second. When I look at an external watch (the PC's watch), I see the metronome is running too slow - it misses about one beat each minute, which is app. 15msec of consistent error. The relevant code piece is:
- (void)startDriverTimer:(id)info {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Give the sound thread high priority to keep the timing steady.
[NSThread setThreadPriority:1.0];
BOOL continuePlaying = YES;
while (continuePlaying) { // Loop until cancelled.
// An autorelease pool to prevent the build-up of temporary objects.
NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];
[self playSound];
[self performSelectorOnMainThread:@selector(animateArmToOppositeExtreme) withObject:nil waitUntilDone:NO];
NSDate *curtainTime = [[NSDate alloc] initWithTimeIntervalSinceNow:self.duration];
NSDate *currentTime = [[NSDate alloc] init];
// Wake up periodically to see if we've been cancelled.
while (continuePlaying && ([currentTime compare:curtainTime] != NSOrderedDescending)) {
if ([soundPlayerThread isCancelled] == YES) {
continuePlaying = NO;
}
[NSThread sleepForTimeInterval:0.01];
[currentTime release];
currentTime = [[NSDate alloc] init];
}
[curtainTime release];
[currentTime release];
[loopPool drain];
}
[pool drain];
}
Where
self.duration
is 1.0 second in the case of 60 BPM. I wonder where this error comes from, and how can I make a more accurate timer/interval counter.
EDIT: The problem exists as well when I change the sleep time to smaller values, e.g .001.
EDIT2 (update): The problem exists as well when I use the CFAbsoluteTimeGetCurrent()
method for timing. When I use the same method to measure timing between a button tap events, the timing seems accurate - I tap once a second (while watching a watch), and the measured rate is 60 BPM (on average). So I guess it must be some issue with the NSThread
(?). Another thing is that on the device (iPod) the problem seems more severe then on the simulator.
Apple's NTP servers make sure iPhones and Apple Watches keep time at "Stratum One" accuracy, within milliseconds of "Stratum Zero" devices.
CACurrentMediaTime()Returns the current absolute time, in seconds.
Ok, I have some answers after doing some more tests, so I am sharing it with anyone who is interested.
I've placed a variable to measure time intervals between ticks, inside the play
method (the method that actually sends the play
message to the AVAudioPlayer
object), and as my simple compare-to-external-watch experiment showed, the 60 BPM was too slow - I got these time intervals (in seconds):
1.004915
1.009982
1.010014
1.010013
1.010028
1.010105
1.010095
1.010105
My conclusion was that some overhead time elapses after each 1-second-interval is counted, and that extra time (about 10msec) is accumulated to a noticeable amount after a few tens of seconds --- quite bad for a metronome. So instead of measuring the interval between calls, I decided to measure the total interval from the first call, so that the error won't be accumulated. In other words I've replaced this condition:
while (continuePlaying && ((currentTime0 + [duration doubleValue]) >= currentTime1)
with this condition:
while (continuePlaying && ((_currentTime0 + _cnt * [duration doubleValue]) >= currentTime1 ))
where now _currentTime0
and _cnt
are class members (sorry if it's a c++ jargon, I am quite new to Obj-C), the former holds the time stamp of the first call to the method, and the latter is an int
counting number of ticks (==function calls). This resulted in the following measured time intervals:
1.003942
0.999754
0.999959
1.000213
0.999974
0.999451
1.000581
0.999470
1.000370
0.999723
1.000244
1.000222
0.999869
and it is evident even without calculating the average, that these values fluctuate around 1.0 second (and the average is close to 1.0 with at least a millisecond of accuracy).
I will be happy to hear more insights regarding what causes the extra time to elapse - 10msec sounds as eternity for a modern CPU - though I am not familiar with the specs of the iPod CPU (it's iPod 4G, and Wikipedia says the CUP is PowerVR SGX GPU 535 @ 200 MHz)
If you need <100msec precision, look into CADisplayLink. It calls a selector at regular intervals, as fast as 60 times a second, i.e. every .0166667 sec.
If you want to do your metronome test, you'd set frameInterval
to 60 so it gets called back once a second.
self.syncTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(syncFired:)];
self.syncTimer.frameInterval = 60;
[self.syncTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
-(void)syncFired:(CADisplayLink *)displayLink
{
static NSTimeInterval lastSync = 0;
NSTimeInterval now = CACurrentMediaTime();
if (lastSync > 0) {
NSLog(@"interval: %f", now - lastSync);
}
lastSync = now;
}
In my tests, that timer is called consistently every second within 0.0001 sec.
On the off chance you're running on a device with a different refresh rate than 60Hz, you'd have to divide displayLink.duration
into 1.0 and round to the nearest integer (not the floor).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With