この記事は WebAudio/WebMIDI API Advent Calendar 2017 Advent Calendar 2017の25日目です。
TL;DR
この記事はChromiumのWeb AudioのコミッタであるSoftware EngineerのHongchan Choiが書いた**Enter AudioWorklet**の記事の邦訳です。
(This is the Japanese version of 'Enter AudioWorklet' of Google Developers.)
前書き
Chrome 64(実験的フラグ付きではあるものの)で待ち望まれていたWeb Audioの新しい機能AudioWorkletが利用可能になります。この記事ではJavaScriptでカスタムのAudio処理を作つことを首を長くして待っていた開発者へAPIのコンセプトと使い方の紹介を書いたものです。GitHubで公開しているライブデモ、またChrome64でこの実験的APIを使うための手順をご覧ください。
バックグラウンドの軽い説明
AudioWorkletの位置づけはScriptProcessorNode
の後継です。では、なぜScriptProcessorNode
は変更されるのでしょうか?
Web Audio APIはAudio processing(音声処理)を出来る限りスムーズに行う為にブラウザのメインのスレッドであるUIのスレッドとは別のスレッド(Audioスレッド)で動作しています。JavaScriptでカスタムの音声処理をする為に、Web Audio APIでは「ユーザが記述したメインスレッドで動作するスクリプトの実行をAudioスレッドからイベントハンドラを使って行う」ScriptProcessorNode
が定義され、実装されています。
このScriptProcessorNodeには2つの問題があります。
イベントのハンドリングは非同期に行われるような仕様になっていて、そしてイベントで動作するコードはメインスレッドで動作します。よって前者はレイテンシ(遅延)を、後者はUIやDOMに関連したタスクをより過密化させてることでメインスレッドに圧力を与え、UIに対してカクカクした動作等を代表されるようなUIの非スムースな動作「ジャンク」、また不自然な音切れに代表されるような音声再生中の問題「グリッチ」を引き起こします。
この基本的な欠陥がある為にScriptProcessorNode
はWeb Audio APIの仕様から削除され、AudioWorkletに置き換えられるのです。
Concepts
AudioWorkletは開発者が記述したJavaScriptのコードをAudioスレッドで動作させることができます。音声処理をメインスレッドにジャンプさせて行う必要がなくなりました。開発者が記述したJavaScriptのコードは他のAudioNodeが実装されているオーディオスレッド(AudioWorkletGlobalScope
)で動作するようになるので、処理をする為にレイテンシを増加させることなく、また同期して音声を発せさせることを確実なものにしています。
登録とインストール
AudioWorkletで音声処理を記述するには2つの要素から構成します。それらは、AudioWorkletProcessor
とAudioWorketNode
です。これはScriptProcessorNode
を使う場合に比べて複雑になりますが、開発者にカスタムの音声処理の為にLow Levelの機能を提供する必要があるからです。
AudioWorketProcessor
はJavaScriptで記述された実際の音声処理のことを示し、AudioWorketGlobalScope
にスコープされます。
AudioWorkletNode
は他のAudioNodeとのメッセージのやり取りをAudioWorketProcessor
に呼応して動作することで面倒をみます。そして、他のAudioNodeと同じくGlobal Scopeで動作します。
以下がAudioWorkletの登録とインストールを行うコードスニペットです。
// 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
を作るには少なくとも2つのことが要求されます。その2つとはAudioContectのオブジェクトとその処理の名前がstring
で定義されていることです。処理の定義は新しいAudioWorkletオブジェクトのAddModule()
で読み込まれ、登録されます。AudioWorkletを含むWorkletに関連するAPIはSecure Contentでのみ動作します。つまりWorkletを使うページ、アプリはHTTPSが利用可能なサーバから配信される必要がるということです。例外的にhttp://localhost
でも利用可能です。
Worklet上で動作している処理によって裏付けられたカスタムNodeを定義する為にAudioWorkletNode
をサブクラス化できることにも注目してください。
// この'processor.js'ファイルはGlobalScopeの`AudioWorklet.addModule()`
// のCallによってAudioWorkletGlobalScopeとして評価されます
class MyWorkletProcessor extends AudioWorkletProcessor {
constructor() {
super();
}
process(inputs, outputs, parameters) {
// ここに音声処理を書く
}
}
registerProcessor('my-worklet-processor', MyWorkletProcessor);
registerProcessor()
はstring
の名前でAudioWorkletGlobalScope
に登録、またClassとして定義されます。GlobalScopeでのコードの評価が完了すると、AudioWorklet.addModukle()
からGlobalScopeでClassの定義が完了し、利用ができるというresolve
(または何らかのエラー)がPromise
で返されます。
カスタム(独自の)のAudio Param
AudioNodeでのAudioParamのオートメションは便利な機能の1つです。AudioWorkletNode
でも利用することが可能で、自動的に制御できる公開されたパラメータを取得することが可能です。
開発者が定義したAudioParamはAudioWorkletProcessor
クラスでAudioParamDescriptor
を設定することでその定義が可能です。
/* 例えば、"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|はWeb Audioエンジンで通常のAudioParamの操作で計算された
// 128サンプルのFloat32Array。すべてのパラメータは0.707で(automation,
// methods, setter)がデフォルトで設置されている。
let myParamValues = parameters.myParam;
}
}
AudioWorkletProcessor.process()
実際の音声処理はAudioWorkletProcessor
のprocess()
コールバックで行われ、開発者がクラス定義内で実装する必要があります。WebAudioエンジンは、これを inputs および parameters を入力として outputs を取得するための関数として、等時間間隔で呼び出します。
/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
// 複数のInput、Outputがあった場合、最初のInput、Outputを取得する
let input = inputs[0];
let output = outputs[0];
// 複数のチャンネルがあった場合、最初のチャンネルを取得する.
let inputChannel0 = input[0];
let outputChannel0 = output[0];
// パラメータを配列で取得
let myParamValues = parameters.myParam;
// シンプルな多重のGainの128サンプルを超えた処理
// モノラルのみ対応
for (let i = 0; i < inputChannel0.length; ++i) {
outputChannel0[i] = inputChannel0[i] * myParamValues[i];
}
// 処理を続ける
return true;
}
更に、process()
の戻り値を使ってAudioWorkletNode
のライフタイムを制御することが可能なので、開発者はメモリの管理もすることが可能です。process()
メソッドからfalse
が返ってくると、その処理は非アクティブとしてマークされてWebAudioエンジンはそのメソッドをもう呼び出すことはありません。処理を有効な状態に保つにはtrue
を返す必要があります。さもなければ、最終的にはAudioWorkletNode
、AudioWorkletProcessor
のペアはシステムのガベージコレクションによって回収されるでしょう。
MessagePortによる双方向のやりとり
AudioWorkletNode
はAudioParamには定義されていない値をNodeの外からコントロールさせたい場合がある。例えば、string
で定義された属性type
はFilterをコントロールするのに利用される。この目的場合、AudioWorkletNode
とAudioWorkletProcessor
は双方向でやり取りするためにMessagePort
が用意されている。いかなる種類のカスタムな値でもこのChannelを使うことでやり取りすることが可能になっている。
MessagePort
はAudioWorkletNode
、AudioWorkletProcessor
の.port
の属性からアクセスすることが可能。AudioWorkletNode
側のport.postMessage()
でメッセージを送り、AudioWorkletProcessor
側は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
はスレッドの境界を超えてデータを保存する、またWASM(Web Assembly)に乗り換えさせることも可能になっている。これによってAudioWorkletで実現できることの可能性を無限に広げています。
実装してみる:GainNode
上記を踏まえて、GaiNodeをAudioWorkletNode
、AudioWorkletProcessor
を実装してみる。
- 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>
- 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) {
let input = inputs[0];
let output = outputs[0];
let gain = parameters.gain;
for (let channel = 0; channel < input.length; ++channel) {
let inputChannel = input[channel];
let outputChannel = output[channel];
for (let i = 0; i < inputChannel.length; ++i)
outputChannel[i] = inputChannel[i] * gain[i];
}
return true;
}
}
registerProcessor('gain-processor', GainProcessor);
これでAudioWorkletの基本的な部分はカバーできていると思います。ライブデモはChrome WebAudio team's GitHub repositoryにありますので参考にしてみてください。
実験的利用とOrigin Trials
AudioWorkletは Chrome 64にて実験的フラグ付き(behind the experimental flag)で利用可能です。AudioWorkletを有効にするには2つ方法があります。
Chrome起動時にオプションをつける
--enable-blink-features=Worklet,AudioWorklet
Chrome起動後にGUIから有効にする
アドレスバーに以下を入力し、有効(enable)にすることで利用可能になります。
chrome://flags/#enable-experimental-web-platform-features`
この方法を取った場合、Chromeが実験的に実装している機能すべてが有効になるので注意してください。
Origin Trials(開発者向け)
また、Origin TrialもすべてのPlatformで行っています。Origin Trialとはフィードバックを受けることを目的とし、特定のオリジン(scheme + domain + port)にのみ期限付きでその機能を提供する仕組みです。申込はこちらから行ってください。(申請フォーム)
詳しくはこちらの「Origin Trials」の項目を参照してください。
Profiling Web Audio apps in Chrome
(追記:2020年5月13日)最後にWeb AudioのPerfomanceのProfilingについて素晴らしいをやっぱり @hochsaysが記事を書いてくださいましたのでリンクをつけときます!
Is your Web Audio app glitching? I wrote something for you: https://t.co/rqLjYptJJI #chrome #webaudio
— Hongchan Choi (@hochsays) May 6, 2020
Thanks for Hongchan🤘
Thanks for Hongchan who have been making effort of implementing and of making big improvement of Web Audio in Chromium!!