Chrome 64 には、待望の Web Audio API の新機能 AudioWorklet が搭載されています。この記事では、JavaScript コードでカスタム オーディオ プロセッサを作成する方向けに、そのコンセプトと使用方法を紹介します。GitHub のライブデモをご覧ください。シリーズの次の記事、オーディオ ワークレット設計パターンも、高度なオーディオ アプリの作成に関する興味深い記事です。
バックグラウンド: ScriptProcessorNode
Web Audio API のオーディオ処理は、メインの UI スレッドとは別のスレッドで実行されるため、スムーズに実行されます。JavaScript でカスタム音声処理を可能にするために、Web Audio API では、イベント ハンドラを使用してメイン UI スレッドでユーザー スクリプトを呼び出す ScriptProcessorNode が提案されました。
この設計には 2 つの問題があります。イベント処理が非同期設計になっていることと、コード実行がメインスレッドで行われることです。前者はレイテンシを誘発し、後者は一般的にさまざまな UI や DOM 関連のタスクで混雑しているメインスレッドに負荷をかけ、UI が「ジャンク」したり、音声が「グリッチ」したりします。このような基本的な設計上の欠陥により、ScriptProcessorNode
は仕様で非推奨になり、AudioWorklet に置き換えられました。
概念
オーディオ ワークレットは、ユーザー指定の JavaScript コードをすべて音声処理スレッド内に保持します。つまり、音声を処理するためにメインスレッドにジャンプする必要はありません。つまり、ユーザー指定のスクリプト コードは、他の組み込み AudioNode とともにオーディオ レンダリング スレッド(AudioWorkletGlobalScope)で実行されるため、追加のレイテンシなしで同期レンダリングが保証されます。
登録とインスタンス化
オーディオ ワークレットの使用は、AudioWorkletProcessor と AudioWorkletNode の 2 つの部分で構成されています。これは ScriptProcessorNode を使用する場合よりも複雑ですが、カスタム オーディオ処理の低レベルの機能をデベロッパーが提供するために必要です。AudioWorkletProcessor は、JavaScript コードで記述された実際のオーディオ プロセッサを表し、AudioWorkletGlobalScope にあります。AudioWorkletNode は AudioWorkletProcessor に対応し、メインスレッド内の他の AudioNode との接続を管理します。これはメインのグローバル スコープで公開され、通常の AudioNode のように機能します。
次の 2 つのコード スニペットは、登録とインスタンス化を示しています。
// 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 オブジェクトと、文字列としてのプロセッサ名が必要です。プロセッサ定義は、新しいオーディオ ワークレット オブジェクトの addModule()
呼び出しによって読み込んで登録できます。オーディオ ワークレットを含むワークレット API は安全なコンテキストでのみ使用できるため、この API を使用するページは HTTPS 経由で提供する必要がありますが、http://localhost
はローカルテストでは安全とみなされます。
また、AudioWorkletNode をサブクラス化して、ワークレット上で実行されるプロセッサを基盤とするカスタムノードを定義することもできます。
// This is "processor.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 が解決され、クラス定義をメインのグローバル スコープで使用できるようになったことがユーザーに通知します。
カスタム AudioParam
AudioNode に役立つ点の 1 つは、AudioParams によるスケジュール可能なパラメータの自動化です。AudioWorkletNode はこれらを使用して、音声レートで自動的に制御できる公開パラメータを取得できます。
ユーザー定義の AudioParams は、AudioParamDescriptors のセットを設定することにより、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 では、AudioParam にマッピングされないコントロールを公開したい場合があります。たとえば、文字列ベースの type
属性を使用してカスタム フィルタを制御できます。この目的のために、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!');
});
/* "processor.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 via 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>
ゲイン プロセッサ.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 では 試験運用版フラグの後にあります