AudioWorker を試してみる

# 2016.09.08 追記

**AudioWorkerは編纂中の仕様から削除されました。Web Audio API先生の次回作AudioWorkletにご期待ください！！**

https://github.com/WebAudio/web-audio-api/issues/956


- - -


Web Audio API には ScriptProcessorNode という、JavaScript で信号処理を記述するための部品があります。Web Audio API で用意されている部品の組み合わせではやりようがない処理をしたいときに重宝するのですが、[最新のドラフト仕様](http://webaudio.github.io/web-audio-api)では DEPRECATED となっていて将来的には廃止されるようです。

そして廃止される ScriptProcessorNode に変わって提案されているのが AudioWorker という仕組みで、ScriptProcessorNode との大きな違いとしてメインスレッドで行っていた信号処理をワーカースレッドで行うようになります。また、任意の AudioParam を設定できるインターフェイスが用意されているなど、よりネイティブな AudioNode と近い形式で独自の AudioNode を生成できるようになります。

残念ながら 2016年1月1日 の時点では実装されているブラウザがないため使うことはできないのですが、既存の Web Audio API 上で AudioWorker のインターフェースを実装したライブラリがあるので、ちょっとだけなら試してみることができます。

AudioWorkerShim - https://github.com/mohayonao/audio-worker-shim

_注意1：仕様が確定しているわけではないので、今後変更される可能性があります_

_注意2：AudioWorkerShim は提案されている仕様とは以下の点が異なります_

  - 信号処理はメインスレッドで行う (内部は ScriptProcessorNode を使っている)
  - メッセージのデータはコピーでなく参照で渡される
  - チャネルスプリット/マージの機能がない (単純なモノ or ステレオミックスしか扱えない)

## AudioWorkerShim を使う

[audio-worker-shim.js](https://raw.githubusercontent.com/mohayonao/audio-worker-shim/master/build/audio-worker-shim.js) または [audio-worker-shim-light.js](https://raw.githubusercontent.com/mohayonao/audio-worker-shim/master/build/audio-worker-shim-light.js) をダウンロードしてHTMLファイルで読み込んで、`AudioWorkerShim.polyfill();` を実行します。これで AudioContext で `createAudioWorker` メソッドが定義されます。

```html
<script src="/path/to/audio-worker-shim.js"></script>
<script>AudioWorkerShim.polyfill();</script>
```

_補足：audio-worker-shim-light.js とは？_
audio-worker-shim-light.js は [AudioWorkerGlobalScope](http://webaudio.github.io/web-audio-api/#the-audioworkerglobalscope-interface) で定義されているグローバル変数/関数を暗黙的に使用することができず、コード内で明示的に `self` を書く必要がありますが、ファイルサイズが 1/8 (560kB -> 70kB) と軽量なバージョンです。

```js
// audio-worker-shim.js ではこう書けるけど
onmessage = function(e) {
  console.log(sampleRate);
};

// audio-worker-shim-light.js ではこう書かないといけない (selfが必要)
self.onmessage = function(e) {
  console.log(self.sampleRate);
};
```

## AudioWorker の構成

<img width="719" alt="audio-worker.png" src="https://qiita-image-store.s3.amazonaws.com/0/2568/9b8f129c-b383-bdcd-3410-01c59de26abc.png">

流れは以下の順番

1. _main_ : [AudioWorker](http://webaudio.github.io/web-audio-api/#AudioWorkerNode) を生成
2. _worker_ : [AudioWorkerGlobalScope](http://webaudio.github.io/web-audio-api/#the-audioworkerglobalscope-interface) 内で `onaudioprocess` や `onnodecreate` を定義する
3. _main_ : [AudioWorkerNode](http://webaudio.github.io/web-audio-api/#AudioWorkerNode) を生成
4. _worker_ : `onnodecreate` 内で各ノードごとのパラメータを初期化する
5. _worker_ : `onaudioprocess` で信号処理を行う

ポイントとしては、`AudioWorker` と `AudioWorkerGlobalScope` がグローバルな空間としてあって、`AudioWorkerNode` と `AudioWorkerNodeProcessor` が個々の AudioNode として振る舞うという点です。

## インターフェース

### [AudioWorker](http://webaudio.github.io/web-audio-api/#AudioWorkerNode)

AudioWorker の **メインスレッド側** のインターフェースです。

```
interface AudioWorker : Worker {
    void            terminate ();
    void            postMessage (any message, optional sequence<Transferable> transfer);
                    attribute EventHandler                 onmessage;
                    attribute EventHandler                 onloaded;

    AudioWorkerNode createNode (int numberOfInputs, int numberOfOutputs);

    readonly        attribute AudioWorkerParamDescriptor[] parameters;
    AudioParam      addParameter (DOMString name, float defaultValue);
    void            removeParameter (DOMString name);
};
```

[Worker](https://developer.mozilla.org/ja/docs/Web/API/Worker) のインターフェース (`postMessage` や `onmessage`) に加えて、`AudioWorkerNode` を生成する `createNode()` や AudioParam を設定するためのインターフェース (`parameters`, `addParameter` や `removeParameter`) が定義されています。

_著者注： `addParameter` の戻り値が `AudioParam` となっているのは仕様の間違いな気がします_

### [AudioWorkerGlobalScope](http://webaudio.github.io/web-audio-api/#the-audioworkerglobalscope-interface)

AudioWorker の **ワーカースレッド側** のインターフェースです。

```
interface AudioWorkerGlobalScope : DedicatedWorkerGlobalScope {
    readonly        attribute float                        sampleRate;
                    attribute EventHandler                 onaudioprocess;
                    attribute EventHandler                 onnodecreate;

    readonly        attribute AudioWorkerParamDescriptor[] parameters;
    AudioParam      addParameter (DOMString name, float defaultValue);
    void            removeParameter (DOMString name);
};
```

[DedicatedWorkerGlobalScope](https://developer.mozilla.org/ja/docs/Web/API/DedicatedWorkerGlobalScope) のインターフェース (`postMessage` や `onmessage`) に加えて、AudioParam を設定するためのインターフェース (`parameters`, `addParameter` や `removeParameter`)、 信号処理を書く `onaudioprocess` や個々のノードの初期化に使われる `onnodecreate` といったイベントハンドラが定義されています。 

### [AudioWorkerParamDescriptor](http://webaudio.github.io/web-audio-api/#AudioWorkerParamDescriptor)

上記のインターフェースで定義されている `parameters` の戻り値になる型です。

```
interface AudioWorkerParamDescriptor {
    readonly        attribute DOMString name;
    readonly        attribute float     defaultValue;
};
```

定義されているパラメータを列挙したりするのに使います。

```js
self.parameters.map((param) => {
  console.log(param.name, param.defaultValue);
});
```  

### [AudioWorkerNode](http://webaudio.github.io/web-audio-api/#AudioWorkerNode)

AudioWorker から生成される AudioNode の **メインスレッド側** のインターフェースです。

```
interface AudioWorkerNode : AudioNode {
    void            postMessage (any message, optional sequence<Transferable> transfer);
                    attribute EventHandler onmessage;
};
```

通常の [AudioNode](http://webaudio.github.io/web-audio-api/#the-audionode-interface) のインターフェース (`connect` や `disconnect`) に加えて、`AudioWorkerNodeProcessor` とメッセージをやり取りするための `postMessage` や `onmessage` が定義されています。

### [AudioWorkerNodeProcessor](http://webaudio.github.io/web-audio-api/#the-audioworkernodeprocessor-interface)

AudioWorker から生成される AudioNode の **ワーカースレッド側** のインターフェースです。

```
interface AudioWorkerNodeProcessor : EventTarget {
    void            postMessage (any message, optional sequence<Transferable> transfer);
                    attribute EventHandler onmessage;
};
```

上記の `AudioWorkerNode` とメッセージをやり取りするための `postMessage` や `onmessage` が定義されています。

### [AudioWorkerNodeCreationEvent](http://webaudio.github.io/web-audio-api/#the-audioworkernodecreationevent-interface)

AudioWorker が AudioWorkerNode を生成したときに発火される **ワーカースレッド側** のイベントです。

```
interface AudioWorkerNodeCreationEvent : Event {
    readonly        attribute AudioWorkerNodeProcessor node;
    readonly        attribute Array                    inputs;
    readonly        attribute Array                    outputs;
};
```

個々のノード用のパラメータ (バッファや設定値など) を設定するのに使います。

```js:worker.js
self.onnodecreate = function(e) {
  e.node.anyParameter = Math.random();
  e.node.buffer = new Float32Array(4);
};
```

### [AudioProcessEvent](http://webaudio.github.io/web-audio-api/#the-audioprocessevent-interface)

AudioWorkerNode の信号処理時に発火される **ワーカースレッド側** のイベントです。

```
interface AudioProcessEvent : Event {
    readonly        attribute double                   playbackTime;
    readonly        attribute AudioWorkerNodeProcessor node;
    readonly        attribute Float32Array[][]         inputs;
    readonly        attribute Float32Array[][]         outputs;
    readonly        attribute object                   parameters;
};
```

`node` プロパティで個々のノードの設定値を読み書きすることができます。

```js:worker.js
self.onaudioprocess = function(e) {
  e.node.anyParameter; // ノードごとに異なる値
  e.node.buffer;       // ノードごとに異なるバッファ
};
```

## やってみる

では Web Audio API のドラフト仕様の [2.12.7.1 A Bitcrusher Node](http://webaudio.github.io/web-audio-api/#a-bitcrusher-node) のコードを参考にしながら AudioWorker の使い方を説明します。

[Bitcrusher Node デモ](http://mohayonao.github.io/audio-worker-shim/examples/bitcrusher.html) (オーディオファイルをドラッグ＆ドロップすると開始します)

```js:main.js
var bitcrusherFactory = null;

audioContext.createAudioWorker("bitcrusher_worker.js").then(function(factory) {
  // cache 'factory' in case you want to create more nodes!
  bitcrusherFactory = factory;
  
  var bitcrusherNode = factory.createNode();
      
  bitcrusherNode.bits.setValueAtTime(8,0);
  bitcrusherNode.connect(output); 
  
  input.connect(bitcrusherNode);
});
```

```js:bitcrusher_worker.js
// Custom parameter - number of bits to crush down to - default 8
this.addParameter( "bits", 8 );

// Custom parameter - frequency reduction, 0-1, default 0.5
this.addParameter( "frequencyReduction", 0.5 );

onnodecreate = function(e) {
  e.node.phaser = 0;
  e.node.lastDataValue = 0;
};

onaudioprocess = function(e) {
  for (var channel = 0; channel < e.inputs[0].length; channel++) {
    var inputBuffer = e.inputs[0][channel];
    var outputBuffer = e.outputs[0][channel];
    var bufferLength = inputBuffer.length;
    var bitsArray = e.parameters.bits;
    var frequencyReductionArray = e.parameters.frequencyReduction;

    for (var i = 0; i < bufferLength; i++) {
      var bits = bitsArray ? bitsArray[i] : 8;
      var frequencyReduction = frequencyReductionArray ? frequencyReductionArray[i] : 0.5;
      var step = Math.pow(1/2, bits);

      e.node.phaser += frequencyReduction;
      if (e.node.phaser >= 1.0) {
          e.node.phaser -= 1.0;
          e.node.lastDataValue = step * Math.floor(inputBuffer[i] / step + 0.5);
      }
      outputBuffer[i] = e.node.lastDataValue;
    }
  }
};
```

### 1. AudioWorker を生成

**メインスレッド側**

AudioWorker は `AudioContext#createAudioWorker` で生成します。WebWorker との違いとして Promise ベースのインターフェースとなっています。また他の AudioNode の違いとして `createAudioWorker` で得られるのは AudioNode ではなく AudioWorker なので、これを他の AudioNode に接続したりすることはできません。

以下の例では `bitcrusher_worker.js` を読み込んで `bitcrusherFactory` に代入しています。(`factory` が AudioWorker のインスタンスです)

```js:main.js
var bitcrusherFactory = null;

audioContext.createAudioWorker("bitcrusher_worker.js").then(function(factory) {
  // cache 'factory' in case you want to create more nodes!
  bitcrusherFactory = factory;
});
```

### 2. AudioWorkerGlobalScope 内で AudioParam を定義する

**ワーカースレッド側**

`addParameter` を使って AudioParam を定義します。`this` のかわりに `self` とすることもできますし、省略することもできます。ここで定義した AudioParam がメインスレッド側の AudioWorkerNode で操作することができます。

```js:bitcrusher_worker.js
// Custom parameter - number of bits to crush down to - default 8
this.addParameter( "bits", 8 );

// Custom parameter - frequency reduction, 0-1, default 0.5
this.addParameter( "frequencyReduction", 0.5 );

parameters.map((param) => {
  return [ param.name, param.defaultValue ];
});
// → [ [ "bits", 8 ], [ "frequencyReduction", 0.5 ] ]
```

```js:main.js
audioContext.createAudioWorker("bitcrusher_worker.js").then(function(worker) {
  worker.parameters.map((param) => {
    return [ param.name, param.defaultValue ];
  });
  // → [ [ "bits", 8 ], [ "frequencyReduction", 0.5 ] ]
});
```

### 3. AudioWorkerNode を生成

**メインスレッド側**

`AudioContext#createAudioWorker` で生成した AudioWorker の `AudioWorker#createNode` を呼び出して AudioWorkerNode を生成します。ここで生成される AudioWorkerNode は他の AudioNode と同じように `connect`, `disconnect` の操作ができます。また、前段で設定した AudioParam を参照することができます。

```js:main.js
var node = bitcrusherFactory.createNode(2, 2);

node.bits.value = 2;
node.frequencyReduction.value = 0.25;

input.connect(node);
node.connect(audioContext.destination);
```

### 4. `onnodecreate` 内で各ノードごとのパラメータを初期化する

**ワーカースレッド側**

メインスレッド側で `createNode` をしたときに ワーカースレッド側で呼び出されます。ここでノードごとのパラメータの初期化を行います。

```js:bitcrusher_worker.js
onnodecreate = function(e) {
  e.node.phaser = 0;
  e.node.lastDataValue = 0;
};
```

### 5. `onaudioprocess` で信号処理を行う

**ワーカースレッド側**

メインスレッド側で AudioWorkerNode を接続したあと、連続的に呼ばれます。ここで信号処理を行います。

`e.inputs[0][channel]: Float32Array` でチャネルごとの入力を読み取り、
`e.outputs[0][channel]: Float32Array` でチャネルごとの出力を書き込みます。
`e.inputs[0]` の `0` 以外はチャネルマージや経路の付け替えなど、より高度な処理をしたいときに使うことを想定しているようですが、基本的な用途では気にしなくて良いと思います。

前段で `addParameter` を使って定義した AudioParam のデータは `e.parameters[name]: Float32Array` で信号データとして読み取ることができます。

`e.node[name]: any` で各ノードごとの設定値を読み書きすることができます。

ポイントとしては `onaudioprocess` はすべてのノードで共通で、渡されるイベントオブジェクトの属性値でそれぞれのノードの設定値を操作するという点です。(C でオブジェクト指向っぽいことをするときの感覚に似ていると思います)

`bitcrusher_worker` の例では `e.parameters.bits` や `e.parameters.frequencyReduction` の値を外部パラメータとして受け取り、信号情報を劣化させる処理を行っています。その際に必要になるパラメータとして `e.node.phaser` や `e.node.lastDataValue` を使用しています。

```js:bitcrusher_worker.js
onaudioprocess = function(e) {
  for (var channel = 0; channel < e.inputs[0].length; channel++) {
    var inputBuffer = e.inputs[0][channel];
    var outputBuffer = e.outputs[0][channel];
    var bufferLength = inputBuffer.length;
    var bitsArray = e.parameters.bits;
    var frequencyReductionArray = e.parameters.frequencyReduction;

    for (var i = 0; i < bufferLength; i++) {
      var bits = bitsArray ? bitsArray[i] : 8;
      var frequencyReduction = frequencyReductionArray ? frequencyReductionArray[i] : 0.5;
      var step = Math.pow(1/2, bits);

      e.node.phaser += frequencyReduction;
      if (e.node.phaser >= 1.0) {
          e.node.phaser -= 1.0;
          e.node.lastDataValue = step * Math.floor(inputBuffer[i] / step + 0.5);
      }
      outputBuffer[i] = e.node.lastDataValue;
    }
  }
};
```

### 6. メインスレッド側とのメッセージング

#### AudioWorker <--> AudioWorkerGlobalScope

これは Worker 全体の設定やデータのやり取りに使えます。
以下の例はマウスカーソルの位置に応じて全ノードの設定を変える例です。

- [MouseNoise Node デモ](http://mohayonao.github.io/audio-worker-shim/examples/mousenoise.html) (オーディオファイルをドラッグ＆ドロップすると開始します)

```js:main.js
audioContext.createAudioWorker("mousenoise_worker.js").then(function(worker) {
  window.addEventListener("mousemove", function(e) {
    var x = e.pageX / window.innerWidth;
    var y = e.pageY / window.innerHeight;

    worker.postMessage({ x: x, y: y }); 
  });
});
```

```js:mousenoise_worker.js
var mouseGain = [ 0, 0 ];

onmessage = function(e) {
  mouseGain[0] = e.data.x;
  mouseGain[1] = e.data.y;
};

onaudioprocess = function(e) {
  for (var channel = 0; channel < e.inputs[0].length; channel++) {
    var inputBuffer = e.inputs[0][channel];
    var outputBuffer = e.outputs[0][channel];
    var bufferLength = inputBuffer.length;
    var gain = mouseGain[channel % 2];
    var noise;

    for (var i = 0; i < bufferLength; i++) {
      noise = Math.random();

      if (noise < gain) {
        outputBuffer[i] = inputBuffer[i];
      } else if (noise < gain * gain) {
        outputBuffer[i] = noise - 0.5;
      } else {
        outputBuffer[i] = 0;
      }
    }
  }
};
```

#### AudioWorkerNode <--> AudioWorkerNodeProcessor

これは各ノードごとの設定やデータのやり取りに使えます。

以下の例はキャプチャした信号データをメインスレッドに送る例です。
少し凝った試みとして 1つの Float32Array を Transferable Objects としてグルグル回すことでデータをやり取りしています。

- [Capture Node デモ](http://mohayonao.github.io/audio-worker-shim/examples/capture.html) (オーディオファイルをドラッグ＆ドロップすると開始します)

```js:main.js
function preview(buffer) {
  console.log(buffer[0][0]);
}

audioContext.createAudioWorker("capture_worker.js").then(function(worker) {
  var node = worker.createNode(1, 1);

  node.onmessage = function(e) {
    var buffers = e.data;

    preview(buffers);

    node.postMessage(buffers, [ buffers[0].buffer, buffers[1].buffer ]);
  };

  inputs.connect(node);
  node.connect(audioContext.destination);
});
```

```js:capture_worker.js
var bufferLength = 2048;

onnodecreate = function(e) {
  var node = e.node;

  node.buffers = [ new Float32Array(bufferLength), new Float32Array(bufferLength) ];
  node.bufferIndex = 0;
  node.onmessage = function(e) {
    node.buffers = e.data;
    node.bufferIndex = 0;
  };
};

onaudioprocess = function(e) {
  var node = e.node;

  for (var channel = 0; channel < e.inputs[0].length; channel++) {
    var inputBuffer = e.inputs[0][channel];

    e.outputs[0][channel].set(inputBuffer);

    if (node.buffers !== null) {
      node.buffers[channel].set(inputBuffer, node.bufferIndex);
    }
  }

  if (node.buffers !== null) {
    node.bufferIndex += e.inputs[0][0].length;
    if (bufferLength <= node.bufferIndex) {
      node.postMessage(node.buffers, [ node.buffers[0].buffer, node.buffers[1].buffer ]);
      node.buffers = null;
    }
  }
};
```

## まとめ

ざっくり AudioWorker(Shim) の使い方を説明しました。
まだ実装されているブラウザはありませんし、確定した仕様ではないため、最終的には全然違うインタフェースになる可能性もあります。それでも、なんとなくの使い方はイメージできたかなと思います。

最後に僕が試しに書いてみたサンプルを紹介してこのエントリを終えたいと思います。

- [Stutter Node デモ](http://mohayonao.github.io/audio-worker-shim/examples/stutter.html) (オーディオファイルをドラッグ＆ドロップすると開始します)