環形緩衝區行為

環形緩衝區用於在各方 (通常位於不同程序中) 之間傳送音訊,可同時非同步存取資料,不需使用鎖定。這個模式之所以有效,是因為各方都瞭解哪些緩衝區可安全存取,以及這些區域如何隨時間變化。

以下說明音訊資料影格如何在各方之間移動。舉例來說,我們詳細說明瞭 (1) 音訊從「用戶端」傳輸到「驅動程式」 (在該驅動程式庫的硬體上播放),以及 (2) 音訊從「驅動程式」傳輸到「用戶端」 (從該驅動程式庫程式的硬體錄製)。不過,環形緩衝區可用於在兩個應用程式層級的用戶端之間,或在兩個驅動程式之間傳送音訊。

部分音訊硬體可將資料傳輸至/自系統記憶體,無需軟體 (例如以主機為基礎的驅動程式庫) 參與;其他音訊硬體則透過硬體本身執行的軟體 (例如 DSP 韌體) 完成這項作業。不過,為求一致,我們之後會將音訊資料的這項移動作業稱為由「驅動程式庫」執行。如要進一步瞭解這項差異,請參閱本文結尾的「硬體與軟體」一節。

環形緩衝區使用者不需要互斥鎖或其他主動同步處理,因為他們共用三項重要資訊。第一個是環形緩衝區本身的記憶體界限。第二個是音訊的產生和消耗速率,這個速率是由 CreateRingBuffer 指令中指定的格式定義。第三,雙方必須瞭解環形緩衝區的開始時間

如果環形緩衝區尚未啟動,時間不會影響環形緩衝區的狀態。啟動循環緩衝區後,循環緩衝區位置會以預先定義格式設定的固定速度,持續在循環緩衝區中移動。根據定義,在「開始時間」,這個環形緩衝區位置「R」會從環形緩衝區的開頭開始,也就是從影格 0 開始。

啟動環形緩衝區時,驅動程式庫在單一 I/O 作業中可進行的資料傳輸大小會受到限制。如果是播放 (驅動程式庫為 Consumer,用戶端為 Producer),驅動程式庫會以最大 driver_transfer_bytes 的傳輸量耗用音訊影格。如果是擷取 (驅動程式庫為 Producer,用戶端為 Consumer),驅動程式庫會在傳輸中產生音訊影格,大小可達 driver_transfer_bytes

這些驅動程式庫資料轉移作業表示,環狀緩衝區中總有一部分不安全,無法供用戶端寫入 (或讀取,如果環狀緩衝區用於擷取)。這個不安全緩衝區區域的一側是由目前的環形緩衝區位置「R」定義,另一側則是由「安全指標」位置定義。視環形緩衝區用於播放或擷取而定,這分別是「Producer 的安全影格位置,可供寫入」(「P」) 或「Consumer 的安全影格位置,可供讀取」(「C」)。下圖將這些指標標示為「R」、「P」、「C」。

如要播放,用戶端不得在當時寫入「R」和「P」之間的區域。擷取時,不得讀取「C」和「R」之間的區域。

環形緩衝區啟動後,這些指標就會開始以固定速率移動。「R」會從 RingBuffer::Start 傳回的 start_time 開始移動,從較低的位址移至較高的位址,並在到達環形緩衝區結尾時立即重新開始。在適當的時間長度過後,消費者就能安全讀取先前由生產者寫入的環形緩衝區內容。

如要透過環形緩衝區傳遞音訊,Producer 必須在 Consumer 轉移發生前寫入資料。反之,消費者必須在生產者轉移資料後讀取資料。因此,'P' (我們在播放期間定義) 一律會早於 'R',而 'C' (我們在擷取期間定義) 一律會晚於 'R'。換句話說,「P」是指比「R」參照的影格更早的影格,「C」是指比「R」參照的影格更晚的影格。特定影格會先以「P」表示,再以「R」表示,最後以「C」表示。在下圖中,環形緩衝區影格 0 位於左側,環形緩衝區位置則從左向右移動。因此從左到右看時,我們預期圖表會顯示「C」、「R」和「P」(模數是環繞效果)。

播放

開始環形緩衝區之前,播放用戶端可以安全地寫入任何環形緩衝區位置。因此,目前尚未定義「P」。

                                 Ring Buffer
+-----------------------------------------------------------------------------+
[<--                             safe to write                             -->)
[             (to pre-populate the ring buffer before starting it)            )
+-----------------------------------------------------------------------------+
0=R                                                                           0

