Web Audio APIで音源ファイルを使わずにブラウザから音を出す

プログラミング

波形を処理する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]の要素何個分なのかを知る必要があります。

このグラフの場合、0から2πまでで1周期。440Hzなら、この1周期が1/440秒。

その数は、次のような計算で求められます。

実行環境のサンプリングレート ÷ 周波数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);