views:

206

answers:

3

Hi All, I am looking for this for weeks now: My app needs a metronome like function and I did it with a thread (copied and adapted from Apples Metronom example) but the timing is miserable. I know that the timer ressources are not accurate but I cannot find another feaseable technique. And I need just short "beep" sounds so tackling the full Audio playback stuff is not required. I divide the BPM (beats per minute) by 12 to have the possibillity to play also the eigth, sixteenth and so on notes. So the problem is not the audio play, the problem is the accurate time distance between two "ticks".

Here the Code for calculation the tick distance:

- (double)Be_get12TickDistance:(NSInteger)bpm{
double result = 60.0 / (bpm*12);
return result;

} Here the thread:

- (void)startDriverTimer:(id)info {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// Give sound thread high priority to keep the timing steady
[NSThread setThreadPriority:1.0];
BOOL continuePlaying = YES;

while (continuePlaying) {  // loop until cancelled
    // Use an autorelease pool to prevent the build-up of temporary date objects
    NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init]; 

     SoundEffect *currentSoundEffect= [[self beepAndLEDKeeper] Be_GetBeepSound4CurrentTick:beatNumber theCurrentGroove:grooveNumber];
     [currentSoundEffect play];

     if ([[self beepAndLEDKeeper] Be_isLEDFlashPoint:beatNumber]){
         [self performSelectorOnMainThread:@ selector(setLEDtoHidden:) withObject:[NSNumber numberWithInt:LEDoldBeatNumber+oldGrooveNumber*9] waitUntilDone:NO];
         [self performSelectorOnMainThread:@ selector(setLEDtoUnHidden:) withObject:[NSNumber numberWithInt:LEDbeatNumber+grooveNumber*9] waitUntilDone:NO];
         LEDoldBeatNumber = LEDbeatNumber++;
     }

     oldBeatNumber = beatNumber++;
     oldGrooveNumber = grooveNumber;

     if (beatNumber >= grooveEnd[grooveNumber]) 
     {
         beatNumber=0;
         LEDbeatNumber=0;
         if (++grooveNumber > 1)
             grooveNumber=0;
         if (grooveEnd[grooveNumber] == 0)
             grooveNumber=0;
     }

     if ([soundPlayerThread isCancelled] == YES)
         continuePlaying = NO;
     else
         [NSThread sleepForTimeInterval:self.duration];

     [loopPool release];
}
[pool release];

}

Question: Does anybody has a hint how to do that in an easy way? I just need a better timing possibillity, the playing with SoundEffect is good enough.

Thanks for all help, Andreas

A: 

One strategy that will work is to have a background thread that sleeps for a fixed amount of time (say, every 1/16th of a second). This uses the more accurate CPU clock, so when it returns from the call, you'll be much more accurate.

John Feminella
Hi John,ok, you mean that I have a fixed distance between a fine granular tick and then construct my BPM ticks out of it?I tried that once on a 80C535 design and needed to realize that the timing error for some Beat rates is not negligible because you have a rounding error for beats like 300bpm where a 12tick distance is 16.66666667 ms. That adds up ... but I could do such a thread with the actual tick time.But am I not using a background thread by doing [NSThread sleepForTimeInterval:self.duration];???Andreas
Andreas
A: 
NSTimer timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(YOUR_METHOD:) userInfo:nil repeats:YES];

Replace scheduledTimerWithTimeInterval:0.1 with whatever interval (in seconds) you want, and replace YOUR_METHOD with whatever your method is.

James
Ah, you beat me to it -- but remember that if you're going to hold onto the timer like that, you must retain it! `[NSTimer scheduledTimer...]` brings back an autoreleased `NSTimer` by default.
Ian Henry
Quite right! Thanks for catching the mistake of a sleepy Java programmer ;)
James
Ok, so if I use the [NSTimer scheduleTimer ...] Version when is the timer released? I ask because I start and stop that timer constantly and want to avoid memory leakeage surprises.Thanks
Andreas
+1  A: 

Simplest solution, and one which is quite accurate, is to just use an NSTimer. I created a simple demo app to test it, and it was accurate to a thousandth of a millisecond (I just NSLogged every time a timer ticked).

2010-02-07 17:11:07.548 TimerTest[12775:207] Ticked
2010-02-07 17:11:07.648 TimerTest[12775:207] Ticked
2010-02-07 17:11:07.748 TimerTest[12775:207] Ticked
2010-02-07 17:11:07.848 TimerTest[12775:207] Ticked
2010-02-07 17:11:07.948 TimerTest[12775:207] Ticked
2010-02-07 17:11:08.048 TimerTest[12775:207] Ticked
2010-02-07 17:11:08.148 TimerTest[12775:207] Ticked

This is output from an iPod Touch 2nd Gen device. (Note that accuracy will decrease if you're doing a lot of hard processing in the meantime, but I don't imagine a metronome will have that much else going on). The timer came from the very simple code:

[NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:@selector(tick) userInfo:nil repeats:YES];

But my guess is that it is in fact the sound code that's causing the problem. What are you using to play the sounds? If it's anything but OpenAL, you can't expect it to be very accurate -- it can pause before playback, after playback, etc. Try to get a test app and see if the timer is ticking at the correct rate without the sound and with the sound to identify the cause of the problem.

Ian Henry
Hi Ian,thanks for the answer, I appreciate every help here :-)I use the "SoundEffect" Class as used in the Metronome demo from Apple. I also change the hidden state of some images in the Main thread but that is all processing done in the tick loop.I will try the NSTimer and see if the Sound/Graphic LED status change is the root of the problem.Andreas
Andreas