為盡快播放音訊,用戶端應在環形緩衝區開始時,將第一批音訊影格寫入環形緩衝區的開頭,這樣驅動程式庫就會先讀取這些影格。如果用戶端有足夠的音訊 (可能是整個音訊檔案),可能會選擇在啟動前預先填入整個循環緩衝區。其他用戶端會以即時串流的形式接收音訊;這些用戶端仍可預先填入環形緩衝區開頭的音訊,但必須寫入超過 driver_transfer_bytes 的音訊,因為驅動程式庫可能會在 Start 時立即從環形緩衝區取用這麼多資料。也就是說,用戶端必須在驅動程式讀取該影格 (下方標示為「s」) 之前,繼續從該影格寫入音訊 (在「P」到達該影格之前)。

                               Ring Buffer
+-------------------------+---------+-----------------------------------------+
[<-- Pre-populated by the client -->)      not yet written by the client      )
[< driver_transfer_bytes >)                                                   )
+-------------------------+---------+-----------------------------------------+
0=R                                 s                                         0

如果用戶端無法預先填入足夠的音訊,就應從偏移量開始音訊,而非從環形緩衝區開頭。這項作業會依據 VMO 的歸零內容,做為驅動程式庫讀取的第一個音訊。如上所述,這個偏移 (同樣稱為「s」) 必須足夠,才能讓用戶端在驅動程式庫耗用音訊影格前,提供後續音訊影格。例如:

                               Ring Buffer
+-------------------------+---------+-----------------------------------------+
[    Offset   [<-- Pre-populated -->)      not yet written by the client      )
[< driver_transfer_bytes >)                                                   )
+-------------------------+---------+-----------------------------------------+
0=R                                 s                                         0

啟動環形緩衝區後,用戶端就無法在「R」和「P」之間將資料寫入環形緩衝區,因為這代表資料已在使用中 (可能已由驅動程式庫耗用)。用戶端可以安全地寫入環形緩衝區的其餘部分 (介於「P」和「0/R」之間)。

如常,用戶端絕不應靠近「P」寫入,因為這是即時的假設指標,即使是單一 CPU 指令延遲,也可能導致指標前進。用戶端的「可安全寫入」有效區域會不斷變化,因為「P」會持續移動。因此,用戶端應「預先」 (在較高的記憶體位址) 寫入資料,確保有足夠時間在「P」之前寫入更多資料。

這是播放環形緩衝區的狀態,啟動時的狀態如下:

                               Ring Buffer
