音频队列播放完成的准确时间

Posted

技术标签:

【中文标题】音频队列播放完成的准确时间【英文标题】:Precise time of audio queue playback finish 【发布时间】:2015-05-21 15:11:29 【问题描述】:

我正在使用音频队列来播放音频文件。我需要在最后一个缓冲区完成时精确计时。 我需要在播放完最后一个缓冲区后不迟于 150ms-200ms 通知一个函数...

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

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

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

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

            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

【问题讨论】:

你试过我下面贴的方法了吗?您可以准确预测最后一个样本何时离开扬声器。 【参考方案1】:

编辑:

我找到了一种更简单的方法,可以得到相同的结果 (+/-10ms)。使用 AudioQueueNewOutput() 设置输出队列后,您初始化 AudioQueueTimelineRef 以在输出回调中使用。 (ticksToSeconds 函数包含在我的第一个方法中)不要忘记 import<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 之前这样设置它。请注意将桥接的 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];
    

【讨论】:

我正在做类似double estimatedTimeNeded=(lastbufferPacketCount*dataFormat.mFramesPerPacket)/dataFormat.mSampleRate; 的操作,它会为您的float secondsFromNow = ticksFromNow * ticksToSeconds(); 提供接近的结果,但是当缓冲区大小较小时float secondsFromNow = ticksFromNow * ticksToSeconds(); 不够准确,因为出于某种原因正在快速播放音频。当我选择非常大的缓冲区大小并应用您的第一个解决方案时,它似乎有些准确,但我需要对其进行测试。 processingTapCallback if 条件 kAudioQueueProcessingTap_EndOfStream 仅在队列停止播放后才开始。另外samplesInThisCallbacksecondsInCallback 行有点令人困惑,对我来说不准确,您能否详细说明一下。例如,为什么要将此 double secondsInCallback = * 相乘,您声明了本地 frameCount 并且还向类 self-&gt;frameCount 提问?你指的是哪个帧数? 没有使用局部变量frameCount,它只是AudioQueueProcessingTapGetQueueTime()函数需要的,但它指的是回调中调用的样本数。我需要从函数中获取的值是sampleTime,它指的是ioTimeStamp 处的播放位置(以帧为单位)。 self->frameCount 的值是指音频文件中的总帧数,我用它来获取缓冲区中剩余的样本数。您可以将所有这些逻辑拉到 if (*ioFlags == kAudioQueueProcessingTap_EndOfStream) 之外,它也可以工作。 对于第一种方法,AudioQueueGetCurrentTime()不需要在回调中调用,唯一的要求是音频正在播放,所以它实际上适用于短文件,它会比你上面发布的方法准确得多。如果您喜欢第二种方法,您可以做的另一件事是将 kAudioQueueProcessingTap_EndOfStream 更改为 kAudioQueueProcessingTap_StartOfStream ,它将在音频开始时计算最后一个采样时间。基本上,一旦您获得与机器时间相关的采样时间,您只需进行数学运算。 我认为processingTapCallback 是一个很好的解决方案,我按照你说的稍微改变了逻辑,它似乎工作正常。【参考方案2】:

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

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

err=AudioQueueStop(queue, FALSE);

来自文档:

如果传入false,函数立即返回,但音频 队列在播放或记录其排队缓冲区之前不会停止 (即,停止异步发生)。音频队列回调是 必要时调用,直到队列实际停止。

【讨论】:

好的答案,但音频中断不是我的确切问题,问题是能够告诉最后一个缓冲区在毫秒内完成播放,所以我可以在完成的那一刻做一个重要的动作,我故意使用AudioQueueStop(queue, TRUE); 来证明我的计算不起作用 好的,我明白了。听起来您想要实际最后一个样本通过输出单元(并产生声音)的精确时间(挂钟),而不是之前任何时间的时间,通过音频管道。是对的吗?你的 Q 回调情况是什么样的,你有一个精简的项目要运行吗?我的想法是一个渲染回调来观察/计算飞过的样本,但需要更多的上下文。【参考方案3】:

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

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

停止队列后,您可以为动作做好准备您需要在音频结束时执行,并且当侦听器触发时 - 实际执行此动作。

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

【讨论】:

以上是关于音频队列播放完成的准确时间的主要内容,如果未能解决你的问题,请参考以下文章

在准确的时间停止音频播放

播放流后音频队列无法录制音频

Objective-C - 将流数据传递到音频队列

从后台播放音频时出错。音频队列错误 12985 [重复]

iOS音频播放

如何在 AVAudioPlayer 开始播放音频时准确获取通知?