AudioWorkletの導入

この記事は WebAudio/WebMIDI API Advent Calendar 2017 Advent Calendar 2017の25日目です。

# TL;DR
この記事はChromiumのWeb AudioのコミッタであるSoftware Engineerの[Hongchan Choi](https://developers.google.com/web/resources/contributors#hongchanchoi)が書いた**[Enter AudioWorklet](https://developers.google.com/web/updates/2017/12/audio-worklet)**の記事の邦訳です。
(This is the Japanese version of ['Enter AudioWorklet'](https://developers.google.com/web/updates/2017/12/audio-worklet) of [Google Developers](https://developers.google.com/web).)

# 前書き
Chrome 64（実験的フラグ付きではあるものの）で待ち望まれていたWeb Audioの新しい機能AudioWorkletが利用可能になります。この記事ではJavaScriptでカスタムのAudio処理を作つことを首を長くして待っていた開発者へAPIのコンセプトと使い方の紹介を書いたものです。GitHubで公開している[ライブデモ](https://googlechromelabs.github.io/web-audio-samples/audio-worklet/)、またChrome64でこの実験的APIを使うための[手順](https://developers.google.com/web/updates/2017/12/audio-worklet#experimental)をご覧ください。

## バックグラウンドの軽い説明
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`）で動作するようになるので、処理をする為にレイテンシを増加させることなく、また同期して音声を発せさせることを確実なものにしています。

<img width="480" alt="Screen Shot 2017-12-23 at 1.37.06 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/61221/4760c527-f527-feb3-64cb-6ce1d6d1e433.png">

## 登録とインストール
AudioWorkletで音声処理を記述するには2つの要素から構成します。それらは、`AudioWorkletProcessor`と`AudioWorketNode`です。これは`ScriptProcessorNode`を使う場合に比べて複雑になりますが、開発者にカスタムの音声処理の為にLow Levelの機能を提供する必要があるからです。
`AudioWorketProcessor`はJavaScriptで記述された実際の音声処理のことを示し、`AudioWorketGlobalScope`にスコープされます。
`AudioWorkletNode`は他のAudioNodeとのメッセージのやり取りを`AudioWorketProcessor`に呼応して動作することで面倒をみます。そして、他のAudioNodeと同じくGlobal Scopeで動作します。

以下がAudioWorkletの登録とインストールを行うコードスニペットです。

````javascript
// 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](https://w3c.github.io/webappsec-secure-contexts/)でのみ動作します。つまりWorkletを使うページ、アプリはHTTPSが利用可能なサーバから配信される必要がるということです。例外的に`http://localhost`でも利用可能です。

Worklet上で動作している処理によって裏付けられたカスタムNodeを定義する為に`AudioWorkletNode`をサブクラス化できることにも注目してください。

````JavaScript
// この'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`でも利用することが可能で、自動的に制御できる公開されたパラメータを取得することが可能です。

<img width="480" alt="Screen Shot 2017-12-23 at 2.28.17 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/61221/a58a7c7d-31af-6c24-4a0b-58482676b2a3.png">

開発者が定義したAudioParamは`AudioWorkletProcessor`クラスで`AudioParamDescriptor`を設定することでその定義が可能です。

````javascript
/* 例えば、"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__ を取得するための関数として、等時間間隔で呼び出します。

````javascript
/* 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を使うことでやり取りすることが可能になっている。

<img width="480" alt="Screen Shot 2017-12-23 at 2.56.55 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/61221/3cc02b99-a5c9-1070-0d8f-62a203881730.png">

`MessagePort`は`AudioWorkletNode`、`AudioWorkletProcessor`の`.port`の属性からアクセスすることが可能。`AudioWorkletNode`側の`port.postMessage()`でメッセージを送り、`AudioWorkletProcessor`側は`port.onmessage`でハンドラを指定する。またその逆も同様に行うことも可能。

````JavaScript
/* 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!');
});
````

````javascript
/* "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**

````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**

````javascript
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](https://googlechromelabs.github.io/web-audio-samples/audio-worklet/)にありますので参考にしてみてください。

## 実験的利用とOrigin Trials
AudioWorkletは Chrome 64にて実験的フラグ付き(behind the experimental flag)で利用可能です。AudioWorkletを有効にするには2つ方法があります。

### Chrome起動時にオプションをつける

````shell
--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)にのみ期限付きでその機能を提供する仕組みです。申込はこちらから行ってください。[（申請フォーム）](https://docs.google.com/forms/d/e/1FAIpQLSfO0_ptFl8r8G0UFhT0xhV17eabG-erUWBDiKSRDTqEZ_9ULQ/viewform)
詳しくは[こちらの「Origin Trials」の項目](https://blog.jxck.io/entries/2016-09-29/vender-prefix-to-origin-trials.html)を参照してください。


## Profiling Web Audio apps in Chrome
（追記：2020年5月13日）最後にWeb AudioのPerfomanceのProfilingについて素晴らしいをやっぱり [@hochsays](https://twitter.com/hochsays)が記事を書いてくださいましたのでリンクをつけときます！

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Is your Web Audio app glitching? I wrote something for you: <a href="https://t.co/rqLjYptJJI">https://t.co/rqLjYptJJI</a> <a href="https://twitter.com/hashtag/chrome?src=hash&amp;ref_src=twsrc%5Etfw">#chrome</a> <a href="https://twitter.com/hashtag/webaudio?src=hash&amp;ref_src=twsrc%5Etfw">#webaudio</a></p>&mdash; Hongchan Choi (@hochsays) <a href="https://twitter.com/hochsays/status/1258094636888813569?ref_src=twsrc%5Etfw">May 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



# Thanks for Hongchan🤘
Thanks for Hongchan who have been making effort of implementing and of making big improvement of Web Audio in Chromium!!
<img width="400" alt="Screen Shot 2017-12-23 at 12.51.38 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/61221/e709c875-82cd-b7f3-cd4d-b62f48b022bb.png">