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 の構成
流れは以下の順番
- main : AudioWorker を生成
-
worker : AudioWorkerGlobalScope 内で
onaudioprocess
やonnodecreate
を定義する - main : AudioWorkerNode を生成
-
worker :
onnodecreate
内で各ノードごとのパラメータを初期化する -
worker :
onaudioprocess
で信号処理を行う
ポイントとしては、AudioWorker
と AudioWorkerGlobalScope
がグローバルな空間としてあって、AudioWorkerNode
と AudioWorkerNodeProcessor
が個々の 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 のインターフェース (postMessage
や onmessage
) に加えて、AudioWorkerNode
を生成する createNode()
や AudioParam を設定するためのインターフェース (parameters
, addParameter
や removeParameter
) が定義されています。
著者注: 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 のインターフェース (postMessage
や onmessage
) に加えて、AudioParam を設定するためのインターフェース (parameters
, addParameter
や removeParameter
)、 信号処理を書く 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 のインターフェース (connect
や disconnect
) に加えて、AudioWorkerNodeProcessor
とメッセージをやり取りするための postMessage
や onmessage
が定義されています。
AudioWorkerNodeProcessor
AudioWorker から生成される AudioNode の ワーカースレッド側 のインターフェースです。
interface AudioWorkerNodeProcessor : EventTarget {
void postMessage (any message, optional sequence<Transferable> transfer);
attribute EventHandler onmessage;
};
上記の AudioWorkerNode
とメッセージをやり取りするための postMessage
や onmessage
が定義されています。
AudioWorkerNodeCreationEvent
AudioWorker が AudioWorkerNode を生成したときに発火される ワーカースレッド側 のイベントです。
interface AudioWorkerNodeCreationEvent : Event {
readonly attribute AudioWorkerNodeProcessor node;
readonly attribute Array inputs;
readonly attribute Array outputs;
};
個々のノード用のパラメータ (バッファや設定値など) を設定するのに使います。
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
プロパティで個々のノードの設定値を読み書きすることができます。
self.onaudioprocess = function(e) {
e.node.anyParameter; // ノードごとに異なる値
e.node.buffer; // ノードごとに異なるバッファ
};
やってみる
では Web Audio API のドラフト仕様の 2.12.7.1 A Bitcrusher Node のコードを参考にしながら AudioWorker の使い方を説明します。
Bitcrusher Node デモ (オーディオファイルをドラッグ&ドロップすると開始します)
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);
});
// 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 のインスタンスです)
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 で操作することができます。
// 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 ] ]
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 を参照することができます。
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
をしたときに ワーカースレッド側で呼び出されます。ここでノードごとのパラメータの初期化を行います。
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
を使用しています。
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 デモ (オーディオファイルをドラッグ&ドロップすると開始します)
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 });
});
});
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 デモ (オーディオファイルをドラッグ&ドロップすると開始します)
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);
});
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 デモ (オーディオファイルをドラッグ&ドロップすると開始します)