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

What's the reason of using Circular Buffer in iOS Audio Calling APP?

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

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

提前致谢。

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

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

原来如此。还有其他一些细节问题,但为了您更好地理解,我将其简化为简单。有问题就在下方评论吧。

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

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

循环缓冲区的作用是在线程之间传递数据。在将样本异步移入和移出音频线程的音频应用程序中。一个线程在缓冲区的 "head" 上生成样本,另一个线程从 "tail".

中使用它们

这是一个示例,从麦克风中检索音频样本并将它们写入磁盘。您的应用已订阅每 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 是一篇很好的文章,解释了实现循环缓冲区的简洁记忆技巧。