Chrome 64 では、Web Audio API の待望の新機能である AudioWorklet が導入されています。ここでは、JavaScript コードを使用してカスタム音声プロセッサを作成するコンセプトと使用方法について説明します。ライブデモをご覧ください。高度なオーディオアプリを構築する場合は、シリーズの次の記事「オーディオ ワークレットの設計パターン」もご覧ください。
背景: ScriptProcessorNode
Web Audio API のオーディオ処理は、メイン UI スレッドとは別のスレッドで実行されるため、スムーズに実行されます。JavaScript でカスタム音声処理を有効にするために、Web Audio API では、イベント ハンドラを使用してメイン UI スレッドでユーザー スクリプトを呼び出す ScriptProcessorNode が提案されました。
この設計には 2 つの問題があります。イベント処理は設計上非同期であり、コードの実行はメインスレッドで行われます。前者はレイテンシを引き起こし、後者は通常、さまざまな UI と DOM 関連のタスクで混雑しているメインスレッドに負荷をかけ、UI の「ジャンク」や音声の「グリッチ」を引き起こします。この根本的な設計上の欠陥のため、ScriptProcessorNode
は仕様から非推奨となり、AudioWorklet に置き換えられました。
コンセプト
Audio Worklet は、ユーザー提供の JavaScript コードをすべてオーディオ処理スレッド内に保持します。つまり、音声を処理するためにメインスレッドにジャンプする必要はありません。つまり、ユーザー提供のスクリプト コードは、他の組み込み AudioNodes
とともにオーディオ レンダリング スレッド(AudioWorkletGlobalScope
)で実行されるため、追加のレイテンシが発生せず、同期レンダリングが保証されます。
登録とインスタンス化
Audio Worklet の使用は、AudioWorkletProcessor
と AudioWorkletNode
の 2 つの部分で構成されます。これは ScriptProcessorNode を使用するよりも複雑ですが、カスタム音声処理の低レベル機能をデベロッパーに提供するために必要です。AudioWorkletProcessor
は、JavaScript コードで記述された実際のオーディオ プロセッサを表し、AudioWorkletGlobalScope
に存在します。AudioWorkletNode
は AudioWorkletProcessor
に対応しており、メインスレッド内の他の 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()
呼び出しによって読み込まれ、登録できます。Audio Worklet などの 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()
の Promise が解決され、クラス定義がメインのグローバル スコープで使用できる状態になったことをユーザーに通知します。
カスタム音声パラメータ
AudioNode の便利な点の 1 つは、AudioParam
によるスケジュール設定可能なパラメータ オートメーションです。AudioWorkletNodes は、これらのパラメータを使用して、オーディオ レートで自動的に制御できる公開パラメータを取得できます。
ユーザー定義の音声パラメータは、AudioParamDescriptor
のセットを設定して AudioWorkletProcessor
クラス定義で宣言できます。基盤となる WebAudio エンジンは、AudioWorkletNode の作成時にこの情報を取得し、それに応じて 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()
メソッド
実際の音声処理は、AudioWorkletProcessor
の process()
コールバック メソッドで行われます。これは、ユーザーがクラス定義で実装する必要があります。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
で、カスタム フィルタの制御に使用される文字列ベースの type
属性など、AudioParam
にマッピングされないコントロールを公開することがあります。この目的とそれ以外のために、AudioWorkletNode
と AudioWorkletProcessor
には双方向通信用の MessagePort
が装備されています。このチャネルでは、あらゆる種類のカスタムデータを交換できます。
MessagePort には、ノードとプロセッサの両方で .port
属性を使用してアクセスできます。ノードの 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
は transferable をサポートしているため、スレッド境界を越えてデータ ストレージまたは WASM モジュールを転送できます。これにより、オーディオ ワークレット システムの使用方法が無数に広がります。
チュートリアル: GainNode を作成する
AudioWorkletNode
と AudioWorkletProcessor
上に構築された GainNode の完全な例を次に示します。
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 では、この機能は試験運用版フラグで制限されていました。