+-------------------------+---------------------------------------------------+
[<--  unsafe to write  -->[<--           safe to write (not yet            -->)
[< driver_transfer_bytes >[              consumed by the driver)              )
+-------------------------+---------------------------------------------------+
0=R                       P                                                   0

隨著時間經過,驅動程式庫會以 CreateRingBuffer 中指定的速率,讀取 driver_transfer_bytes 或更少的資料區塊。許多驅動程式會使用「乒乓」模式,一次讀取分配到的環形緩衝區區域的一半,以便安全地進行這些讀取作業。無論驅動程式庫轉移的大小為何,位置和安全指標 (「R」和「P」) 都會以相同速率向右移動,但會平穩移動。因此,「不安全供用戶端寫入」區域會逐漸通過環形緩衝區,同時維持等於 driver_transfer_bytes 的固定大小。因此,經過一段時間後,我們現在有:

                               Ring Buffer
+------------+-------------------------+--------------------------------------+
[<-- safe -->[<--  unsafe to write  -->[<--     safe to write (not yet     -->)
[  to write  [< driver_transfer_bytes >[        consumed by the driver)       )
+------------+-------------------------+--------------------------------------+
0            R                         P                                      0

稍後,'P' 會在 'R' 之前環繞環形緩衝區。請注意,從 0 到「P」的區域,加上從「R」到環形緩衝區結尾的區域,加總為 driver_transfer_bytes

                               Ring Buffer
+---------------+--------------------------------------------------+----------+
[<--  unsafe -->[<--        safe to write (to overwrite         -->[<-unsafe->)
[ransfer_bytes >[              already-consumed data               [< driver_t)
+---------------+--------------------------------------------------+----------+
0               P                                                  R          0

在穩定狀態下 (即程序已環繞環形緩衝區),用戶端可安全地寫入任何大於或等於「P」的影格 (上限為「R + ring_buffer_size」)。重新說明,並將環形緩衝區的環繞納入考量,Producer 可以安全地寫入範圍 [0, R) + [P, ring_buffer_size),或範圍 [P, R),視「R」相對於環形緩衝區環繞點的位置而定,如上圖或 (更常見的) 下圖所示:

                               Ring Buffer
+--------------------------+-------------------------+------------------------+
[<--   safe to write    -->[<--  unsafe to write  -->[<--   safe to write  -->)
[                          [< driver_transfer_bytes >[                        )
+--------------------------+-------------------------+------------------------+
0                          R                         P                        0

請注意邊界規定:「Producer 無法安全寫入」的區域是 [R, P),因此 Producer 無法安全寫入位置「R」(這等同於「R + ring_buffer_size」,也就是 Producer 的高水位位置)。同樣地,「可供 Producer 寫入」區域是 [P, R) (會換行),因此 Consumer 無法安全讀取位置「P」。

但實際上,任何一方都不應存取該確切影格。 框架指標位置「P」和「R」是理論上的瞬時位置。當驅動程式庫從「R」讀取資料時,該指標會稍微移動,導致該位置不適合讀取;當用戶端寫入「P」時,該指標會稍微移動,導致該位置不適合寫入。Producer 和 Consumer 必須一律在「安全」指標位置前方維持安全緩衝區。

驅動程式庫指定的 driver_transfer_bytes 值至關重要,可確保用戶端不會在驅動程式庫仍在主動讀取時寫入記憶體。如上所述,使用「乒乓」模式時,驅動程式庫會為 driver_transfer_bytes 指定的值,是實際傳輸大小的兩倍。這確實會反映內部雙緩衝區的大小,該緩衝區提供額外的安全緩衝時間長度。

錄音

錄音時,用戶端只能讀取驅動程式庫未同時寫入的環形緩衝區部分,在擷取開始前,驅動程式庫尚未寫入任何供用戶端讀取的內容。

擷取作業開始時 (由 RingBuffer::Start 回報),驅動程式庫無法立即將影格傳輸至環形緩衝區,因為這些影格尚未取得。驅動程式必須先累積足夠的影格才能進行轉移,之後才會從影格「0」開始,將該數量移至環形緩衝區。許多驅動程式會使用雙緩衝區 (或「乒乓」) 模式,在每次傳輸時傳輸一半的緩衝區量。由於在「開始」時,用戶端尚未讀取任何音訊影格,因此「C」實際上是未定義。不過,請考慮位置「b」(會變成「C」)。這個「b」會以固定偏移量延遲影格「R」,且尚未到達影格位置 0。以下是啟動時的環形緩衝區狀態:

                               Ring Buffer
+---------------------------------------------------+-------------------------+
[<--         safe to read (but empty, not yet written by driver)           -->)
[                                                   [< driver_transfer_bytes >)
+---------------------------------------------------+-------------------------+
0=R                                                 b                         0

啟動循環緩衝區後,但在「R」前進 driver_transfer_bytes 之前,用戶端還無法安全讀取任何新擷取的影格,因為這些影格可能尚未轉移至循環緩衝區。雖然「R」正在前進,但驅動程式庫可能尚未將任何資料傳輸到緩衝區。採用「乒乓」模式時,驅動程式庫會等到內部緩衝區的前半部填滿,再將內容轉移至環形緩衝區。轉移期間,內部緩衝區的後半部仍可安全接收後續影格。

實際擷取到環形緩衝區的音訊量會隨著每次驅動程式庫轉移而改變,因此會以「塊狀」方式在環形緩衝區中移動。相反地,根據定義,「R」和「C」會以完美平滑的方式移動;保證一律繫結在實際最近擷取的影格所在位置。

此時,由於「R」尚未前進 driver_transfer_bytes,「C」仍處於未定義狀態。標記「b」會持續前進,以固定偏移量落後於「R」,且尚未達到 0:

                               Ring Buffer
+--------------+--------------------------------------------------+-----------+
[<-- unsafe -->[<--           empty, not yet written by driver             -->)
[ansfer_bytes >)                                                  [< driver_tr)
+--------------+--------------------------------------------------+-----------+
0              R                                                  b           0

環形緩衝區位置「R」精確前進 driver_transfer_bytes 後,驅動程式庫保證至少會將初始音訊影格傳輸到環形緩衝區。採用「乒乓」模式時,驅動程式庫先前已將前半部 (「乒」) 緩衝區傳輸至環形緩衝區,後半部 (「乓」) 緩衝區則剛填滿,現在可以寫入環形緩衝區。位置「b」已到達環形緩衝區的開頭,因此現在定義了「C」,並開始以與「R」相同的速率平穩前進 (由環形緩衝區的影格速率和樣本格式決定)。因此,此時我們有:

                               Ring Buffer
+-------------------------+---------------------------------------------------+
[<--       unsafe      -->[<--       empty, not yet written by driver      -->)
[< driver_transfer_bytes >[                                                   )
+-------------------------+---------------------------------------------------+
0=C                       R                                                 b=0

隨著環形緩衝區位置「R」進一步前進,用戶端可以安全地讀取「0」和「C」之間的影格。用戶端從「C」到「R」讀取資料並不安全,因為驅動程式庫同時會寫入資料。這個區域會在環形緩衝區中移動,並維持 driver_transfer_bytes 的固定大小。概念上,環形緩衝區現在處於以下狀態:

                               Ring Buffer
+--------------------+-------------------------+------------------------------+
[<   safe to read   >[<--  unsafe to read   -->[<--     empty, not yet     -->)
[newly-captured audio[< driver_transfer_bytes >[       written by driver      )
+--------------------+-------------------------+------------------------------+
0                    C                         R                              0

稍後,'R' 會在 'C' 之前環繞環形緩衝區。請注意,從 0 到「R」的區域,加上從「C」到環形緩衝區結尾的區域,總共加起來是 driver_transfer_bytes

如常,用戶端絕不應讀取「R」接近的位置,因為這是假設的即時指標,即使是單一 CPU 指令延遲,也可能導致指標前進。用戶端的「可安全讀取」有效區域會不斷變化,因為「R」會持續移動。因此,用戶端應「預先」讀取 (位於較高的記憶體位址),確保有足夠時間在「R」之前讀取更多資料。

這是環形緩衝區的狀態,在第一次環繞後的一段時間:

                               Ring Buffer
+-----------+--------------------------------------------------+--------------+
[<--unsafe->[<--                safe to read                -->[<-- unsafe -->)
[fer_bytes >[                 (captured audio)                 [< driver_trans)
+-----------+--------------------------------------------------+--------------+
0           R                                                  C              0

在穩定狀態下 (也就是程序已環繞環形緩衝區),任何「C」較少的影格 (最多可達「R - ring_buffer_size」的限制) 都可供用戶端安全讀取。重新說明,並將環繞式包裝納入考量後,消費者可以安全地讀取範圍 [0, C) + [R, ring_buffer_size),或範圍 [R, C) (視「R」相對於環繞式包裝點的位置而定) - 上方圖表或 (更常見) 下方圖表:

                               Ring Buffer
+--------------------------+-------------------------+------------------------+
[<--    safe to read    -->[<--      unsafe       -->[<--   safe to read   -->)
[                          [< driver_transfer_bytes >[                        )
+--------------------------+-------------------------+------------------------+
0                          C                         R                        0

請注意邊界規定:「消費者無法安全讀取」的區域是 [C, R),因此消費者無法安全讀取位置「C」(消費者低水位框架位置)。同樣地,「消費者可安全讀取」的區域是 [R, C) (含環繞),因此 Producer 無法安全地寫入位置「R」。

但實際上,任何一方都不應存取該確切影格。 框架指標位置「R」和「C」是理論上的瞬時位置。當驅動程式庫寫入「C」時,該指標會稍微移動,導致該位置不適合寫入;當用戶端從「R」讀取時,該指標會稍微移動,導致該位置不適合讀取。Producer 和 Consumer 必須一律在「安全」指標位置前方維持安全緩衝區。

驅動程式庫指定的 driver_transfer_bytes 值非常重要,可確保用戶端不會讀取驅動程式庫仍在主動更新的記憶體。如上所述,使用「乒乓」模式時,驅動程式庫會為 driver_transfer_bytes 指定的值,是實際傳輸大小的兩倍。這確實會反映其內部雙緩衝區的大小,該緩衝區提供額外的安全緩衝時間長度。

硬體與軟體 (或硬體轉移與驅動程式庫程式處理和複製)

音訊硬體可直接取用/產生環形緩衝區資料框架:也就是說,driver_transfer_bytes 可能會直接對應至硬體 FIFO 區塊的大小,因為該 FIFO 區塊會決定預先讀取或保留的資料量上限。請注意,如果 FIFO 緩衝區是以傳統的「高水位」方式使用 (例如「乒乓」設計,任何時間只會使用一半的 FIFO,在第一次填滿整個 FIFO 後),則 driver_transfer_bytes 會設為內部 FIFO 緩衝區的大小,如果使用「乒乓」模式,則會是內部傳輸大小的兩倍。Start即使使用較小的傳輸量,如果使用 FIFO 的完整大小 (例如在填入最初空白的硬體 FIFO 時),則 driver_transfer_bytes 必須設為這個 FIFO 緩衝區的完整大小。Start

音訊驅動程式庫軟體可能會改為取用/產生環形緩衝區資料,這類軟體在概念上位於環形緩衝區和音訊硬體之間。以播放為例,driver_transfer_bytes 預先讀取的資料量必須夠大,驅動程式庫才能根據用戶端需求,保證不會發生未偵測到的緩衝區不足情況,並以 CreateRingBuffer 指定的速率產生資料,以及在 Startstart_time 衍生位置產生資料。反之,對於擷取作業,driver_transfer_bytes 必須夠大,才能確保驅動程式庫在產生資料時不會發生欠載情形,這取決於 CreateRingBufferStart。此外,由於必須納入任何安全邊界,以因應排程和執行這項驅動程式庫處理作業時的延遲,因此預期在這些情況下,driver_transfer_bytes 會大於傳輸本身的大小。