音訊小程式現已預設使用

Hongchan Choi

Chrome 64 推出備受期待的 Web Audio API 新功能 - AudioWorklet。本課程將說明概念和使用方式 自訂音訊處理器和 JavaScript 程式碼查看 現場示範。 系列的下一篇文章「音訊工作程式設計模式」 認為建構先進的音訊應用程式可能會很有趣

背景:ScriptProcessorNode

Web Audio API 的音訊處理作業會在主執行緒以外的獨立執行緒中執行 UI 執行緒,讓執行緒可以順暢運作。如何在以下位置啟用自訂音訊處理功能: JavaScript,Web Audio API 提議使用 ScriptProcessorNode 事件處理常式,在主 UI 執行緒中叫用使用者指令碼。

此設計中有兩個問題:事件處理為非同步 且程式碼會在主執行緒上執行。前 後者會使得延遲時間變慢,如果後者造成 經常擁擠的 UI 與 DOM 相關任務,導致使用者介面 「jank」或音訊轉換成「毛刺」。正因為這種基本設計瑕疵 ScriptProcessorNode 已不適用本規格, 替換為 AudioWorklet。

概念

音訊小程式會儲存使用者提供的 JavaScript 程式碼,所有 音訊處理執行緒。這意味著 無須跳轉至 處理音訊這表示使用者提供的指令碼程式碼可以執行 在音訊轉譯執行緒 (AudioWorkletGlobalScope) 與其他 內建 AudioNodes,可確保零延遲和同步程序 算繪。

主要全域範圍和音訊小程式範圍圖表
Fig.1

註冊與建立例項

使用 Audio Worklet 包含兩個部分:AudioWorkletProcessorAudioWorkletNode。這比使用 ScriptProcessorNode 還要複雜 但必須讓開發人員擁有低階自訂音訊的功能 和資料處理之間AudioWorkletProcessor 代表實際的音訊處理器 ,且位於 AudioWorkletGlobalScope 中。 AudioWorkletNodeAudioWorkletProcessor 的對應部分,取用 參照主執行緒中其他 AudioNodes 的連線。這項服務 會顯示在主要全域範圍和函式中,就像一般的 AudioNode 一樣。

以下為說明註冊的程式碼片段,以及 建立例項。

// The code in the main global scope.
class MyWorkletNode extends AudioWorkletNode {
  constructor(context) {
    super(context, 'my-worklet-processor');
  }
}

let context = new AudioContext();

context.audioWorklet.addModule('processors.js').then(() => {
  let node = new MyWorkletNode(context);
});

如要建立 AudioWorkletNode,您必須新增 AudioContext 物件和處理器名稱做為字串。處理器定義可以是 由新 Audio Worklet 物件的 addModule() 呼叫載入及註冊。 包含音訊小程式的 Worklet API 僅適用於 安全內容,因此 使用這類網址的網頁,必須透過 HTTPS 提供,但 http://localhost 確保本機測試的安全性

您可以將 AudioWorkletNode 的子類別 執行該工作程式的處理器支援的自訂節點。

// This is the "processors.js" file, evaluated in AudioWorkletGlobalScope
// upon audioWorklet.addModule() call in the main global scope.
class MyWorkletProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
  }

  process(inputs, outputs, parameters) {
    // audio processing code here.
  }
}

registerProcessor('my-worklet-processor', MyWorkletProcessor);

AudioWorkletGlobalScope 中的 registerProcessor() 方法會 要註冊的處理器名稱和類別定義字串。 在全域範圍內完成指令碼程式碼評估後, 「AudioWorklet.addModule()」承諾會解決通知使用者的問題 類別定義已準備好用於主要全域範圍

自訂音訊參數

AudioNodes 的優點之一是可排程參數 運用AudioParam自動化技術。AudioWorkletNodes 可以利用這些節點 可以自動以音訊速率控管的曝光參數

音訊 Worklet 節點和處理器圖表
Fig.2

使用者定義的音訊參數可在 AudioWorkletProcessor 中宣告 方法是設定一組 AudioParamDescriptor。 基礎 WebAudio 引擎會在 建立及連結 相應 AudioParam 物件傳送到節點。

/* A separate script file, like "my-worklet-processor.js" */
class MyWorkletProcessor extends AudioWorkletProcessor {

  // Static getter to define AudioParam objects in this custom processor.
  static get parameterDescriptors() {
    return [{
      name: 'myParam',
      defaultValue: 0.707
    }];
  }

  constructor() { super(); }

  process(inputs, outputs, parameters) {
    // |myParamValues| is a Float32Array of either 1 or 128 audio samples
    // calculated by WebAudio engine from regular AudioParam operations.
    // (automation methods, setter) Without any AudioParam change, this array
    // would be a single value of 0.707.
    const myParamValues = parameters.myParam;

    if (myParamValues.length === 1) {
      // |myParam| has been a constant value for the current render quantum,
      // which can be accessed by |myParamValues[0]|.
    } else {
      // |myParam| has been changed and |myParamValues| has 128 values.
    }
  }
}

