波形を処理するJavaScript … 4(webaudioawp-1.0.1.js)
4.のJavaScriptのプログラムが起動すると、processメソッド(この名前は仕様で規定されています)が繰り返し呼び出されます。第2引数として渡される配列(この例ではoutputs)に、波形データを格納することで、音声が出力されます。outputsは、配列の配列で、チャンネル数分の要素がありますが、今回は単音ですので、outputs[0]に値を格納します。
outputs[0]の各要素(outputs[0][n])が、出力波形の1サンプルとなります。つまり、実行環境のサンプリングレートが44.1kHzであれば、毎秒44100個、48kHzであれば、毎秒48000個のデータを配列に格納してやる必要があります。
実行環境によりますが、outputs[0]の要素数は128個前後のようです。ということは、サンプリングレートが48kHzの環境であれば、1秒間にprocessメソッドが375回呼ばれることになります。今回のプログラムでは、実際の波形の計算は同じクラス内のgetWaveメソッドで行っています。
ここでは、波形の値は、-1から+1までの範囲の値で配列に格納します。今回のプログラムでは、波形の値の計算にMath.sin(JavaScriptの組み込み関数である正弦関数)を利用しており、この関数の戻り値はちょうど-1〜+1なので、それをそのまま出力していますが、通常は計算結果をこの範囲に収まるように補正(変換)する必要があります。
指定された周波数(=高さ)の音を出すには、周波数をnとすると、1/n秒で波形が一巡して元に戻るようにする必要があります。そのためには、1/n秒が先ほどのoutputs[0]の要素何個分なのかを知る必要があります。
その数は、次のような計算で求められます。
実行環境のサンプリングレート ÷ 周波数n
実行環境のサンプリグレートは、AudioContextのsampleRateというプロパティに値(単位はHz)で格納されていますので、3.のJavaScriptで取得し、addModuleメソッドで4.のJavaScriptを読み込む時に、rateという名前のパラメータで引き渡しています。
4.のJavaScriptでは、それをsamplingRateという名前の引数として受け取り、利用しています。
上記の式で求めた要素数(今回のプログラムではscale変数)に対して、今何個目を計算しているのかをcounter変数で管理し、それに基づいて波形の値を計算しています。
例えば、440Hz(「ラ」の音)であれば、実行環境のサンプリングレートが48kHzの場合、1周期が配列の要素109個になります。今回のサンプルであれば、Math.sin(0)からMath.sin(Math.PI*2)で1周期ですから、0からMath.PI*2の値を109段階に分割してsinの値を求め、結果をoutputs[0][n]〜outputs[0][n+108]に格納していくことになります。
なお、processメソッドの中ですべての計算を行わず、getWaveメソッド(を含むオブジェクト)に外出ししているのは、このcounter変数の値を保持するためです。
class WAPAudioWorklet extends AudioWorkletProcessor {
constructor(options) {
super();
var WEBAUDIOAWP_internal = function (samplingRate) {
var counter = 0;
var toneFreqHz = 0;
var result;
var scale = toneFreqHz == 0 ? samplingRate / toneFreqHz : 0; // 発声する周波数と実行環境のサンプリングレートの比
// 波形データ生成
this.getWave = function () {
if (toneFreqHz == 0) return 0;
// 正弦波
result = Math.sin(Math.PI * 2 * counter / scale);
counter++;
if (counter > scale) counter = 0;
return (result); // -1 〜 +1
};
this.setFrequency = function (freq) {
toneFreqHz = freq;
counter = 0;
scale = toneFreqHz != 0 ? samplingRate / toneFreqHz : 0;
};
};
var sampleRate;
if (options.processorOptions) {
sampleRate = options.processorOptions.rate;
}
this.wap = new WEBAUDIOAWP_internal(sampleRate);
this.enable = false;
this.port.onmessage = (event) => {
const message = event.data;
switch (message.message) {
case 'frequency':
// 発音周波数
this.wap.setFrequency(message.freq);
break;
}
}
}
// AudioWorkletProcessor interface
process(inputs, outputs, parameters) {
if (!this.enable) {
this.enable = true;
this.port.postMessage({
message: 'enabled'
});
}
var output = outputs[0];
var len = output[0] ? output[0].length : 0;
for (var i = 0; i < len; i++) {
const out = this.wap.getWave();
output[0][i] = out;
}
return true;
}
}
registerProcessor("WEBAUDIO", WAPAudioWorklet);