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.
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
playmethod (the method that actually sends theplaymessage to theAVAudioPlayerobject), and as my simple compare-to-external-watch experiment showed, the 60 BPM was too slow – I got these time intervals (in seconds):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:
with this condition:
where now
_currentTime0and_cntare 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 anintcounting number of ticks (==function calls). This resulted in the following measured time intervals: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)