iOS 音频通话 APP 使用 Circular Buffer 的原因是啥?

Posted

技术标签:

【中文标题】iOS 音频通话 APP 使用 Circular Buffer 的原因是啥?【英文标题】:What's the reason of using Circular Buffer in iOS Audio Calling APP?iOS 音频通话 APP 使用 Circular Buffer 的原因是什么? 【发布时间】:2015-06-07 08:50:59 【问题描述】:

我的问题几乎是不言自明的。对不起,如果它看起来太愚蠢。

我正在编写一个 ios VoIP 拨号器并检查了一些开源代码(iOS 音频通话应用程序)。几乎所有这些都使用循环缓冲区来存储录制和接收的 PCM 音频数据。所以我想知道为什么在这种情况下我们需要使用循环缓冲区。使用这种音频缓冲区的确切原因是什么。

提前致谢。

【问题讨论】:

循环缓冲区避免了与分配和释放缓冲区空间相关的开销 - 这在诸如 VoIP 应用程序之类的应用程序中尤其重要,因为数据将在缓冲区中保留很短的时间并且会有很多其中。持续分配和释放存储也可能导致堆碎片,这也可能导致性能问题 【参考方案1】:

使用循环缓冲区可让您从源异步处理输入和输出数据。音频渲染过程发生在高优先级线程上。它从您的应用程序(播放)中请求音频样本,并以回调的形式在计时器上提供音频(录制/处理)。

一个典型的场景是音频回调每 0.023 秒触发一次,以请求(和/或提供)1024 个音频样本。该线程与系统硬件同步,因此您的回调必须在 0.023 秒结束之前返回。如果您不这样做,硬件将不会等待您,它只会跳过该循环,您将听到爆音或静音,或者错过您尝试录制的音频。

循环缓冲区的作用是在线程之间传递数据。在一个音频应用程序中,它将异步地将样本移入和移出音频线程。一个线程在缓冲区的“头部”产生样本,另一个线程从“尾部”消耗它们。

这是一个示例,从麦克风中检索音频样本并将其写入磁盘。您的应用已订阅每 0.023 秒触发一次的回调,提供 1024 个要记录的样本。天真的方法是简单地将音频从回调中写入磁盘。

void myCallback(float *samples,int sampleCount, SampleSaver *saver)
    SampleSaverSaveSamples(saver,samples,sampleCount);

这会奏效!!大多数时候...

问题在于,无法保证写入磁盘将在 0.023 秒之前完成,因此您的录音时不时地会弹出,因为 SampleSaver 只是花费了太长时间,而硬件只是跳过了下一个回调。

正确的做法是使用循环缓冲区。我个人使用TPCircularBuffer,因为它很棒。它的工作方式(外部)是在一个线程上向缓冲区请求将数据写入(头部)的指针,然后在另一个线程上向缓冲区请求要从(尾部)读取的指针。以下是使用 TPCircularBuffer 的方法(跳过设置并使用简化的回调)。

//this is on the high priority thread that can't wait for anything like a slow write to disk
void myCallback(float *samples,int sampleCount, TPCircularBuffer *buffer)
    int32_t availableBytes = 0;
    float *head = TPCircularBufferHead(buffer, &availableBytes);
    memcpy(head,samples,sampleCount * sizeof(float));//copies samples to head
    TPCircularBufferProduce(buffer,sampleCount * sizeof(float)); //moves buffer head "forward in the circle"


此操作非常快速,不会对敏感的音频线程施加额外压力。然后,您可以创建自己的计时器,并在一个单独的线程中将样本写入磁盘。

//this is on some background thread that can take it's sweet time
void myLeisurelySavingCallback(TPCircularBuffer *buffer, SampleSaver *saver)
    int32_t available;
    float *tail = TPCircularBufferTail(buffer, &available);
    int samplesInBuffer = available / sizeof(float); //mono 
    SampleSaverSaveSamples(saver, tail, samplesInBuffer);
    TPCircularBufferConsume(buffer, samplesInBuffer * sizeof(float)); // moves tail forward

你有它,你不仅可以避免音频故障,而且如果你初始化一个足够大的缓冲区,你可以将你的写入磁盘回调设置为每两秒触发一次(在循环缓冲区建立一个好一点的音频)在您的系统上比每 0.023 秒写入磁盘要容易得多!

使用缓冲区的主要原因是可以异步处理样本。它们也是在没有锁的线程之间传递消息的好方法。 Here 是一篇很好的文章,它解释了实现循环缓冲区的巧妙内存技巧。

【讨论】:

很好的解释!我还有一个问题:应该如何调用读取缓冲区尾部的回调?我的第一种方法是使用 while(true) 循环,这当然会导致 CPU 使用率非常高,并且需要大量“主动等待”以等待新缓冲区的进入。 我只使用一个普通的 NSTimer。在此示例中,我可能将计时器设置为每 0.023 秒触发一次(尽管我个人不太经常这样做),然后在该回调中调用 myLeisurelySavingCallback 函数。注意我是如何在函数中计算 samplesInBuffer 的?如果它是空的,它只会等到下一次调用,如果它有一个或多个样本缓冲区存储在其中,它将处理所有这些缓冲区。 或者在消费者的踏板上设置一个睡眠,比如每 1000 毫秒 sleep(1000)。而不是 while(true) 使它成为 while(!stop)。这使得: while(!stop) TPCircularBufferConsume(buffer, samplesInBuffer * sizeof(float)); sleep(1000); 不错的答案@dave234【参考方案2】:

好问题。使用循环缓冲区还有另一个很好的理由。

在 iOS 中,如果您使用回调(音频单元)来录制和播放音频(事实上,如果您想创建一个实时音频传输应用程序,您需要使用它),那么您将获得大量数据记录器回调的特定时间量(比如说 20 毫秒)。而在 iOS 中,你永远不会得到固定长度的数据(如果你将回调间隔设置为 20 毫秒,那么你会得到 370 或 372 字节的数据。你永远不知道什么时候会得到 370 字节或 372 字节。纠正我如果我错了)。然后,要通过 UDP 数据包传输音频,您需要使用编解码器进行数据编码和解码(G729 通常用于 VoIP 应用程序)。但是 g729 通过 8 的乘数获取数据。假设您每 20 毫秒编码 368(8*46) 个字节。那么你打算如何处理剩下的数据呢?您需要按顺序存储它以供下一个要处理的块。

所以这就是原因。还有一些其他细节的东西,但为了您更好地理解,我把它简单化了。如果您有任何问题,请在下方评论。

【讨论】:

您是如何使用 AudioUnit 获得 20 毫秒回调的?我很好奇你是怎么做到的。

以上是关于iOS 音频通话 APP 使用 Circular Buffer 的原因是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在 Agora.io 视频通话后 Unity VideoPlayer 音频中断

iOS linphone-iphone 没有视频只有音频

iOS 14 耳机调节功能,提升音乐和通话音频质量

如何在 URL 中嵌入 App ID 和 Channel ID (Agora.io)

iOS 上的 AVCapture 会话中没有音频

测试经验| 音视频通话相关app如何进行测试