AudioWorkletProcessor.process() 方法

實際音訊處理作業是在 process() 回呼方法的 AudioWorkletProcessor。必須由類別中的使用者實作 定義WebAudio 引擎會以非均勻叫用這個函式 同時動態饋給輸入和參數以及擷取輸出內容

/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
  // The processor may have multiple inputs and outputs. Get the first input and
  // output.
  const input = inputs[0];
  const output = outputs[0];

  // Each input or output may have multiple channels. Get the first channel.
  const inputChannel0 = input[0];
  const outputChannel0 = output[0];

  // Get the parameter value array.
  const myParamValues = parameters.myParam;

  // if |myParam| has been a constant value during this render quantum, the
  // length of the array would be 1.
  if (myParamValues.length === 1) {
    // Simple gain (multiplication) processing over a render quantum
    // (128 samples). This processor only supports the mono channel.
    for (let i = 0; i < inputChannel0.length; ++i) {
      outputChannel0[i] = inputChannel0[i] * myParamValues[0];
    }
  } else {
    for (let i = 0; i < inputChannel0.length; ++i) {
      outputChannel0[i] = inputChannel0[i] * myParamValues[i];
    }
  }

  // To keep this processor alive.
  return true;
}

此外,process() 方法的傳回值可用於 控制 AudioWorkletNode 的生命週期,讓開發人員能夠 記憶體用量從 process() 方法標記傳回 false 處理器無效,WebAudio 引擎也不再叫用 方法。如要讓處理器保持運作,方法必須傳回 true。 否則,系統會垃圾收集節點和處理器組合 。

使用 MessagePort 進行雙向通訊

有時候,自訂 AudioWorkletNode 想要顯示不會 對應至 AudioParam,例如以字串為基礎的 type 屬性 以便控制自訂篩選器基於此目的及未來 AudioWorkletNodeAudioWorkletProcessor配備 MessagePort 用於雙向通訊。任何類型的自訂資料 可透過這個管道交換

Fig.2
Fig.2

可以使用 .port 屬性在節點和節點上存取 MessagePort 此架構節點的 port.postMessage() 方法會傳送訊息至 相關處理器的 port.onmessage 處理常式以及反向操作

/* The code in the main global scope. */
context.audioWorklet.addModule('processors.js').then(() => {
  let node = new AudioWorkletNode(context, 'port-processor');
  node.port.onmessage = (event) => {
    // Handling data from the processor.
    console.log(event.data);
  };

  node.port.postMessage('Hello!');
});
/* "processors.js" file. */
class PortProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.port.onmessage = (event) => {
      // Handling data from the node.
      console.log(event.data);
    };

    this.port.postMessage('Hi!');
  }

  process(inputs, outputs, parameters) {
    // Do nothing, producing silent output.
    return true;
  }
}

registerProcessor('port-processor', PortProcessor);

MessagePort 支援可轉移,也就是說,您可以 轉移資料儲存空間或 WASM 模組開啟 能找出無數可能性。

逐步說明:建構 GainNode

以下是以 GainNode 為基礎建構的完整範例 《AudioWorkletNode》和《AudioWorkletProcessor》。

index.html 檔案:

<!doctype html>
<html>
<script>
  const context = new AudioContext();

  // Loads module script with AudioWorklet.
  context.audioWorklet.addModule('gain-processor.js').then(() => {
    let oscillator = new OscillatorNode(context);

    // After the resolution of module loading, an AudioWorkletNode can be
    // constructed.
    let gainWorkletNode = new AudioWorkletNode(context, 'gain-processor');

    // AudioWorkletNode can be interoperable with other native AudioNodes.
    oscillator.connect(gainWorkletNode).connect(context.destination);
    oscillator.start();
  });
</script>
</html>

gain-processor.js 檔案:

class GainProcessor extends AudioWorkletProcessor {

  // Custom AudioParams can be defined with this static getter.
  static get parameterDescriptors() {
    return [{ name: 'gain', defaultValue: 1 }];
  }

  constructor() {
    // The super constructor call is required.
    super();
  }

  process(inputs, outputs, parameters) {
    const input = inputs[0];
    const output = outputs[0];
    const gain = parameters.gain;
    for (let channel = 0; channel < input.length; ++channel) {
      const inputChannel = input[channel];
      const outputChannel = output[channel];
      if (gain.length === 1) {
        for (let i = 0; i < inputChannel.length; ++i)
          outputChannel[i] = inputChannel[i] * gain[0];
      } else {
        for (let i = 0; i < inputChannel.length; ++i)
          outputChannel[i] = inputChannel[i] * gain[i];
      }
    }

    return true;
  }
}

registerProcessor('gain-processor', GainProcessor);

這涵蓋「音訊工作程式」系統的基本知識。有現場示範 造訪 Chrome WebAudio 團隊的 GitHub 存放區

功能轉換:實驗功能至穩定版

根據預設,系統會為 Chrome 66 以上版本啟用音訊小程式。在 Chrome 64 和 65 版中,這項功能是沿用實驗性旗標。