在上一篇文章中, Audio Worklet 會詳細介紹基本概念和使用方式。自 Chrome 66 版推出以來,許多人要求我們提供更多範例,說明如何在實際應用程式中加以運用。Audio Worklet 可讓您充分發揮 WebAudio 的潛力,但要善加利用這項技術可能並不容易,因為這項工具需要瞭解與多個 JS API 包裝的並行程式設計。即便開發人員熟悉 WebAudio,整合 Audio Worklet 與其他 API (例如 WebAssembly) 也並不容易。
這篇文章將引導讀者在實際設定中使用 Audio Worklet 方法,並提供發揮最大功效的提示。也請務必查看程式碼範例和現場示範!
重點回顧:音訊小程式
在深入探討之前,我們先快速回顧先前在這篇文章中介紹的音訊 Worklet 系統相關術語和事實。
- BaseAudioContext:Web Audio API 的主要物件。
- 音訊小程式:用於音訊 Worklet 作業的特殊指令碼檔案載入器。屬於 BaseAudioContext。一個 BaseAudioContext 可含有一個音訊小程式。載入的指令碼檔案會在 AudioWorkletGlobalScope 中評估,並用於建立 AudioWorkletProcessor 執行個體。
- AudioWorkletGlobalScope:音訊 Worklet 作業的特殊 JS 全域範圍。會在 WebAudio 的專用轉譯執行緒上執行。一個 BaseAudioContext 可含有一個 AudioWorkletGlobalScope。
- AudioWorkletNode:專為音訊 Worklet 作業設計的 AudioNode。從 BaseAudioContext 例項化。BaseAudioContext 可包含多個 AudioWorkletNode,方法與原生 AudioNode 相似。
- AudioWorkletProcessor: AudioWorkletNode 的對應部分。由使用者提供的程式碼處理音訊串流的 AudioWorkletNode 實際引數。建構 AudioWorkletNode 時,會在 AudioWorkletGlobalScope 中進行例項化。一個 AudioWorkletNode 可以有一個相符的 AudioWorkletProcessor。
設計模式
搭配 WebAssembly 使用 Audio Worklet
WebAssembly 是 AudioWorkletProcessor 的理想配套。搭配使用這兩項功能可為網路處理音訊帶來多種優勢,但兩者的兩個最大優點如下:a) 將現有的 C/C++ 音訊處理程式碼帶入 WebAudio 生態系統中,以及 b) 避免音訊處理程式碼中的 JS JIT 編譯和垃圾收集開銷。
如果您已投資音訊處理程式碼和程式庫,前者對開發人員來說非常重要,但後者對於幾乎所有 API 使用者而言都相當重要。在 WebAudio 的世界中,穩定音訊串流的時間預算相當耗費心力:取樣率為 44.1 Khz 時只有 3 毫秒。即使音訊處理程式碼有些中斷,也可能導致畫面故障。開發人員必須最佳化程式碼才能加快處理速度,同時盡量減少產生的 JS 垃圾。使用 WebAssembly 可以是可以同時解決這兩個問題的解決方案:速度較快,而且不會透過程式碼產生任何垃圾。
下一節將說明 WebAssembly 如何與 Audio Worklet 搭配使用,您可以在這裡找到隨附的程式碼範例。如需如何使用 Emscripten 和 WebAssembly (尤其是 Emscripten glue 程式碼) 的基本教學課程,請參閱這篇文章。
設定
聽起來都很棒,但我們需要一些架構才能正確設定。 第一個需要思考的設計問題,是將 WebAssembly 模組例項化的方式與位置。擷取 Emscripten 的膠水程式碼後,模組例項化作業會有以下兩種路徑:
- 透過
audioContext.audioWorklet.addModule()
將膠合程式碼載入 AudioWorkletGlobalScope,以將 WebAssembly 模組例項化。 - 在主要範圍內對 WebAssembly 模組執行個體化,然後透過 AudioWorkletNode 的建構函式選項轉移模組。
要做出決定的主要因素主要取決於您的設計和偏好,但這麼做的概念是 WebAssembly 模組可以在 AudioWorkletGlobalScope 中產生 WebAssembly 執行個體,而該執行個體會成為 AudioWorkletProcessor 執行個體中的音訊處理核心。
為了讓 A 模式正常運作, Emscripten 需要下列幾種選項,為設定產生正確的 WebAssembly glue 程式碼:
-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js
這些選項可確保 AudioWorkletGlobalScope 中的 WebAssembly 模組進行同步編譯。也會在 mycode.js
中附加 AudioWorkletProcessor 的類別定義,以便在模組初始化後載入。使用同步編譯的主要原因是,audioWorklet.addModule()
的承諾解析結果不會等待 AudioWorkletGlobalScope 中承諾的解析結果。一般來說,我們不建議在主執行緒中執行同步載入或編譯,因為這麼做會封鎖同一執行緒中的其他工作,但這裡可以略過規則,因為編譯作業是在主要執行緒外執行的 AudioWorkletGlobalScope 上進行。(詳情請參閱這裡)。
如果需要非同步的大量提升操作,模式 B 就非常實用。它利用主要執行緒從伺服器擷取膠合程式碼,並編譯模組。接著,系統會透過 AudioWorkletNode 的建構函式轉移 WASM 模組。在 AudioWorkletGlobalScope 開始轉譯音訊串流後,您必須以動態方式載入模組,這個模式會更加合理。視模組的大小而定,如果在算繪期間編譯這個模組,可能會導致串流出現異常情形。
WASM 堆積和音訊資料
WebAssembly 程式碼僅適用於分配於專屬 WASM 堆積中的記憶體。為了善加運用,您必須在 WASM 堆積和音訊資料陣列之間來回複製音訊資料。範例程式碼中的 HeapAudioBuffer 類別能順暢處理這項作業。
為了將 WASM 堆積直接整合到 Audio Worklet 系統,我們正在討論早期提案。移除 JS 記憶體與 WASM 堆積之間的這類冗餘資料複製作業看似自然,但具體細節必須多加運用。
處理緩衝區大小不相符的問題
AudioWorkletNode 和 AudioWorkletProcessor 組合是以一般 AudioNode 的形式運作;AudioWorkletNode 會處理與其他程式碼的互動,而 AudioWorkletProcessor 會負責處理內部音訊處理。由於 AudioNode 會一次處理 128 個影格,因此 AudioWorkletProcessor 必須執行此操作才能成為核心功能。這是 Audio Worklet 設計的優點之一,可避免因為在 AudioWorkletProcessor 中引入內部緩衝區而產生額外延遲,但若處理函式需要的緩衝區大小與 128 個影格不同,就會產生問題。常見的解決方案是使用環形緩衝區,也稱為環形緩衝區或 FIFO。
下圖顯示的 AudioWorkletProcessor 使用兩個環形緩衝區,可配合 WASM 函式,傳入及傳出 512 個影格。(此處的數字 512 經過任意挑選)。
圖表的演算法如下:
- AudioWorkletProcessor 會將 128 個影格從輸入內容推送到 Input RingBuffer。
- 只有在 Input RingBuffer 大於或等於 512 個影格時,才執行下列步驟。
- 從 Input RingBuffer 提取 512 個影格。
- 使用指定的 WASM 函式處理 512 個影格。
- 將 512 個影格推送至 Output RingBuffer。
- AudioWorkletProcessor 會從 Output RingBuffer 提取 128 個影格,來填入其輸出內容。
如圖所示,輸入框架一律會在 Input RingBuffer 中累積,並覆寫緩衝區中最舊的影格區塊,藉此處理緩衝區溢位。這對即時音訊應用程式來說是合理的做法。同樣地,系統一律會提取 Output frame 區塊。輸出 RingBuffer 中的緩衝區反向溢位 (資料不足) 會導致系統不停串流,進而造成串流出現故障。
以 AudioWorkletNode 取代 ScriptProcessorNode (SPN) 時,這個模式很有用。由於 SPN 可讓開發人員選擇 256 至 16384 影格之間的緩衝區空間,因此使用 AudioWorkletNode 取代 SPN 可能難以替換,而使用環環緩衝區可提供更理想的解決方法。以此設計為基礎建構的音訊錄音工具會是絕佳的範例。
然而,請務必瞭解此設計只會協調緩衝區大小不相符的問題,而且不會有更多時間執行特定指令碼程式碼。如果程式碼無法在轉譯量子的時間預算內完成工作 (以 44.1 Khz 為約 3 毫秒),就會影響後續回呼函式的啟動時間,最終造成異常終止。
由於 WASM 堆積需要管理記憶體,因此將這個設計與 WebAssembly 搭配使用可能會變得複雜。在寫入時,必須複製傳入及傳出 WASM 堆積的資料,不過我們可以使用 HeapAudioBuffer 類別簡化記憶體管理流程。未來將討論使用使用者分配的記憶體來減少冗餘資料複製的概念。
RingBuffer 類別請見這裡。
WebAudio Powerhouse:Audio Worklet 和 SharedArrayBuffer
本文的最後一項設計模式是將數個先進的 API 放在同一處:Audio Worklet、SharedArrayBuffer、Atomics 和 Worker。只要進行這項簡單的設定,就能讓使用 C/C++ 編寫的現有音訊軟體在網路瀏覽器中執行,同時維持順暢的使用者體驗。
此設計的最大優勢,就是只能單獨使用 DedicatedWorkerGlobalScope 進行音訊處理。在 Chrome 中,WorkerGlobalScope 的執行緒優先順序比 WebAudio 轉譯執行緒低,但與 AudioWorkletGlobalScope 有多項優點。專屬 WorkerGlobalScope 的 API 介面在範圍內也較不受限制。此外,由於 Worker API 已有多年的使用,因此 Emscripten 會提供更好的支援。
SharedArrayBuffer 是維持此設計效率的關鍵角色。雖然 Worker 和 AudioWorkletProcessor 都配備了非同步訊息傳遞功能 (MessagePort),但因為會重複記憶體配置和訊息傳遞延遲,這對即時音訊處理來說不太理想。因此,我們預先分配了一個記憶體區塊,並讓這兩個執行緒都能存取,以便快速雙向資料移轉。
從 Web Audio API 取得的觀點來看,這項設計可能不夠完善,因為這個設計使用音訊小程式做為簡單的「音訊接收器」,並在工作站中執行所有動作。但考量到使用 JavaScript 重寫 C/C++ 專案的成本是禁止的,甚至是不可能的,這種設計對這類專案來說可能是最有效率的實作路徑。
共用狀態與原子
使用共用記憶體處理音訊資料時,兩邊的存取都必須謹慎協調。共用能夠完整存取的狀態,就是解決這類問題的最佳解決方案。我們可以使用 SAB 支援的 Int32Array
來達到這個效果。
同步處理機制:SharedArrayBuffer 和 Atomics
狀態陣列的每個欄位都代表共用緩衝區的重要資訊。最重要的一個是同步處理作業 (REQUEST_RENDER
) 的欄位。概念是,工作站會等待 AudioWorkletProcessor 觸碰此欄位,並在喚醒時處理音訊。除了 SharedArrayBuffer (SAB) 外,Atomics API 也實現了這個機制。
請注意,兩個執行緒的同步處理作業其實是鬆散的。AudioWorkletProcessor.process()
方法會觸發 Worker.process()
的啟動作業,但 AudioWorkletProcessor 不會等到 Worker.process()
完成後才執行。這是出自設計:AudioWorkletProcessor 是由音訊回呼所驅動,因此不能同步封鎖。最糟糕的情況下,音訊串流可能會發生重複或退出的問題,但當轉譯效能穩定時,音訊串流最終會復原。
設定與執行
如上圖所示,這項設計有數個要安排的元件:uniqueWorkerGlobalScope (DWGS)、AudioWorkletGlobalScope (AWGS)、SharedArrayBuffer 和主執行緒。下列步驟說明在初始化階段應發生的情況。
初始化
- [主] 系統會呼叫 AudioWorkletNode 建構函式。
- 建立 worker。
- 系統將建立相關聯的 AudioWorkletProcessor。
- [DWGS] 工作站建立 2 個 SharedArrayBuffers。(一個用於共用狀態,另一種則用於音訊資料)
- [DWGS] 工作站將 SharedArrayBuffer 參照傳送至 AudioWorkletNode。
- [Main] AudioWorkletNode 會將 SharedArrayBuffer 參照傳送至 AudioWorkletProcessor。
- [AWGS] AudioWorkletProcessor 會通知 AudioWorkletNode 設定已完成。
初始化完成後,系統會開始呼叫 AudioWorkletProcessor.process()
。每次疊代轉譯迴圈時,應執行以下動作。
轉譯迴圈
- [AWGS] 每個轉譯量子都會呼叫
AudioWorkletProcessor.process(inputs, outputs)
。- 系統會將「
inputs
」推送至「Input SAB」。 - 系統會在 Output SAB 中使用音訊資料填入
outputs
。 - 根據相應的新緩衝區索引更新狀態 SAB。
- 如果「Output SAB」接近欠位門檻,Wake 工作站就會轉譯更多音訊資料。
- 系統會將「
- [DWGS] 工作站等待 (睡眠)
AudioWorkletProcessor.process()
的 Wake 訊號。喚醒時:- 從狀態 SAB 擷取緩衝區索引。
- 使用「Input SAB」中的資料執行程序函式,以填入「Output SAB」。
- 據此使用緩衝區索引更新狀態 SAB。
- 會進入睡眠並等待下一個訊號。
您可以在此找到範例程式碼,但請注意,必須啟用 SharedArrayBuffer 實驗旗標,此示範才能正常運作。為了方便起見,編寫這類程式碼時使用的是純 JS 程式碼,但也可以視需要改用 WebAssembly 程式碼。您應該在處理這類情況時,使用 HeapAudioBuffer 類別納入記憶體管理。
結論
Audio Worklet 的最終目標是讓 Web Audio API 具有「可擴充」特性。我們多年來投入了大量心力,設計出其餘的 Web Audio API 搭配 Audio Worklet。但現在我們的設計變得更加複雜,這會帶來意想不到的挑戰。
幸運的是,這類複雜性僅為開發人員提供強大支援。能夠在 AudioWorkletGlobalScope 上執行 WebAssembly, 發揮網路高效能音訊處理能力的巨大潛力對於以 C 或 C++ 編寫的大規模音訊應用程式而言,將 Audio Worklet 與 SharedArrayBuffers 和 Worker 搭配使用,可能是很有吸引力的選項。
抵免額
特別感謝 Chris Wilson、Jason Miller、Joshua Bell 和 Raymond Toy 閱讀本文的草稿,並提供精闢的意見回饋。