Precise time of audio queue playback finish

EDIT:

I found an easier way that gets the same results (+/-10ms). After you set up your output Queue with AudioQueueNewOutput() you initialize a AudioQueueTimelineRef to be used in your output callback. (ticksToSeconds function is included below in my first method) don't forget to import<mach/mach_time.h>

//After AudioQueueNewOutput()
AudioQueueTimelineRef timeLine;     //ivar
AudioQueueCreateTimeline(queue, self.timeLine);

Then in your output callback you call AudioQueueGetCurrentTime(). Caveat: queue must be playing for valid timestamps. So for very short files you might need to use the AudioQueueProcessingTap method below.

AudioTimeStamp timestamp;
AudioQueueGetCurrentTime(queue, self->timeLine, &timestamp, NULL);

The timestamp ties together the current sample playing with the current machine time. With that info we can get an exact machine time in the future when our last sample will be played.

Float64 samplesLeft    = self->frameCount - timestamp.mSampleTime;//samples in file - current sample
Float64 secondsLeft    = samplesLeft / self->sampleRate;          //seconds of audio to play
UInt64  ticksLeft      = secondsLeft / ticksToSeconds();          //seconds converted to machine ticks  
UInt64  machTimeFinish = timestamp.mHostTime + ticksLeft;         //machine time of first sample + ticks left 

Now that we have this future machine time we can use it to time whatever it is that you want to do with some accuracy.

UInt64 currentMachTime = mach_absolute_time();
Uint64 ticksFromNow = machTimeFinish - currentMachTime;
float secondsFromNow = ticksFromNow * ticksToSeconds();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsFromNow * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    //do the thing!!!
    printf("Giggety");
});

If GCD dispatch_async isn't accurate enough there are ways to set up a precision timer

Using AudioQueueProcessingTap

You can get fairly low response time from an AudioQueueProcessingTap. First you make your callback that will essentially put itself in-between the audio stream. The MyObject type is just whatever self is in your code(this is ARC bridging here to get self inside the function). Inspecting ioFlags tells you when the stream starts and finishes. The ioTimeStamp of an output callback describes time that the first sample in the callback will hit the speaker in the future. So if you want to get exact here's how you do it. I added some convenience functions for converting machine time to seconds.

#import <mach/mach_time.h>

double getTimeConversion(){
    double timecon;
    mach_timebase_info_data_t tinfo;
    kern_return_t kerror;
    kerror = mach_timebase_info(&tinfo);
    timecon = (double)tinfo.numer / (double)tinfo.denom;

    return  timecon;
}
double ticksToSeconds(){
    static double ticksToSeconds = 0;
    if (!ticksToSeconds) {
        ticksToSeconds = getTimeConversion() * 0.000000001;
    }
    return ticksToSeconds;
}

void processingTapCallback(
                 void *                          inClientData,
                 AudioQueueProcessingTapRef      inAQTap,
                 UInt32                          inNumberFrames,
                 AudioTimeStamp *                ioTimeStamp,
                 UInt32 *                        ioFlags,
                 UInt32 *                        outNumberFrames,
                 AudioBufferList *               ioData){

    MyObject *self = (__bridge Object *)inClientData;
    AudioQueueProcessingTapGetSourceAudio(inAQTap, inNumberFrames, ioTimeStamp, ioFlags, outNumberFrames, ioData);
    if (*ioFlags ==  kAudioQueueProcessingTap_EndOfStream) {
        Float64 sampTime;
        UInt32 frameCount;
        AudioQueueProcessingTapGetQueueTime(inAQTap, &sampTime, &frameCount);
        Float64 samplesInThisCallback = self->frameCount - sampleTime;//file sampleCount - queue current sample
        //double secondsInCallback = outNumberFrames / (double)self->sampleRate; outNumberFrames was inaccurate
        double secondsInCallback = * samplesInThisCallback / (double)self->sampleRate;
        uint64_t timeOfLastSampleLeavingSpeaker = ioTimeStamp->mHostTime + (secondsInCallback / ticksToSeconds());
        [self lastSampleDoneAt:timeOfLastSampleLeavingSpeaker];
    }
}

-(void)lastSampleDoneAt:(uint64_t)lastSampTime{
    uint64_t currentTime = mach_absolute_time();
    if (lastSampTime > currentTime) {
        double secondsFromNow = (lastSampTime - currentTime) * ticksToSeconds();
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsFromNow * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //do the thing!!!
        });
    }
    else{
        //do the thing!!!
    }
}

You set it up like this after AudioQueueNewOutput and before AudioQueueStart. Notice the passing of bridged self to the inClientData argument. The queue actually holds self as void* to be used in callback where we bridge it back to an objective-C object within the callback.

AudioStreamBasicDescription format;
AudioQueueProcessingTapRef tapRef;
UInt32 maxFrames = 0;
AudioQueueProcessingTapNew(queue, processingTapCallback, (__bridge void *)self, kAudioQueueProcessingTap_PostEffects, &maxFrames, &format, &tapRef);

You could get the end machine time as soon as the file starts too. A little cleaner too.

void processingTapCallback(
                 void *                          inClientData,
                 AudioQueueProcessingTapRef      inAQTap,
                 UInt32                          inNumberFrames,
                 AudioTimeStamp *                ioTimeStamp,
                 UInt32 *                        ioFlags,
                 UInt32 *                        outNumberFrames,
                 AudioBufferList *               ioData){

    MyObject *self = (__bridge Object *)inClientData;
    AudioQueueProcessingTapGetSourceAudio(inAQTap, inNumberFrames, ioTimeStamp, ioFlags, outNumberFrames, ioData);
    if (*ioFlags ==  kAudioQueueProcessingTap_StartOfStream) {

        uint64_t timeOfLastSampleLeavingSpeaker = ioTimeStamp->mHostTime + (self->audioDurSeconds / ticksToSeconds());
        [self lastSampleDoneAt:timeOfLastSampleLeavingSpeaker];
    }
}