1

I am working with an app that needs to play a sound "tap" at 60 ms intervals (making for 1000 taps per minute). When I get past about 120 ms intervals (500 taps per minute), the sounds become very erratic and seem to be piling up on one another.

The sound byte that I'm using is 10 ms long, and I have a thread timer that is running at 20 ms intervals. I am using AVAudioPlayer to play the sound, and have tried formats of wav, caf and m4a. I have tested with NSLog outputs to see when the AVAudioPlayer is being fired off, and when the timer loop is polling...and all the times seem accurate enough.

I have added Audio Session handling in my viewDidLoad() method as follows:

 [[AVAudioSession sharedInstance]
 setCategory: AVAudioSessionCategoryPlayback
 error: &setCategoryError];

Here is my timer loop that is running in it's own thread:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 

// Give the sound thread high priority to keep the timing steady.
[NSThread setThreadPriority:1.0];

while (running) {     // loop until stopped. 
    [self performSelectorOnMainThread:@selector(playOutput) withObject:nil waitUntilDone:NO];

    // An autorelease pool to prevent the build-up of temporary objects.
    NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];

    NSDate *curtainTime = [[NSDate alloc] initWithTimeIntervalSinceNow:sleepValue];
    NSDate *currentTime = [[NSDate alloc] init];
    // Wake up periodically to see if we've been cancelled. 
    while (running && ([currentTime compare:curtainTime] == NSOrderedAscending)) { 
        [NSThread sleepForTimeInterval:0.015];
        [currentTime release];
        currentTime = [[NSDate alloc] init];
    }

    [curtainTime release]; 
    [currentTime release]; 
    [loopPool drain];
}
[pool drain];

Here is how I am initializing my AVAudioPlayer:

NSString *beatOne = @"3h";
NSString *beatOneSoundFile = [[NSBundle mainBundle]
                              pathForResource:beatOne ofType:@"caf"];
NSURL *beatOneURL = [[NSURL alloc] initFileURLWithPath:beatOneSoundFile];
beatPlayerOne = [[AVAudioPlayer alloc]initWithContentsOfURL:beatOneURL error:nil];
beatPlayerOne.delegate = self;
beatPlayerOne.currentTime = 0;
[beatPlayerOne prepareToPlay];
[beatOneURL release];

And here is where the audio player is played:

[beatPlayerOne pause];
beatPlayerOne.currentTime = 0;
[beatPlayerOne play];

Thanks in advance for any help...

TheTwoNotes
  • 453
  • 3
  • 15
  • perhaps you could think of playing audio files containing say ten taps? – amergin Aug 22 '11 at 18:04
  • Ultimately, the interval between taps will be adjustable. It needs to be able to handle a max of 1000 taps per minute (higher if possible), but eventually, you approach a "quantum" issue of the wait time is too close to the smallest discernable amount of time (i.e. the millisecond). – TheTwoNotes Aug 22 '11 at 18:48

3 Answers3

2

NSTimer (and, relatedly, NSThread sleep) are not reliable "clock-timing" mechanisms. If something blocks the main thread, the run loop won't end until it unblocks, and timers and thread sleeps won't be called on time.

Also the resolution on NSTimer is at BEST about 1/30 of a second, though the framework makes no promises about the regularity of its firing. As you've seen, it's not regular. If your mail app fires up in the background, it's likely to throw all your fancy buzzing audio out of whack.

You'll be best off, I think, to generate longer sound files that contain various speeds of clicks, and fire them less frequently.

Dan Ray
  • 21,623
  • 6
  • 63
  • 87
0

You should always avoid [NSThread Sleep] as much as possible. Use NSTimer instead:

[NSTimer scheduledTimerWithTimeInterval:(0.015) target:self selector:@selector(timerfn) userInfo:nil repeats:NO];
//the @selector(timerfn) is which function it calls

recall this timer in the function it calls if you want to continue replaying the sound

OR

save the timer to a variable like so:

 mytimer=[NSTimer scheduledTimerWithTimeInterval:(0.015) target:self selector:@selector(timerfn) userInfo:nil repeats:YES];

and use:

 [mytimer invalidate];
    mytimer=nil;

to kill it.

Then just play the sound in the function. You could also use [NSTimer alloc] and [mytimer release] i think...

Also, 66.6 calls a second does seem somewhat overkill.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
apple16
  • 1,137
  • 10
  • 13
  • NSTimer is unfortunately very unreliable when it comes to precision. As with other languages (C++, Java, etc...) threading can be very useful, but you need to handle the level of accuracy internally. And I think 60ms turns out to be 16.66667 clicks per second. – TheTwoNotes Aug 25 '11 at 20:56
  • I did not know that. Thanks!, i was going off the 0.015 and not 60ms... so were both correct! – apple16 Aug 26 '11 at 19:16
0

As Dan Ray said, NSTimer and NSThread are not reliable for precise timing. If I were you, I would use an audio queue - it will request buffers of audio through an input callback, and you will fill them. Provided you respond promptly, it will request and reproduce these buffers at exactly the hardware sample rate, so you can do some simple calculations to figure out the timing of your taps and fill each buffer from your file or from samples stored in an array or vector. You'd fill it using AudioFileReadPackets if you choose to go straight from your file.

This will be a little more work but it's much more versatile and powerful. You can use audio queues for live synthesis and processing of audio, so their timing is perfectly precise as long as you meet their deadlines. If you're careful about processor usage, you could have a tap sound every few milliseconds with no issue.

Take a look at the SpeakHere sample application to get started with audio queues. Audio units are also useful for this sort of thing, and you can provide data to them through a callback, but they take a little more setup and have a steeper learning curve.

Good luck!

Luke
  • 7,110
  • 6
  • 45
  • 74
  • Thanks for all the good input. As it turns out, simpler is better. I was already aware of the short comings of the NSTimer, so my threaded timing loop I think will suffice. – TheTwoNotes Aug 25 '11 at 20:39