0

Hi !

I'm building a timer using GCD for the purpose of playing a sound at a specific interval, to be more precise, it's a metronome sound. I've been trying for days to solve my issue but nothing. Everything is good but when I set my tempo to a bigger value , let's say 150 bpm or 200 bpm, when the sound starts for the first time, it fires very quickly(almost like two sounds in the same time meaning it does not have the expected interval) and after this , it calibrates. I start the sound the second time , all is good... so this happens only the first time I resume my dispatch source so I'm guessing it has something to do with loading the sound from the disk , like in this post : Slow start for AVAudioPlayer the first time a sound is played . For my sound I used at first an instance of AVAudioPlayer with prepareToPlay and play and also created it in the AppDelegate class, it hasn't work...I have even tried the SoundManager class developed by @NickLockwood,same issue. At present, I'm using a SystemSoundID. As for the timers, this is my first GCD timer , I've already tried the classical NSTimer, CADisplayLink and other timers found on git... all in vain.

Another interesting issue is that with the other timers , everything is perfect on the simulator but on the device the same glitch.

Here's the code, I hope someone will bring me to the light.

 -(void)playButtonAction  // 
    {
        if (_metronomeIsAnimatingAndPLaying == NO)
        {
            [self startAnimatingArm]; // I start my animation and create my timer
           
            metronomeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
           
            dispatch_source_set_timer(metronomeTimer,dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC),duration * NSEC_PER_SEC,duration *NSEC_PER_SEC);
            
            dispatch_source_set_event_handler(metronomeTimer, ^{[self playTick];});
            
            dispatch_resume(metronomeTimer);
     
            _metronomeIsAnimatingAndPLaying = YES;
        }
   
    }

-(void)playTick
{
   AudioServicesPlaySystemSound(appDeleg.soundID); // soundID is created in appDelegate
} 

In my application didFinishLaunching

NSString *path = [[NSBundle mainBundle] pathForResource:@"tick"
                                                 ofType:@"caf"];
AudioServicesCreateSystemSoundID((CFURLRef)[NSURL fileURLWithPath:path]
                                 , &_soundID);

And BPM setter and getter :

- (NSUInteger)bpm
{
    return round(60.0 / duration);
}

- (void)setBpm:(NSUInteger)bpm
{
    if (bpm >= MaxBPM) {
        bpm = MaxBPM;
    } else if (bpm <= MinBPM) {
        bpm = MinBPM;
    }
    duration = (60.0 / bpm);

}
Community
  • 1
  • 1
QuiBongJin
  • 171
  • 1
  • 9

1 Answers1

5

This arrangement will fundamentally never work.

GCD is a thread-pool designed to facilitate task-level parallelism. It is usually asynchronous and non real-time. These are almost precisely the opposite characteristics to those required in an audio application.

Each thread servicing a GCD queue is contending with other threads in the system for an opportunity to execute. Furthermore, the queue may be busy at requested time processing something else. If that something else is long-running - and long-running tasks are precisely the kind of thing that GCD is made for - the scheduler may pre-empt the thread before the operation has completed and penalise the queue; it may wait a long time for service.

The Manpage for GCD states the following about timers on GCD queues:

A best effort attempt is made to submit the event handler block to the target queue at the specified time; however, actual invocation may occur at a later time.

NSTimer will not be any better. Its documentation states A timer is not a real-time mechanism. Since you'll probably run this on the application's main run-loop, it will also be very unpredictable.

The solution to this problem is to use lower-level audio APIs - specifically Audio Units. The advantage of doing so is that soft-syth units have an event queue which is serviced by the unit's render handler. This runs on a real-time thread, and offers extremely robust and predictable service. Since you can queue a considerable number of events with timestamps in the future, your timing requirements are now quite loose. You could safely use either GCD or a NSTimer for this.

marko
  • 9,029
  • 4
  • 30
  • 46