LoginSignup
50
48

More than 5 years have passed since last update.

AudioWorker を試してみる

Last updated at Posted at 2016-01-02

2016.09.08 追記

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


Web Audio API には ScriptProcessorNode という、JavaScript で信号処理を記述するための部品があります。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 または audio-worker-shim-light.js をダウンロードしてHTMLファイルで読み込んで、AudioWorkerShim.polyfill(); を実行します。これで AudioContext で createAudioWorker メソッドが定義されます。

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

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

// 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 の構成

audio-worker.png

流れは以下の順番

  1. main : AudioWorker を生成
  2. worker : AudioWorkerGlobalScope 内で onaudioprocessonnodecreate を定義する
  3. main : AudioWorkerNode を生成
  4. worker : onnodecreate 内で各ノードごとのパラメータを初期化する
  5. worker : onaudioprocess で信号処理を行う

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

インターフェース

AudioWorker

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 のインターフェース (postMessageonmessage) に加えて、AudioWorkerNode を生成する createNode() や AudioParam を設定するためのインターフェース (parameters, addParameterremoveParameter) が定義されています。

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

AudioWorkerGlobalScope

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 のインターフェース (postMessageonmessage) に加えて、AudioParam を設定するためのインターフェース (parameters, addParameterremoveParameter)、 信号処理を書く onaudioprocess や個々のノードの初期化に使われる onnodecreate といったイベントハンドラが定義されています。

AudioWorkerParamDescriptor

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

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

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

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

AudioWorkerNode

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

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

通常の AudioNode のインターフェース (connectdisconnect) に加えて、AudioWorkerNodeProcessor とメッセージをやり取りするための postMessageonmessage が定義されています。

AudioWorkerNodeProcessor

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

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

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

AudioWorkerNodeCreationEvent

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

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

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

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

AudioProcessEvent

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 プロパティで個々のノードの設定値を読み書きすることができます。

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

やってみる

では Web Audio API のドラフト仕様の 2.12.7.1 A Bitcrusher Node のコードを参考にしながら AudioWorker の使い方を説明します。

Bitcrusher Node デモ (オーディオファイルをドラッグ&ドロップすると開始します)

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);
});
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 のインスタンスです)

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 で操作することができます。

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 ] ]
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 を参照することができます。

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 をしたときに ワーカースレッド側で呼び出されます。ここでノードごとのパラメータの初期化を行います。

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.bitse.parameters.frequencyReduction の値を外部パラメータとして受け取り、信号情報を劣化させる処理を行っています。その際に必要になるパラメータとして e.node.phasere.node.lastDataValue を使用しています。

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 全体の設定やデータのやり取りに使えます。
以下の例はマウスカーソルの位置に応じて全ノードの設定を変える例です。

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 }); 
  });
});
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 デモ (オーディオファイルをドラッグ&ドロップすると開始します)
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);
});
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 デモ (オーディオファイルをドラッグ&ドロップすると開始します)
50
48
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
48