2015-06-07 12 views
4

我的問題很自我解釋。對不起,如果它看起來太愚蠢。在iOS Audio Calling APP中使用循環緩衝區的原因是什麼?

我正在寫一個iOS VoIP撥號程序並檢查了一些開源代碼(iOS音頻通話應用程序)。幾乎所有的人都使用循環緩衝器來存儲已錄製和已接收的PCM音頻數據。所以我想知道爲什麼我們需要在這種情況下使用循環緩衝區。使用這種音頻緩衝區的確切原因是什麼?

在此先感謝。

+0

循環緩衝區避免了與分配和釋放緩衝區空間相關的開銷 - 這在諸如VoIP應用程序等應用程序中非常重要,在這種應用程序中,數據將在很短的時間內保留在緩衝區中,並且會有很多。不斷分配和釋放存儲空間也會導致堆碎片,這也會導致性能問題 – Paulw11

回答

2

好問題。使用循環緩衝區還有另一個好的理由。

在iOS中,如果您使用回調(音頻單元)進行錄製和播放音頻(實際上如果您想創建實時音頻傳輸應用程序,則需要使用它),那麼您將獲得一大塊數據從記錄器回調中獲得特定時間(比如20毫秒)。在iOS中,永遠不會獲得固定長度的數據(如果將回調間隔設置爲20ms,那麼您將獲得370或372字節的數據,並且您將永遠不知道何時將得到370字節或372字節。如果我錯了)。然後,要通過UDP數據包傳輸音頻,您需要使用編解碼器進行數據編碼和解碼(G729通常用於VoIP應用程序)。但g729以8的乘數獲取數據。假設您每20ms編碼368(8 * 46)個字節。那麼你將如何處理其餘的數據呢?您需要通過序列將其存儲爲,以便處理下一個塊。

所以這就是原因。還有一些其他細節,但我爲了更好的理解簡化了它。如果您有任何問題,請在下面評論。

12

使用循環緩衝區可以讓您從源中異步處理輸入和輸出數據。音頻渲染過程發生在高優先級的線程上。它要求從你的應用中播放音頻樣本(回放),並以回調的形式在定時器上提供音頻(記錄/處理)。

一個典型的情況是音頻回調每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是一篇很好的文章,解釋了實現循環緩衝區的簡潔內存技巧。

+0

非常好的解釋!我還有一個問題:如何調用讀取緩衝區尾部的回調?我的第一個方法是一個while(true)循環,當然,這會導致CPU使用率很高,並且會有很多新的緩衝區進入「主動等待」狀態。 –

+2

我只是使用普通的NSTimer。在這個例子中,我可以設置定時器每隔0.023秒觸發一次(儘管我個人不太經常這樣做),然後在該回調中調用myLeisurelySavingCallback函數。注意我如何計算函數中的samplesInBuffer?如果它是空的,它將等待下一次調用,如果它有一個或多個存儲在其中的樣本緩衝區,它將處理所有這些緩衝區。 – dave234