音频队列播放结束的准确时间

Precise time of audio queue playback finish

我正在使用音频队列来播放音频文件。我需要在最后一个缓冲区结束时精确计时。 我需要在播放完最后一个缓冲区后的 150 毫秒到 200 毫秒内通知函数...

通过回调方法我知道有多少缓冲区入队

我知道缓冲区大小,我知道最后一个缓冲区填充了多少字节。

首先我初始化了一些缓冲区并用音频数据填充缓冲区,然后将它们排入队列。当音频队列需要填充缓冲区时,它会调用回调,然后我用数据填充缓冲区。

当没有更多可用的音频数据时,音频队列向我发送最后一个空缓冲区,因此我用我拥有的任何数据填充它:

            if (sharedCache.numberOfToTalPackets>0)
            {
                if (currentlyReadingBufferIndex==[sharedCache.baseAudioCache count]-1) {
                    inBuffer->mAudioDataByteSize = (UInt32)bytesFilled;
                    lastEnqueudBufferSize=bytesFilled;
                    err=AudioQueueEnqueueBuffer(inAQ,inBuffer,(UInt32)packetsFilled,packetDescs);
                    if (err) {
                        [self failWithErrorCode:err customError:AP_AUDIO_QUEUE_ENQUEUE_FAILED];
                    }
                    printf("if that was the last free packet description, then enqueue the buffer\n");
                    //go to the next item on keepbuffer array
                    isBufferFilled=YES;
                    [self incrementBufferUsedCount];
                    return;
                }
            }

当音频队列通过回调请求更多数据而我没有更多数据时,我开始对缓冲区进行倒计时。如果缓冲区计数等于零,这意味着要播放的航班上只剩下一个缓冲区,播放完成后我会尝试停止音频队列。

-(void)decrementBufferUsedCount
{

    if (buffersUsed>0) {
        buffersUsed--;
        printf("buffer on the queue %i\n",buffersUsed);
        if (buffersUsed==0) {
            NSLog(@"playback is finished\n");
            // end playback
            isPlayBackDone=YES;
            double sampleRate = dataFormat.mSampleRate;
            double bufferDuration = lastEnqueudBufferSize/ sampleRate;
            double estimatedTimeNeded=bufferDuration*1;
            [self performSelector:@selector(stopPlayer) withObject:nil afterDelay:estimatedTimeNeded];
        }
    }
}  

-(void)stopPlayer
{
    @synchronized(self)
    {
        state=AP_STOPPING;
    }
    err=AudioQueueStop(queue, TRUE);
    if (err) {
        [self failWithErrorCode:err customError:AP_AUDIO_QUEUE_STOP_FAILED];
    }
    else
    {
        @synchronized(self)
        {
            state=AP_STOPPED;
            NSLog(@"Stopped\n");
        }

不过我好像不能在这里得到准确的时间。上面的代码提前停止播放器。

如果我也早点跟随音频剪辑

double bufferDuration = XMAQDefaultBufSize/ sampleRate;
double estimatedTimeNeded=bufferDuration*1;

如果将 1 增加到 2,因为缓冲区大小很大,我会有一些延迟,目前看来 1.5 是最佳值,但我不明白为什么 lastEnqueudBufferSize/ sampleRate 不工作

音频文件和缓冲区的详细信息:

Audio file has 22050 sample rate
#define kNumberPlaybackBuffers  4
#define kAQDefaultBufSize 16384
it is a vbr file format with no bitrate information available

如果您在 异步 模式下使用 AudioQueueStop,则在播放或记录所有排队的缓冲区后会停止。参见 doc

您正在 同步 模式下使用它,在这种模式下会尽快停止,并且播放会立即中断,而不考虑之前缓冲的音频数据。你想要精确的时间,但这只是因为音频被切断了。正确的?因此,与其采用同步方式 + 添加额外的 timing/callback 代码,我建议采用异步方式:

err=AudioQueueStop(queue, FALSE);

来自文档:

If you pass false, the function returns immediately, but the audio queue does not stop until its queued buffers are played or recorded (that is, the stop occurs asynchronously). Audio queue callbacks are invoked as necessary until the queue actually stops.

编辑:

我找到了一种更简单的方法来获得相同的结果 (+/-10ms)。使用 AudioQueueNewOutput() 设置输出队列后,您可以初始化 AudioQueueTimelineRef 以用于输出回调。 (ticksToSeconds 函数包含在我的第一个方法中)不要忘记导入<mach/mach_time.h>

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

然后在您的输出回调中调用 AudioQueueGetCurrentTime()。警告:队列必须播放有效的时间戳。因此,对于非常短的文件,您可能需要使用下面的 AudioQueueProcessingTap 方法。

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

时间戳将当前播放的样本与当前机器时间联系在一起。有了这些信息,我们就可以在将来播放最后一个样本时获得准确的机器时间。

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 

既然我们有了这个未来的机器时间,我们就可以用它来准确地为您想做的任何事情计时。

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");
});

如果 GCD dispatch_async 不够准确,可以通过多种方式设置 precision timer

使用 AudioQueueProcessingTap

您可以从 AudioQueueProcessingTap 获得相当短的响应时间。首先,您进行回调,基本上将其自身置于音频流之间。 MyObject 类型就是您代码中的任何 self(这是 ARC 桥接以将 self 置于函数内部)。检查 ioFlags 会告诉您流何时开始和结束。输出回调的 ioTimeStamp 描述回调中的第一个样本 在未来击中扬声器的时间。因此,如果您想获得准确的信息,这就是您的操作方法。我添加了一些将机器时间转换为秒的便利函数。

#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!!!
    }
}

在AudioQueueNewOutput之后,AudioQueueStart之前这样设置。注意将 bridged self 传递给 inClientData 参数。队列实际上将 self 保存为 void* 以在回调中使用,我们将其桥接回回调中的 objective-C 对象。

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

你也可以在文件启动时立即获得结束机器时间。也更干净一点。

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];
    }
}

对我来说,这对我所注意的事情非常有效:

  • 当数据结束时使用 AudioQueueStop(queue, FALSE) 在回调中停止队列,同时:
  • 使用 kAudioQueueProperty_IsRunning 属性 收听实际停止(发生在调用 AudioQueueStop() 之后,实际上,当最后一个缓冲区被实际渲染时)

停止队列后你可以为动作做好准备你需要在音频结束时执行,当听众触发时 - 实际执行这个动作。

我不确定该事件的时间精度,但对于我的任务来说,它的表现肯定比直接从回调中使用通知要好。 AudioQueue 和输出设备本身内部有缓冲,因此 IsRunning 侦听器在 AudioQueue 停止播放时肯定会提供更好的结果。