音频队列播放完成的准确时间
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, ×tamp, 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
仅在队列停止播放后才开始。另外samplesInThisCallback
和secondsInCallback
行有点令人困惑,对我来说不准确,您能否详细说明一下。例如,为什么要将此 double secondsInCallback = *
相乘,您声明了本地 frameCount
并且还向类 self->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 停止播放时提供更好的结果。
【讨论】:
以上是关于音频队列播放完成的准确时间的主要内容,如果未能解决你的问题,请参考以下文章