AnalyserNode

Web Audio APIにおいて, サウンドの視覚化のために定義されているのが, AnalyserNodeクラスです. そして, インスタンスの生成には, AudioContextインスタンスのcreateAnalyerメソッドを利用します. AnalyserNodeクラスもAudioNodeクラスを (プロトタイプ) 継承してます. したがって, connect / disconnectメソッドを呼び出すことが可能です.

サンプルコード 01


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of AnalyserNode
var analyser = context.createAnalyser();

// Create the instance of OscillatorNode
var oscillator = context.createOscillator();

// for legacy browsers
oscillator.start = oscillator.start || oscillator.noteOn;
oscillator.stop  = oscillator.stop  || oscillator.noteOff;

// OscillatorNode (Input) -> AnalyserNode (Visualization) -> AudioDestinationNode (Output)
oscillator.connect(analyser);
analyser.connect(context.destination);

ノード接続

図3 - 2 - a. ノード接続

AnalyserNodeインスタンスは以下のように定義されています.

表3 - 2 - a. AnalyserNodeインスタンスのプロパティ
PropertyDescription
fftSize高速フーリエ変換のデータサイズ
frequencyBinCountfftSizeプロパティの1 / 2の値
minDecibelsgetByteFrequencyDataメソッドで取得可能なデシベルの下限
maxDecibelsgetByteFrequencyDataメソッドで取得可能なデシベルの上限
smoothingTimeConstant周波数領域の波形 (振幅スペクトル) 描画に関連するプパティ
getByteTimeDomainData(array)時間領域の波形データを取得するメソッド
getByteFrequencyData(array)周波数領域の波形データ (振幅スペクトル) を取得するメソッド
getFloatFrequencyData(array)周波数領域の波形データ (振幅スペクトル) をデシベル単位で取得するメソッド

デモ 03

時間領域の波形描画

AnalyserNodeインスタンスのgetByteTimeDomainDataメソッドで, 時間領域の波形描画に必要なデータが取得可能です. getByteTimeDomainDataメソッドは, Uint8Array型の配列を引数にとります. この配列に波形描画に必要なデータが格納されます.

引数に指定するUint8Array型の配列のサイズの上限はfftSizeプロパティの値です. これより大きい値を指定してもエラーは発生しませんが, 意味はありません. なぜなら, getByteTimeDomainメソッドは, fftSizeプロパティの単位で時間領域の波形データを取得するので, fftSizeプロパティ以上のインデックスに対応する配列の要素は0になるからです. 具体的には, fftSizeプロパティのデフォルト値は2048ですが, これより大きな配列を指定しても, インデックスの2048以降の要素は0になるだけです.

getByteTimeDomainDataとfftSize

図3 - 2 - b. getByteTimeDomainDataとfftSize

fftSizeプロパティのサイズの配列を指定すれば, あるタイミングにおける時間領域と周波数領域のデータが互いに対応することになります. そして, fftSizeプロパティがとりうる値は高速フーリエ変換の高速性が発揮できる条件である2のべき乗, 具体的には, 32, 64, 128, 256, 512, 1024, 2048のいずれかと定義されています.

getByteTimeDomainDataメソッドが定期的に実行されるように, コールバック関数をsetIntervalメソッドやrequestAnimationFrameメソッドの引数に指定します.

サンプルコード 02


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);
}, 500);

波形データは, 0 ~ 255, つまり, 符号なし整数 (unsigned int) 1バイトの範囲で配列に格納されます. 振幅が1で考えると, 1が255, 0 (無音) が128, -1が0に対応しています. この値の関係に基づいて, Canvasに描画します.

まずは, y座標を算出します. そのために, 配列に格納されている値を最大値である255で割って正規化します.

サンプルコード 03


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    for (var i = 0, len = times.length; i < len; i++) {
        var y = times[i] / 255;  // 0 - 1
    }
}, 500);

正規化によって0 ~ 1の間に値が収まるようになります. この値は, Canvasのy座標の比率, すなわち, Canvasの高さを1としたときのy座標となります.

ただし, Canvasのy座標は下向きに増加します. したがって, 波形データが1 (255) となる点は, y座標の比率を0としたいはずです. 同じように, 0.5 (128) となる点の比率は0.5, 0 (0) となる点の比率は1としたいはずです.

y座標の算出

図3 - 2 - c. y座標の算出

しかし, サンプルコード 03のままだと実際の波形とは上下が反転した波形となってしまいます (1のときには比率1, 0のときには比率0). そこで, 1と正規化した波形データとの差分をとることによって, 反転したy座標の比率ではなく, 本来のy座標の比率を算出します.

サンプルコード 04


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    for (var i = 0, len = times.length; i < len; i++) {
        var y = 1 - (times[i] / 255);
    }
}, 500);

算出された値はCanvasの高さを1としたときのy座標なので, この値にCanvasの高さを乗算することで, y座標を算出可能です.

サンプルコード 05


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    for (var i = 0, len = times.length; i < len; i++) {
        var y = (1 - (times[i] / 255)) * canvas.height;
    }
}, 500);

これで, y座標の算出ができました. あとは, x座標の算出です.

x座標の算出も考え方はy座標の算出と同じです. つまり, Canvasの幅を1としたときのx座標を算出して, Canvasの幅を乗算するだけです.

x座標の算出

図3 - 2 - d. x座標の算出

サンプルコード 06


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    for (var i = 0, len = times.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (times[i] / 255)) * canvas.height;
    }
}, 500);

以上で, 描画に必要な座標計算は完了です.

あとは, パスを定義して描画します. パスの開始位置 (moveToメソッドの引数) は, インデックス0におけるx座標とy座標です. また, 描画処理を繰り返すので, 前回の描画結果を消去する処理も実行します.

サンプルコード 07


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw sound wave
    canvasContext.beginPath();

    for (var i = 0, len = times.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (times[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }
    }

    canvasContext.stroke();
}, 500);

最後に, サウンド開始と同時に描画も開始, また, サウンド停止と同時に描画も停止するようにイベントリスナーを設定します (そもそも, サウンドを開始しないと波形描画を実行する必要がありません).

サンプルコード 08


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of AnalyserNode
var analyser = context.createAnalyser();

analyser.fftSize = 2048;  // The default value

// for the instance of OscillatorNode
var oscillator = null;

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = null;

// Flag for starting or stopping sound
var isStop = true;

document.body.addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    // Create the instance of OscillatorNode
    oscillator = context.createOscillator();

    // for legacy browsers
    oscillator.start = oscillator.start || oscillator.noteOn;
    oscillator.stop  = oscillator.stop  || oscillator.noteOff;

    // OscillatorNode (Input) -> AnalyserNode (Visualization) -> AudioDestinationNode (Output)
    oscillator.connect(analyser);
    analyser.connect(context.destination);

    oscillator.start(0);

    intervalid = window.setInterval(function() {
        // Get data for drawing sound wave
        var times = new Uint8Array(analyser.fftSize);
        analyser.getByteTimeDomainData(times);

        // Clear previous data
        canvasContext.clearRect(0, 0, canvas.width, canvas.height);

        // Draw sound wave
        canvasContext.beginPath();

        for (var i = 0, len = times.length; i < len; i++) {
            var x = (i / len) * canvas.width;
            var y = (1 - (times[i] / 255)) * canvas.height;

            if (i === 0) {
                canvasContext.moveTo(x, y);
            } else {
                canvasContext.lineTo(x, y);
            }
        }

        canvasContext.stroke();
    }, 500);

    isStop = false;
}, false);

document.body.addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

    oscillator.stop(0);

    if (intervalid !== null) {
        window.clearInterval(intervalid);
        intervalid = null;
    }

    isStop = true;
}, false);

ここまでのまとめとしてデモ 04を実行してみてください. このデモでは, Canvasのサイズいっぱいに波形を描画するのではなく, 少し余白を設定していますが, 座標を算出する考え方はサンプルコードと同じです.

デモ 04

グリッドとテキストの描画

時間領域の波形描画は実装できましたが, もっとアナライザーらしくしたい場合もあるかと思います. そこで, このセクションでは波形描画に追加して, グリッドとテキスト (数値) を描画する処理を解説します.

まずは, y軸方向のグリッドとテキストを描画していきましょう. あまり細かくグリッドを描画すると見づらいだけなので, 今回はサンプルとして, 1, 0, -1の3つの振幅値に対応するグリッドとテキストを描画しましょう.

y軸方向に関しては, 波形データの描画と同じ考え方です. すなわち, 1に対応するグリッドのy座標の比率は0, 0に対応するグリッドのy座標の比率は0.5, -1に対応するグリッドのy座標の比率は1となることです.

また, グリッドは高さ1 pxの矩形塗りつぶしで描画することにします.

テキストのx座標はどこでもOKですが, グラフらしさを重視してサンプルではCanvasの左端に描画することにします.

サンプルコード 09


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw sound wave
    canvasContext.beginPath();

    for (var i = 0, len = times.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (times[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.00', '-1.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = ((1 - parseFloat(text)) / 2) * canvas.height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, canvas.width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
}, 500);

次に, x軸方向のグリッドとテキストを描画していきましょう.

まず, グリッドは波形を描画するときに算出したx座標を利用します. ただし, すべての要素のx座標に対して描画すると見づらいだけなので, 今回はサンプルとして, 5 msecの間隔で描画しましょう.

さて, ここで問題となるのが「5 msecごと」をどのように算出するか?です. y軸方向のグリッドとテキストを描画した場合と異なり, 物理量が直接対応していない, すなわち, 配列のインデックスに対して時間となっているので, 配列のインデックスを時間に変換するために音信号処理の知識が必要となります.

時間領域の波形データが格納される配列のインデックスの間隔は, サンプリング周期に対応しています (図3 - 2 - e). したがって, 配列のインデックスにサンプリング周期を乗算することで, x軸方向の物理量である時間を算出可能です.

配列のインデックスとサンプリング周期

図3 - 2 - e. 配列のインデックスとサンプリング周期

5 msecごとにグリッドとテキストを描画するためのコード例は, サンプルコード 10のようになるでしょう. ちなみに, グリッドは幅1 pxの矩形塗りつぶし, テキストはCanvasの下端に描画しています.

サンプルコード 10


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

// Sampling Period
var period = 1 / context.sampleRate;

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw sound wave
    canvasContext.beginPath();

    for (var i = 0, len = times.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (times[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }

        var sec  = i * period;             // index -> time
        var msec = sec * Math.pow(10, 3);  // sec -> msec

        // 5 msec ?
        if ((msec % 5) === 0) {
            var text = Math.round(msec) + ' msec';

            // Draw grid (X)
            canvasContext.fillRect(x, 0, 1, canvas.height);

            // Draw text (X)
            canvasContext.fillText(text, x, canvas.height);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.00', '-1.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = ((1 - parseFloat(text)) / 2) * canvas.height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, canvas.width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
}, 500);

配列のインデックスにサンプリング周期を乗算するために, サンプリング周波数の逆数, すなわち, AudioContextインスタンスのsampleRateプロパティの逆数を乗算していることに着目してください.

配列のインデックスを時間に変換したら, ミリ秒単位に換算して5 msecとの剰余を判定することで, 5 msecごとにグリッドとテキストを描画しています.

以上で理論上はうまくいくはずなのですが, 残念ながら描画されない場合があります. 小数点演算が絡むので, 5 msecとなるはずのインデックスで剰余が0にならない場合があるからです.

そこで, 逆算の方式で処理します. 具体的には, 5 msecごとのサンプリング数 (配列のサイズ) を算出します. そのために, 間隔となる時間にサンプリング周波数を乗算します. そして, 配列のインデックスと5 msecごとのサンプリング数との剰余を判定するというアルゴリズムです.

サンプルコード 11


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

// Sampling period
var period = 1 / context.sampleRate;

// This value is the number of samples during 5 msec
var n5msec = Math.floor(5 * Math.pow(10, -3) * context.sampleRate);

var intervalid = window.setInterval(function() {
    // Get data for drawing sound wave
    var times = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(times);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw sound wave
    canvasContext.beginPath();

    for (var i = 0, len = times.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (times[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }

        // 5 msec ?
        if ((i % n5msec) === 0) {
            var sec  = i * period;             // index -> time
            var msec = sec * Math.pow(10, 3);  // sec -> msec
            var text = Math.round(msec) + ' msec';

            // Draw grid (X)
            canvasContext.fillRect(x, 0, 1, canvas.height);

            // Draw text (X)
            canvasContext.fillText(text, x, canvas.height);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.00', '-1.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = ((1 - parseFloat(text)) / 2) * canvas.height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, canvas.width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
}, 500);

実際に試してみるとわかりますが, サンプルコード 11であれば意図したとおりにグリッドとテキストが描画されます.

ちなみに, サンプルコード 10で描画している時間は, fftSizeプロパティごとの相対的な時間です. 絶対時間を描画するのであれば, アクセスしたインデックス数を格納する変数が必要となるでしょう.

時間領域の波形描画のまとめとしてデモ 05を実行してみてください. これは, デモ 04にグリッドとテキストの描画を追加したデモです. 少し余白を設定しているので, 座標計算がちょっと複雑になっていますが, 考え方はサンプルコードと同じです.

デモ 05

周波数領域の波形描画

AnalyserNodeインスタンスのgetByteFrequencyDataメソッドで, 周波数領域の波形 (振幅スペクトル) 描画に必要なデータが取得可能です. そして, このメソッドの引数も, Uint8Array型の配列です. この配列に波形描画に必要なデータが格納されます.

引数のUint8Array型の配列のサイズの上限は, frequencyBinCountプロパティ (fftSizeプロパティの1 / 2) の値です. これより大きい値を指定してもエラーは発生しませんが, 意味はありません. なぜなら, fftSizeプロパティの1 / 2, つまり, frequencyBinCountプロパティ以上のインデックスに対応する配列の要素は0になるからです.

振幅スペクトルの形状は, 高速フーリエ変換のサイズ (fftSizeプロパティ) の1 / 2 - 1 のインデックスを軸に線対称になります. つまり, 前半のデータが取得できれば後半のデータは不要ということです.

また, 振幅スペクトルの形状が線対称となることに関係しているのが, サンプリング定理です. サンプリング周波数の1 / 2, すなわち, ナイキスト周波数より高い周波数は元のアナログ信号に復元不可能という定理でした. そして, fftSizeプロパティ (高速フーリエ変換のサイズ) の1 / 2のインデックスに対応する周波数は, ナイキスト周波数となります. したがって, fftSizeプロパティ (高速フーリエ変換のサイズ) の1 / 2以上のインデックスに対応する周波数成分は, 元のアナログ信号に復元不可能なので, スペクトルとして意味をもちません. つまり, 言い換えると, fftSizeプロパティ (高速フーリエ変換のサイズ) の1 / 2より小さいインデックスに対応する要素のみ, すなわち, ナイキスト周波数以下の周波数に対応する要素のみがスペクトルとして意味をもつということです.

Web Audio APIでは, サンプリング定理と高速フーリエ変換の性質から, frequencyBinCountプロパティ以上のインデックスに対応する配列の要素は0が格納されるという実装になっています.

また, frequencyBinCountプロパティのサイズの配列を指定すれば, あるタイミングの時間領域と周波数領域のデータが互いに対応します. そして, fftSizeプロパティがとりうる値は, 32, 64, 128, 256, 512, 1024, 2048のいずれかと定義されているので, frequencyBinCountプロパティがとりうる値はそれぞれの1 / 2の値である, 16, 32, 64, 128, 256, 512, 1024のいずれかですが, 読み取り専用なので値を直接設定することはありません. 値を変更したい場合には, fftSizeプロパティの値を変更します.

getByteFrequencyDataメソッドが定期的に実行されるように, コールバック関数をsetIntervalメソッドやrequestAnimationFrameメソッドの引数に指定します.

サンプルコード 12


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);
}, 500);

波形データは, 0 ~ 255, つまり, 符号なし整数 (unsigned int) 1バイトの範囲で配列に格納されます. 振幅が1で考えると, 1が255, 0 (無音) が0に対応しています. この値の関係に基づいてCanvasに描画します.

まずは, y座標を算出します. そのために, 配列に格納されている値を最大値である255で割って正規化します.

サンプルコード 13


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var y = spectrums[i] / 255;  // 0 - 1
    }
}, 500);

正規化によって0 ~ 1の間に値が収まるようになります. この値は, Canvasのy座標の比率, すなわち, Canvasの高さを1としたときのy座標となります.

ただし, Canvasのy座標は下向きに増加します. したがって, 波形データが1 (255) となる点は, y座標の比率を0としたいはずです. 同じように, 0 (0) となる点の比率は1としたいはずです.

y座標の算出

図3 - 2 - h. y座標の算出

しかし, サンプルコード 13のままだと実際の波形とは上下が反転した波形となってしまいます (1のときには比率1, 0のときには比率0) . そこで, 1と正規化した波形データとの差分をとることによって, 反転したy座標の比率ではなく, 本来のy座標の比率を算出します.

サンプルコード 14


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var y = 1 - (spectrums[i] / 255);
    }
}, 500);

算出された値はCanvasの高さを1としたときのy座標なので, この値にCanvasの高さを乗算することで, y座標を算出可能です.

サンプルコード 15


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var y = (1 - (spectrums[i] / 255)) * canvas.height;
    }
}, 500);

これで, y座標の算出ができました. あとは, x座標の算出です.

x座標の算出も考え方はy座標の算出と同じです. つまり, Canvasの幅を1としたときのx座標を算出して, Canvasの幅を乗算するだけです.

x座標の算出

図3 - 2 - i. x座標の算出

サンプルコード 16


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (spectrums[i] / 255)) * canvas.height;
    }
}, 500);

以上で, 描画に必要な座標計算は完了です.

あとは, パスを定義して描画します. パスの開始位置 (moveToメソッドの引数) は, インデックス0におけるx座標とy座標です. また, 描画処理を繰り返すので, 前回の描画結果を消去する処理も実行します.

サンプルコード 17


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw spectrum
    canvasContext.beginPath();

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (spectrums[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }
    }

    canvasContext.stroke();
}, 500);

最後に, サウンド開始と同時に描画も開始, また, サウンド停止と同時に描画も停止するようにイベントリスナーを設定します (そもそも, サウンドを開始しないと波形描画を実行する必要がありません).

サンプルコード 18


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of AnalyserNode
var analyser = context.createAnalyser();

analyser.fftSize = 2048;  // The default value

// for the instance of OscillatorNode
var oscillator = null;

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = null;

// Flag for starting or stopping sound
var isStop = true;

document.body.addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    // Create the instance of OscillatorNode
    oscillator = context.createOscillator();

    // for legacy browsers
    oscillator.start = oscillator.start || oscillator.noteOn;
    oscillator.stop  = oscillator.stop  || oscillator.noteOff;

    // OscillatorNode (Input) -> AnalyserNode (Visualization) -> AudioDestinationNode (Output)
    oscillator.connect(analyser);
    analyser.connect(context.destination);

    oscillator.start(0);

    intervalid = window.setInterval(function() {
        // Get data for drawing spectrum
        var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
        analyser.getByteFrequencyData(spectrums);

        // Clear previous data
        canvasContext.clearRect(0, 0, canvas.width, canvas.height);

        // Draw spectrum
        canvasContext.beginPath();

        for (var i = 0, len = spectrums.length; i < len; i++) {
            var x = (i / len) * canvas.width;
            var y = (1 - (spectrums[i] / 255)) * canvas.height;

            if (i === 0) {
                canvasContext.moveTo(x, y);
            } else {
                canvasContext.lineTo(x, y);
            }
        }

        canvasContext.stroke();
    }, 500);

    isStop = false;
}, false);

document.body.addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

    oscillator.stop(0);

    if (intervalid !== null) {
        window.clearInterval(intervalid);
        intervalid = null;
    }

    isStop = true;
}, false);

周波数領域の波形描画も時間領域の波形描画とほとんど同じ処理です.

ここまでのまとめとしてデモ 06を実行してみてください. このデモでは, Canvasのサイズいっぱいに波形を描画するのではなく, 少し余白を設定していますが, 座標を算出する考え方はサンプルコードと同じです.

デモ 06

デモ 06では周波数領域の波形描画において有効な smoothingTimeConstantプロパティの設定が可能です. このプロパティは, 0から1までの数値型で, 0に近いほど描画の更新がスムーズになり, 1に近いほど描画の更新が鈍くなります. 値を変更しても, 時間領域の波形描画には影響しないという仕様になっています.

グリッドとテキストの描画

周波数領域の波形描画は実装できましたが, もっとアナライザーらしくしたい場合もあると思います. そこで, このセクションでは波形描画に追加して, グリッドとテキスト (数値) を描画する処理を解説します.

まずは, y軸方向のグリッドとテキストを描画していきましょう. あまり細かくグリッドを描画すると見づらいだけなので, 今回はサンプルとして, 1, 0, -1の3つの振幅値に対応するグリッドとテキストを描画しましょう.

y軸方向に関しては, 波形データの描画と同じ考え方です. すなわち, 1に対応するグリッドのy座標の比率は0, 0に対応するグリッドのy座標の比率は0.5, -1に対応するグリッドのy座標の比率は1となることです.

また, グリッドは高さ1 pxの矩形塗りつぶしで描画することにします.

テキストのx座標はどこでもOKですが, グラフらしさを重視してサンプルではCanvasの左端に描画することにします.

サンプルコード 19


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw spectrum
    canvasContext.beginPath();

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (spectrums[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.50', '0.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = (1 - parseFloat(text)) * canvas.height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, canvas.width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
}, 500);

次に, x軸方向のグリッドとテキストを描画していきましょう.

まず, グリッドは波形を描画するときに算出したx座標を利用します. ただし, すべての要素のx座標に対して描画すると見づらいだけなので, 今回はサンプルとして, 500 Hzの間隔で描画しましょう.

さて, ここで問題となるのが「500 Hzごと」をどのように算出するか?です. y軸方向のグリッドとテキストを描画した場合と異なり, 物理量が直接対応していない, つまり, 配列のインデックスに対して周波数となっているので, 配列のインデックスを周波数に変換するために音信号処理の知識が必要となります.

サンプリング周波数をfs, 高速フーリエ変換のサイズ (fftSizeプロパティの値) をNと表現すると, 周波数領域の波形データが格納される配列のインデックスの間隔はfs / Nとなります. この値は, サンプリング周波数を高速フーリエ変換のサイズで除算した値です. つまり, 配列のインデックスにfs / Nを乗算することによって, x軸方向の物理量である時間を算出可能です.

配列のインデックスと周波数

図3 - 2 - j. 配列のインデックスと周波数

500 Hzごとにグリッドとテキストを描画するためのコード例は, サンプルコード 20のようになるでしょう. ちなみに, グリッドは幅1 pxの矩形塗りつぶし, テキストはCanvasの下端に描画しています.

サンプルコード 20


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

// Frequency Resolution
var fsDivN = context.sampleRate / analyser.fftSize;

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw spectrum
    canvasContext.beginPath();

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (spectrums[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }

        var f = Math.floor(i * fsDivN);  // index -> frequency

        // 500 Hz ?
        if ((f % 500) === 0) {
            var text = (f < 1000) ? (f + ' Hz') : ((f / 1000) + ' kHz');

            // Draw grid (X)
            canvasContext.fillRect(x, 0, 1, canvas.height);

            // Draw text (X)
            canvasContext.fillText(text, x, canvas.height);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.50', '0.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = (1 - parseFloat(text)) * canvas.height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, canvas.width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
}, 500);

以上で理論上はうまくいくはずなのですが, 残念ながら描画されない場合があります. 小数点演算が絡むので, 500 Hzとなるはずのインデックスで剰余が0にならない場合があるからです.

そこで, 逆算の方式で処理します. 具体的には, 500 Hzごとのサンプリング数 (配列のサイズ) を算出します. そのために, 間隔となる周波数をfs / Nで除算 (周波数にN / fsを乗算) します. そして, 配列のインデックスと500 Hzごとのサンプリング数との剰余を判定するというアルゴリズムです.

サンプルコード 21


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

// Frequency Resolution
var fsDivN = context.sampleRate / analyser.fftSize;

// This value is the number of samples during 500 Hz
var n500Hz = Math.floor(500 / fsDivN);

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum
    var spectrums = new Uint8Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getByteFrequencyData(spectrums);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw spectrum
    canvasContext.beginPath();

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (1 - (spectrums[i] / 255)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }

        // 500 Hz ?
        if ((i % n500Hz) === 0) {
            var f    = Math.floor(500 * (i / n500Hz));  // index -> frequency
            var text = (f < 1000) ? (f + ' Hz') : ((f / 1000) + ' kHz');

            // Draw grid (X)
            canvasContext.fillRect(x, 0, 1, canvas.height);

            // Draw text (X)
            canvasContext.fillText(text, x, canvas.height);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.50', '0.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = (1 - parseFloat(text)) * canvas.height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, canvas.width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
}, 500);

実際に試してみるとわかりますが, サンプルコード 21であれば意図したとおりにグリッドとテキストが描画されます. だだし, Canvasの幅をかなり長く設定するか, すべての波形データを描画するのではなく, 1 / 4か1 / 8ぐらいのサイズに限定して描画しないと, テキストがきれいに描画できません.

周波数領域の波形描画のまとめとしてデモ 07を試してみてください. これは, デモ 06にグリッドとテキストの描画を追加したデモです. 少し余白を設定しているので, 座標計算がちょっと複雑になっていますが, 考え方はサンプルコードと同じです.

デモ 07

ところで, サンプリング周波数をfs, 高速フーリエ変換のサイズをNと表現すると, 周波数領域の波形データが格納される配列のインデックスの間隔がfs / Nに対応することに関して2点ほど補足の解説をしておきます.

1つは, fs / Nと高速フーリエ変換の1 / 2のサイズN / 2 (frequencyBinCountプロパティの値) を乗算してみてください. その値はfs / 2で, これは, サンプリング周波数の1 / 2の周波数 (ナイキスト周波数) を意味することになります. これで, 高速フーリエ変換のサイズの1 / 2のインデックスに対応する周波数が, サンプリング周波数の1 / 2の周波数となる理由が理解できると思います (図3 - 2 - j).

もう1つは, 少し専門的なことになってしまうので, スルーしてもらって構いません.

周波数分解能について

fs / Nには, 周波数分解能という用語が利用されます. これは, スペクトルにおいて, どれくらいの精度で周波数を表現できるか?を表します. 具体的に解説すると, サンプリング周波数は定数 (48000とします) と見なせるので, 変更可能なパラメータは高速フーリエ変換のサイズを表すNです. ここで, Nが32と2048のfs / Nはそれぞれ1500と23.4375となります. この値 (周波数分解能) と配列のインデックスを乗算して周波数を算出可能でした. つまり, Nが32の場合には, 1500 Hzの粗い精度でしか周波数を測定できないのに対して, Nが2048の場合には, 約23 Hzの細かい精度で周波数を測定できます. 専門的には, Nが2048の場合のほうが周波数分解能が高いということです.

しかし, 高速フーリエ変換のサイズは常に最大値の2048がベストというわけではありません. 高速フーリエ変換のサイズであるfftSizeプロパティは時間領域の波形描画にも関係するパラメータだからです. 高速フーリエ変換のサイズを大きくするほど, より長い時間に対応することになってしまい, どの時間におけるスペクトルなのか?がぼやけてしまいます. ちなみに, 専門的には時間分解能が低いと表現します.

周波数分解能と時間分解能

図3 - 2 - k. 周波数分解能と時間分解能

つまり, 周波数分解能と時間分解能はトレードオフの関係にあります. したがって, 両方を高くすることはできず, 一方を高くすると, もう一方は低くなってしまいます. しかし, だからこそ, fftSizeプロパティのとりうる値が複数定義されていると言えます.

周波数領域の波形描画 (デシベル)

AnalyserNodeインスタンスのgetFloatFrequencyDataメソッドも周波数領域の波形 (振幅スペクトル) 描画のためのメソッドです. getByteFrequencyDataメソッドとの違いは, デシベル (dB) という単位で波形データが格納されることです.

デシベルの解説はちょっと後回しにして, getFloatFrequencyDataメソッドによる波形描画を先に解説します.

メソッドの引数に指定するのは, Uint8Array型の配列ではなく, Float32Array型の配列です. そして, この配列に波形描画に必要なデータが格納されることは同じです. Float32Array型のサイズの上限はfrequencyBinCountプロパティの値です. 理由は, 既に解説したとおりです.

getFloatFrequencyDataとfrequencyBinCount

図3 - 2 - l. getFloatFrequencyDataとfrequencyBinCount

getFloatFrequencyDataメソッドが定期的に実行されるように, コールバック関数をsetIntervalメソッドやrequestAnimationFrameメソッドの引数に指定します.

サンプルコード 22


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var intervalid = window.setInterval(function() {
    // Get data for drawing spectrum (dB)
    var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getFloatFrequencyData(spectrums);
}, 500);

波形データは, デシベル単位で格納されています. getByteFrequencyDataメソッドのように格納される値の範囲が特定できません. したがって, 座標を算出するために, Canvasに対する描画上の範囲を決定します. 今回は, AnalyserNodeインスタンスのminDecibels / maxDecibelsプロパティのデフォルトの値を下限 / 上限に設定して, その間の値を範囲とします. そして, デフォルト値はそれぞれ, -100 dB / -30 dBと実装されているので, -100 dB ~ -30 dBが範囲となります.

minDecibels / maxDecibelsプロパティ

図3 - 2 - m. minDecibels / maxDecibelsプロパティ

まずは, y座標を算出します. そのために, 値から最大値を減算して, その値を描画範囲で除算して正規化します.

サンプルコード 23


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    var range = analyser.maxDecibels - analyser.minDecibels;  // 70 dB

    // Get data for drawing spectrum (dB)
    var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getFloatFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var y = (spectrums[i] - analyser.maxDecibels) / range;  // 0 - 1
    }
}, 500);

正規化によって0 ~ 1の間に値が収まるようになります. この値は, Canvasのy座標の比率, すなわち, Canvasの高さを1としたときのy座標となります.

ただし, Canvasのy座標は下向きに増加します. したがって, デシベルが最大値 (-30 dB) となる点は, y座標の比率を0としたいはずです. 同様に, 最小値 (-100 dB) となる点の比率は1としたいはずです.

y座標の算出

図3 - 2 - n. y座標の算出

しかし, サンプルコード 23のままだと実際の波形とは上下が反転した波形となってしまいます (-30 dBのときには比率1, -100 dBのときには比率0). そこで, -1を乗算することで, 反転したy座標の比率ではなく, 本来のy座標の比率を算出します.

サンプルコード 24


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    var range = analyser.maxDecibels - analyser.minDecibels;  // 70 dB

    // Get data for drawing spectrum (dB)
    var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getFloatFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var y = -1 * ((spectrums[i] - analyser.maxDecibels) / range);
    }
}, 500);

算出された値はCanvasの高さを1としたときのy座標なので, この値にCanvasの高さを乗算することで, y座標を算出可能です.

サンプルコード 25


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    var range = analyser.maxDecibels - analyser.minDecibels;  // 70 dB

    // Get data for drawing spectrum (dB)
    var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getFloatFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var y = (-1 * ((spectrums[i] - analyser.maxDecibels) / range)) * canvas.height;
    }
}, 500);

これで, y座標の算出ができました. あとは, x座標の算出です.

もっとも, x座標の算出はgetByteFrequencyDataメソッドの場合と同じです.

x座標の算出

図3 - 2 - o. x座標の算出

サンプルコード 26


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    var range = analyser.maxDecibels - analyser.minDecibels;  // 70 dB

    // Get data for drawing spectrum (dB)
    var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getFloatFrequencyData(spectrums);

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (-1 * ((spectrums[i] - analyser.maxDecibels) / range)) * canvas.height;
    }
}, 500);

以上で, 描画に必要な座標計算は完了です.

あとは, パスを定義して描画します. パスの開始位置 (moveToメソッドの引数) は, インデックス0におけるx座標とy座標です. また, 描画処理を繰り返すので, 前回の描画結果を消去する処理も実行します.

サンプルコード 27


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    var range = analyser.maxDecibels - analyser.minDecibels;  // 70 dB

    // Get data for drawing spectrum (dB)
    var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getFloatFrequencyData(spectrums);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw spectrum (dB)
    canvasContext.beginPath();

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (-1 * ((spectrums[i] - analyser.maxDecibels) / range)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }
    }

    canvasContext.stroke();
}, 500);

最後に, サウンド開始と同時に描画も開始, また, サウンド停止と同時に描画も停止するようにイベントリスナーを設定します (そもそも, サウンドを開始しないと波形描画を実行する必要がありません).

サンプルコード 28


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of AnalyserNode
var analyser = context.createAnalyser();

analyser.fftSize = 2048;  // The default value

// for the instance of OscillatorNode
var oscillator = null;

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = null;

// Flag for starting or stopping sound
var isStop = true;

document.body.addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    // Create the instance of OscillatorNode
    oscillator = context.createOscillator();

    // for legacy browsers
    oscillator.start = oscillator.start || oscillator.noteOn;
    oscillator.stop  = oscillator.stop  || oscillator.noteOff;

    // OscillatorNode (Input) -> AnalyserNode (Visualization) -> AudioDestinationNode (Output)
    oscillator.connect(analyser);
    analyser.connect(context.destination);

    oscillator.start(0);

    intervalid = window.setInterval(function() {
        var range = analyser.maxDecibels - analyser.minDecibels;  // 70 dB

        // Get data for drawing spectrum (dB)
        var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
        analyser.getFloatFrequencyData(spectrums);

        // Clear previous data
        canvasContext.clearRect(0, 0, canvas.width, canvas.height);

        // Draw spectrum (dB)
        canvasContext.beginPath();

        for (var i = 0, len = spectrums.length; i < len; i++) {
            var x = (i / len) * canvas.width;
            var y = (-1 * ((spectrums[i] - analyser.maxDecibels) / range)) * canvas.height;

            if (i === 0) {
                canvasContext.moveTo(x, y);
            } else {
                canvasContext.lineTo(x, y);
            }
        }

        canvasContext.stroke();
    }, 500);

    isStop = false;
}, false);

document.body.addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

    oscillator.stop(0);

    if (intervalid !== null) {
        window.clearInterval(intervalid);
        intervalid = null
    }

    isStop = true;
}, false);

ここまでのまとめとしてデモ 08を実行してみてください. このデモでは, Canvasのサイズいっぱいに波形を描画するのではなく, 少し余白を設定していますが, 座標を算出する考え方はサンプルコードと同じです.

デモ 08

グリッドとテキストの描画

まず, x軸方向のグリッドとテキストの描画はgetByteFrequencyDataメソッドの場合と同じです.

したがって, このセクションでは, y軸方向のグリッドとテキストを描画することに関して解説します. 今回はサンプルとして, -100 dB ~ -30 dBまでの範囲を10 dBの間隔で描画することにしましょう.

y軸方向に関しては, 波形データの描画と同じ考え方です. すなわち, デシベルが最大値 (-30 dB) に対応するグリッドのy座標の比率は0, 最小値 (-100 dB) に対応するグリッドのy座標の比率は1となることです (図3 - 2 - m) .

また, グリッドは高さ1 pxの矩形塗りつぶしで描画することにします.

テキストのx座標はどこでもOKですが, グラフらしさを重視してサンプルではCanvasの左端に描画することにします.

サンプルコード 29


/*
 * Add code to sample code 01
 */

// ....

analyser.fftSize = 2048;  // The default value

var canvas        = document.querySelector('canvas');
var canvasContext = canvas.getContext('2d');

var intervalid = window.setInterval(function() {
    var range = analyser.maxDecibels - analyser.minDecibels;  // 70 dB

    // Frequency resolution
    var fsDivN = context.sampleRate / analyser.fftSize;

    // This value is the number of samples during 500 Hz
    var n500Hz = Math.floor(500 / fsDivN);

    // Get data for drawing spectrum (dB)
    var spectrums = new Float32Array(analyser.frequencyBinCount);  // Array size is 1024 (half of FFT size)
    analyser.getFloatFrequencyData(spectrums);

    // Clear previous data
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);

    // Draw spectrum (dB)
    canvasContext.beginPath();

    for (var i = 0, len = spectrums.length; i < len; i++) {
        var x = (i / len) * canvas.width;
        var y = (-1 * ((spectrums[i] - analyser.maxDecibels) / range)) * canvas.height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }

        // 500 Hz ?
        if ((i % n500Hz) === 0) {
            var f    = Math.floor(500 * (i / n500Hz));  // index -> frequency
            var text = (f < 1000) ? (f + ' Hz') : ((f / 1000) + ' kHz');

            // Draw grid (X)
            canvasContext.fillRect(x, 0, 1, canvas.height);

            // Draw text (X)
            canvasContext.fillText(text, x, canvas.height);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    for (var i = analyser.minDecibels; i <= analyser.maxDecibels; i += 10) {
        var gy = (-1 * ((i - analyser.maxDecibels) / range)) * canvas.height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, canvas.width, 1);

        // Draw text (Y)
        canvasContext.fillText((i + ' dB'), 0, gy);
    }
}, 500);

デシベル単位の周波数領域の波形描画のまとめとしてデモ 09を実行してみてください. このデモは, デモ 08にグリッドとテキストの描画を追加しています. 少し余白を設定していますが, 座標算出の考え方はサンプルコードと同じです.

デモ 09

デシベル

ところで, デシベルとは何を意味しているのでしょうか? 騒音の大きさなどを表す単位として利用されたり, ヘッドフォンなどの音響機器の性能を表すために利用されたりするので聞いたことはあるという方は多いと思います.

音の大きさは, 波形の振幅が大きく影響すると解説しましたが, より正確には, その2乗であるパワーが大きく影響します. しかし, 値を2乗するのでパワーの示す範囲は非常に広範囲になってしまい, 人間の感覚とうまく対応しないという問題があります.

そこで, ある値を基準に対数をとることで, 値が広範囲になるという問題を解決した物理量が音圧レベルです. そして, 音圧レベルの単位がデシベル (dB) というわけです.

さらに, 対数で表すことの優位性のもう1つの根拠が, フェヒナーの法則という心理学の理論です. この法則は, 人間の感覚量は刺激強度の対数に比例するということを示しています.

20\log_{10}\left(\frac{\rm P_e}{\rm P_{e0}}\right)\ \ (\rm P_{e0} = 2 \times 10^{-5} [\rm P\rm a])

20\log_{10}\left|X(k)\right|

図3 - 2 - p. 音圧レベルの定義 (上は時間領域, 下は振幅スペクトル)

例として, 6 dB音圧レベルが大きくなれば音圧 (音圧のパワー) は約2倍 (20\log_{10}2) になります. 20 dB大きくなれば10倍 (20\log_{10}10) , 40 dB大きくなれば100倍 (20\log_{10}100)…という関係で, 本来であれば広範囲におよぶ値を対数をとることによって解決しています.

表3 - 2 - b. デシベル差と倍率
DifferenceMagnificationExample
0 dB× 1人間の聴力の限界
6 dB× 2
10 dB× 3
20 dB× 10木の葉のふれあう音
40 dB× 100図書館
60 dB× 1000会話
80 dB× 10000目覚まし時計
100 dB× 100000電車のガード下
120 dB× 1000000飛行機のエンジン付近

まとめると, デシベルとは音の大きさを人間にわかりやすく表す音圧レベルの単位ということです.

getByteFrequencyDataメソッドとの関係

getFloatFrequencyDataメソッドとgetByteFrequencyDataメソッドの関係について解説しておきます.

getFloatFrequencyDataメソッドで取得できる波形データは, 振幅スペクトルのデシベル単位での値でした. これらの値から, AnalyserNodeインスタンスのminDecibelsプロパティとmaxDecibelsプロパティの範囲内の値を抽出して, それを0から255に変換した値がgetByteFrequencyDataメソッドで取得できる波形データとなっています.

getFloatFrequencyDataメソッドとgetByteFrequencyDataメソッドの関係

図3 - 2 - q. getFloatFrequencyDataメソッドとgetByteFrequencyDataメソッドの関係

例えば, minDecibels / maxDecibelsプロパティがそれぞれデフォルト値であれば, -100 dBは0に, -30 dBは255に変換されます.

オーディオデータの波形描画

オーディオデータの場合, AnalyserNodeクラスによるリアルタイムの波形描画だけではなく, オーディオデータ全体の波形を描画したいというケースもあるかと思います.

このセクションでは, オーディオデータ全体の波形を描画する方法について解説します (オーディオデータ全体のスペクトルは時間分解能が低すぎてあまり意味がないので, 時間領域の波形描画に限定します).

オーディオデータ全体の波形を描画する場合, AnalyserNodeクラスは必要ありません. その代わりに必要なのが, AudioBufferインスタンスのgetChannelDataメソッドです.

まず, AudioBufferインスタンスを生成します. そして, getChannelDataメソッドを利用して, オーディオデータ全体の波形データを取得します.

サンプルコード 30


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

var xhr = new XMLHttpRequest();
var url = 'http:// xxx.jp/sample.wav';

// XMLHttpRequest Level 2
xhr.responseType = 'arraybuffer';

xhr.onload = function() {
    if (xhr.status === 200) {
        var arrayBuffer = xhr.response;

        if (arrayBuffer instanceof ArrayBuffer) {
            // The 2nd argument for decodeAudioData
            var successCallback = function(audioBuffer) {
                // Get audio binary data for drawing wave

                var channelLs = new Float32Array(audioBuffer.length);
                var channelRs = new Float32Array(audioBuffer.length);

                // Stereo ?
                if (audioBuffer.numberOfChannels > 1) {
                    channelLs.set(audioBuffer.getChannelData(0));
                    channelRs.set(audioBuffer.getChannelData(1));
                } else if (audioBuffer.numberOfChannels > 0) {
                    channelLs.set(audioBuffer.getChannelData(0));
                } else {
                    window.alert('The number of channels is invalid.');
                    return;
                }
            };

            // The 3rd argument for decodeAudioData
            var errorCallback = function(error) {
                /* do something for error .... */
            };

            // Create the instance of AudioBuffer (Asynchronously)
            context.decodeAudioData(arrayBuffer, successCallback, errorCallback);
        }
    }
};

xhr.open('GET', url, true);
xhr.send(null);

重要なポイントは, getChannelDataメソッドはFloat32Array型の配列を返すということです .

getChannelDataメソッドの引数には, チャンネルを表す数値型 (左チャンネルは0, 右チャンネルは1) を指定しますが, オーディオデータのチャンネル数が2 (ステレオ) とは限りません. モノラルの場合, 引数に1を指定するとエラーが発生します. そこで, AudioBufferインスタンスのnumberOfChannelsプロパティでオーディオデータのチャンネル数を取得します.

以上の処理によって, 波形データを取得できれば, あとはCanvasに描画していくだけです. サンプルコードでは, 左チャンネルの波形描画のコードのみ示しますが, 右チャンネルの波形描画も本質的な処理は同じです.

波形データは, -1 ~ 1の範囲で配列に格納されています. つまり, 振幅を1とした場合にそのまま対応しています. また, この値は, Canvasのy座標の比率, すなわち, Canvasの高さを1としたときのy座標となります.

ただし, Canvasのy座標は下向きに増加します. したがって, 波形データが1となる点は, y座標の比率を0としたいはずです. 同様に, 0となる点の比率は0.5, -1となる点の比率は1としたいはずです. つまり, 取得した波形データのままだと意図した座標と整合しないので, y座標を調整する処理が必要になります.

y座標の算出

図3 - 2 - r. y座標の算出

x座標はCanvasの幅を1としたときの座標を算出して, Canvasの幅を乗算するだけです.

サンプルコード 31

x座標の算出

図3 - 2 - s. x座標の算出


/*
 * Add code to sample code 30
 */

// ....

// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
    // Get audio binary data for drawing wave

    var channelLs = new Float32Array(audioBuffer.length);
    var channelRs = new Float32Array(audioBuffer.length);

    // Stereo ?
    if (audioBuffer.numberOfChannels > 1) {
        channelLs.set(audioBuffer.getChannelData(0));
        channelRs.set(audioBuffer.getChannelData(1));
    } else if (audioBuffer.numberOfChannels > 0) {
        channelLs.set(audioBuffer.getChannelData(0));
    } else {
        window.alert('The number of channels is invalid.');
        return;
    }

    var canvas        = document.querySelector('canvas');
    var canvasContext = canvas.getContext('2d');

    var width  = canvas.width;
    var height = canvas.height;

    // Clear previous data
    canvasContext.clearRect(0, 0, width, height);

    // Draw audio wave
    canvasContext.beginPath();

    for (var i = 0, len = channelLs.length; i < len; i++) {
        var x = (i / len) * width;
        var y = ((1 - channelLs[i]) / 2) * height;

        if (i === 0) {
            canvasContext.moveTo(x, y);
        } else {
            canvasContext.lineTo(x, y);
        }
    }

    canvasContext.stroke();
};

// ....

オーディオデータのサイズが大きくなるほど描画に時間を要します. しかし, 時間を要してすべてのデータを描画しても, 波形の概形にはほとんど影響がありません. したがって, 実際のアプリケーションで利用する場合, データを一定間隔で描画するのがベターでしょう. サンプルコード 32は, 50 msecに対応するインデックスの要素のみを描画する例です.

サンプルコード 32


/*
 * Add code to sample code 30
 */

// ....

// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
    // Get audio binary data for drawing wave

    var channelLs = new Float32Array(audioBuffer.length);
    var channelRs = new Float32Array(audioBuffer.length);

    // Stereo ?
    if (audioBuffer.numberOfChannels > 1) {
        channelLs.set(audioBuffer.getChannelData(0));
        channelRs.set(audioBuffer.getChannelData(1));
    } else if (audioBuffer.numberOfChannels > 0) {
        channelLs.set(audioBuffer.getChannelData(0));
    } else {
        window.alert('The number of channels is invalid.');
        return;
    }

    var canvas        = document.querySelector('canvas');
    var canvasContext = canvas.getContext('2d');

    var width  = canvas.width;
    var height = canvas.height;

    // Sampling period
    var period = 1 / context.sampleRate;

    // This value is the number of samples during 50 msec
    var n50msec = Math.floor(50 * Math.pow(10, -3) * context.sampleRate);

    // Clear previous data
    canvasContext.clearRect(0, 0, width, height);

    // Draw audio wave
    canvasContext.beginPath();

    for (var i = 0, len = channelLs.length; i < len; i++) {
        // 50 msec ?
        if ((i % n50msec) === 0) {
            var x = (i / len) * width;
            var y = ((1 - channelLs[i]) / 2) * height;

            if (i === 0) {
                canvasContext.moveTo(x, y);
            } else {
                canvasContext.lineTo(x, y);
            }
        }
    }

    canvasContext.stroke();
};

// ....

ここまでのまとめとしてデモ 10を実行してみてください. このデモでは, Canvasのサイズいっぱいに波形を描画するのではなく, 少し余白を設定していますが, 座標を算出する考え方はサンプルコードと同じです.

デモ 10

グリッドとテキストの描画

オーディオデータ全体の波形描画はできましたが, もっとアナライザーらしくしたい場合もあると思います. このセクションでは波形描画に追加して, グリッドとそれに対応するテキスト (数値) を描画する処理を解説します.

まずは, y軸方向のグリッドとテキストを描画します. あまり細かくグリッドを描画すると見づらいだけなので, 今回はサンプルとして, 1, 0, -1の3つの振幅値に対応するグリッドとテキストを描画しましょう.

y軸方向に関しては, 波形データの描画と同様の考え方です. つまり, 1に対応するグリッドのy座標の比率は0, 同様に, 0に対応するグリッドのy座標の比率は0.5, -1に対応するグリッドのy座標の比率は1となることです.

また, グリッドは高さ1 pxの矩形塗りつぶしで描画することにします.

テキストのx座標は任意ですが, グラフらしさを重視してサンプルではCanvasの左端に描画することにします.

サンプルコード 33


/*
 * Add code to sample code 30
 */

// ....

// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
    // Get audio binary data for drawing wave

    var channelLs = new Float32Array(audioBuffer.length);
    var channelRs = new Float32Array(audioBuffer.length);

    // Stereo ?
    if (audioBuffer.numberOfChannels > 1) {
        channelLs.set(audioBuffer.getChannelData(0));
        channelRs.set(audioBuffer.getChannelData(1));
    } else if (audioBuffer.numberOfChannels > 0) {
        channelLs.set(audioBuffer.getChannelData(0));
    } else {
        window.alert('The number of channels is invalid.');
        return;
    }

    var canvas        = document.querySelector('canvas');
    var canvasContext = canvas.getContext('2d');

    var width  = canvas.width;
    var height = canvas.height;

    // Sampling period
    var period = 1 / context.sampleRate;

    // This value is the number of samples during 50 msec
    var n50msec = Math.floor(50 * Math.pow(10, -3) * context.sampleRate);

    // Clear previous data
    canvasContext.clearRect(0, 0, width, height);

    // Draw audio wave
    canvasContext.beginPath();

    for (var i = 0, len = channelLs.length; i < len; i++) {
        // 50 msec ?
        if ((i % n50msec) === 0) {
            var x = (i / len) * width;
            var y = ((1 - channelLs[i]) / 2) * height;

            if (i === 0) {
                canvasContext.moveTo(x, y);
            } else {
                canvasContext.lineTo(x, y);
            }
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.00', '-1.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = ((1 - parseFloat(text)) / 2) * height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
};

// ....

次に, x軸方向のグリッドとテキストを描画します.

まず, グリッドは波形を描画するときに算出したx座標を利用します. ただし, すべての要素のx座標に対して描画すると非常に見づらくなるだけなので, 今回はサンプルとして, 60 secの間隔で, 対応するグリッドとテキストを描画しましょう. グリッドは幅1 pxの矩形塗りつぶし, テキストはCanvasの下端に描画することにします.

サンプルコード 34


/*
 * Add code to sample code 30
 */

// ....

// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
    // Get audio binary data for drawing wave

    var channelLs = new Float32Array(audioBuffer.length);
    var channelRs = new Float32Array(audioBuffer.length);

    // Stereo ?
    if (audioBuffer.numberOfChannels > 1) {
        channelLs.set(audioBuffer.getChannelData(0));
        channelRs.set(audioBuffer.getChannelData(1));
    } else if (audioBuffer.numberOfChannels > 0) {
        channelLs.set(audioBuffer.getChannelData(0));
    } else {
        window.alert('The number of channels is invalid.');
        return;
    }

    var canvas        = document.querySelector('canvas');
    var canvasContext = canvas.getContext('2d');

    var width  = canvas.width;
    var height = canvas.height;

    // Sampling period
    var period = 1 / context.sampleRate;

    // This value is the number of samples during 50 msec
    var n50msec = Math.floor(50 * Math.pow(10, -3) * context.sampleRate);

    // This value is the number of samples during 60 sec
    var n60sec = Math.floor(60 * context.sampleRate);

    // Clear previous data
    canvasContext.clearRect(0, 0, width, height);

    // Draw audio wave
    canvasContext.beginPath();

    for (var i = 0, len = channelLs.length; i < len; i++) {
        // 50 msec ?
        if ((i % n50msec) === 0) {
            var x = (i / len) * width;
            var y = ((1 - channelLs[i]) / 2) * height;

            if (i === 0) {
                canvasContext.moveTo(x, y);
            } else {
                canvasContext.lineTo(x, y);
            }
        }

        // 60 sec ?
        if ((i % n60sec) === 0) {
            var sec  = i * period;  // index -> time
            var text = Math.round(sec) + ' sec';

            // Draw grid (X)
            canvasContext.fillRect(x, 0, 1, height);

            // Draw text (X)
            canvasContext.fillText(text, x, height);
        }
    }

    canvasContext.stroke();

    // Draw text and grid (Y)
    var textYs = ['1.00', '0.00', '-1.00'];

    for (var i = 0, len = textYs.length; i < len; i++) {
        var text = textYs[i];
        var gy   = ((1 - parseFloat(text)) / 2) * height;

        // Draw grid (Y)
        canvasContext.fillRect(0, gy, width, 1);

        // Draw text (Y)
        canvasContext.fillText(text, 0, gy);
    }
};

デモ 11は, デモ 10にグリッドとテキストの描画を追加したデモです. 少し余白を設定していますが, 座標を算出する考え方はサンプルコードと同じです.

デモ 11

波形描画の総まとめとして, デモ 12・デモ 13を実行してみてください. これらのデモは, オーディオ全体の波形描画に追加して, リアルタイムでの波形描画も実行します.

デモ 13は, 波形描画をSVGで実行しています. Canvasによる描画と違って, 拡大しても画像がぼやけないことを確認してみてください.

デモ 12 (Canvas)

デモ 13 (SVG)

フィルタとスペクトル

フィルタとは?

フィルタという言葉は日常生活でも使われますし, コンピューターサイエンスにおいても, UNIX系OSでパイプとフィルタがあります. フィルタの概念としては, ある成分を遮断して, ある成分を通過させるということでしょう.

音信号処理でも同じで, フィルタの機能は, ある成分を減衰させて, ある成分を通過させることです. そして, そのフィルタの多くは周波数特性を変化させます. この変化は, ある周波数成分を減衰させたり, ある周波数成分を強調したりといったフィルタの機能によるものです.

例えば, A - D変換ではローパスフィルタを利用します. なぜなら, サンプリング定理にしたがい, 高い周波数成分を取り除く必要があるからです. ローパスフィルタは高域の周波数成分を減衰させて, 低域の (Low) 周波数成分を通過 (Pass) させます.

Web Audio APIにおいて, フィルタの機能を定義しているのは, BiquadFilterNodeクラスです. ちなみに, Biquadとは, 「4次の, 双2次の」という意味です. これは, フィルタの数式の次数 (分母・分子がともに2次式となるので双2次) を表していますが, フィルタの理論をきちんと数式で理解するのはちょっと大変です (ちなみに, フィルタの数式はこちらを参照) .

したがって, このセクションでは, BiquadFilterNodeクラスの仕様の概要と利用方法, そして, フィルタの種類について解説します.

BiquadFilterNode

BiquadFilterNodeクラスもインスタンスを生成する必要があります. そのために, AudioContextインスタンスのcreateBiquadFilterメソッドを利用します. BiquadFilterNodeクラスもAudioNodeクラスを (プロトタイプ) 継承しているので, connect / disconnectメソッドを呼び出すことが可能です.

サンプルコード 35


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of BiquadFilterNode
var filter = context.createBiquadFilter();

// Set parameters
filter.type            = (typeof filter.type === 'string') ? 'lowpass' : 0;
filter.frequency.value = 350;
filter.detune.value    = 0;
filter.Q.value         = 1;
filter.gain.value      = 0;

// Create the instance of OscillatorNode
var oscillator = context.createOscillator();

// for legacy browsers
oscillator.start = oscillator.start || oscillator.noteOn;
oscillator.stop  = oscillator.stop  || oscillator.noteOff;

// OscillatorNode (Input) -> BiquadFilterNode (Filter) -> AudioDestinationNode (Output)
oscillator.connect(filter);
filter.connect(context.destination);

ノード接続

図3 - 2 - t. ノード接続

BiquadFilterNodeインスタンスは以下のように定義されています.

表3 - 2 - c. BiquadFilterNodeインスタンスのプロパティ
PropertyDescription
typeフィルタの種類
frequency遮断周波数 (カットオフ周波数), あるいは, 中心周波数の値で単位はHz. サンプリング定理により, 最大値はサンプリング周波数の1 / 2
defunerequencyプロパティと影響するパラメータは同じであるが, 単位はcent
Q遮断周波数の増幅率 (ゲイン) , または, 帯域の幅を決定する値. クオリティファクタと呼ばれるフィルタの特性の1つを制御するプロパティ
gain周波数帯域を強調するレベルで単位はdB
getFrequencyResponse(frequencyHz, magResponse, phaseResponse)フィルタの特性を取得するメソッド

typeプロパティのとりうる値は, フィルタの種類を表す8つの文字列型ですが, 初期の仕様では, フィルタの種類に対応する数値型でした. ブラウザによっては, どちらかの型に未対応の可能性もあるので, サンプルコード 35のようにフォールバックを記述しておくと安全です.

表3 - 2 - d. BiquadFilterNodeインスタンスのtypeプロパティの値
Filter TypeStringNumber
ローパスフィルタlowpass0
ハイパスフィルタhighpass1
バンドパスフィルタbandpass2
ローシェルビングフィルタlowshelf3
ハイシェルビングフィルタhighshelf4
ピーキングフィルタpeaking5
ノッチフィルタnotch6
オールパスフィルタallpass7

また, typeプロパティ以外のプロパティはすべてAudioParamインスタンスです. そして, これらのプロパティはフィルタの種類, すなわち, typeプロパティによって, フィルタに与える影響が異なったり, あるいは, パラメータの設定が無効であったりします.

デモ 14

フィルタの種類

このセクションでは, フィルタの種類の概要と影響するパラメータ (プロパティ) について解説します.

また, BiquadFilterNodeインスタンスでdetuneプロパティを利用することはあまりないと思いますので, 解説の用語からは除外します. もっとも, detuneプロパティがフィルタに対して影響することはfrequencyプロパティと同等なので支障はないかと思います.

ここから本題ですが, フィルタの種類, すなわち, BiquadFilterNodeインスタンスのtypeプロパティがとりうる値 (表3 - 2 - d) をフィルタの機能でグループ化すると, 表3 - 2 - dのようになるでしょう.

表3 - 2 - e. フィルタの機能によるグループ化
Filter TypeGroup
ローパスフィルタ特定の周波数帯域を通過させる / 減衰させる
ハイパスフィルタ
バンドパスフィルタ
ノッチフィルタ
ローシェルビングフィルタ特定の周波数帯域を強調する
ハイシェルビングフィルタ
ピーキングフィルタ
オールパスフィルタ位相を変化させる

特定の周波数帯域を通過させる / 減衰させる

ローパスフィルタとハイパスフィルタ, バンドパスフィルタとノッチフィルタに共通する機能は特定の周波数帯域を通過させる / 減衰させることです.

これらのフィルタ, つまり, BiquadFilterNodeインスタンスのtypeプロパティが, 'lowpass' (0), 'highpass' (1), 'bandpass' (2), 'notch' (6) のいずれかの場合, gainプロパティはフィルタの特性に影響しません.

ローパスフィルタ (低域通過フィルタ : Low-Pass Filter)
  • 低域の (Low) 周波数成分を通過 (Pass) させる
  • frequencyプロパティは遮断周波数 (カットオフ周波数) となる
  • Qプロパティは遮断周波数における増幅率 (ゲイン) となる
ハイパスフィルタ (高域通過フィルタ : High-Pass Filter)
  • 高域の (High) 周波数成分を通過 (Pass) させる
  • frequencyプロパティは遮断周波数 (カットオフ周波数) となる
  • Qプロパティは遮断周波数における増幅率 (ゲイン) となる
バンドパスフィルタ (帯域通過フィルタ : Band-Pass Filter)
  • 特定の帯域の (Band) 周波数成分を通過 (Pass) させる
  • frequencyプロパティは帯域の中心周波数となる
  • Qプロパティは帯域幅を決定する
ノッチフィルタ (帯域阻止フィルタ : Band-Eliminate Filter)
  • 特定の帯域の (Band) 周波数成分を減衰 (Eliminate) させる
  • frequencyプロパティは帯域の中心周波数となる
  • Qプロパティは帯域幅を決定する
ローパスフィルタ・ハイパスフィルタ・バンドパスフィルタ・ノッチフィルタ

図3 - 2 - u. フィルタの周波数特性

理論上のフィルタは, 紫のラインのように通過域と阻止域がくっきりと分離されます. しかし, そのようなフィルタ (理想フィルタ) を実装することは不可能で, 実際には, 赤のラインのように遷移帯域幅をもちます.

ローパスフィルタとハイパスフィルタ, バンドパスフィルタとノッチフィルタは, それぞれ対になるフィルタです. また, バンドパスフィルタとノッチフィルタにおいては, Qプロパティが小さいほど帯域が広く, 大きいほど帯域が狭くなります (図3 - 2 - uを参照).

特定の周波数帯域を強調する

ローシェルビングフィルタとハイシェルビングフィルタ, ピーキングフィルタに共通する機能は特定の周波数帯域を強調することです.

BiquadFilterNodeインスタンスのgainプロパティは, これらのフィルタの場合のみ有効となります. つまり, BiquadFilterNodeインスタンスのtypeプロパティが, 'lowshelf' (3), 'highshelf' (4), 'peaking' (5)のいずれかの場合においてのみ, gainプロパティは増幅率 (ゲイン) としてフィルタの特性に影響します.

また, ローシェルビングフィルタとハイシェルビングフィルタにおけるQプロパティはフィルタの特性に影響しません. ピーキングフィルタにおいては, Qプロパティが小さいほど帯域が広く, 大きいほど帯域が狭くなります.

ローシェルビングフィルタ (Low-Shelving Filter)
  • 低域の周波数成分を強調する
  • frequencyプロパティ以下の帯域を強調する
  • gainプロパティは指定した帯域の増幅率 (ゲイン) となる
ハイシェルビングフィルタ (High-Shelving Filter)
  • 高域の周波数成分を強調する
  • frequencyプロパティ以上の帯域を強調する
  • gainプロパティは指定した帯域の増幅率 (ゲイン) となる
ピーキングフィルタ (Peaking Filter)
  • 特定の帯域の周波数成分を強調する
  • frequencyプロパティは帯域の中心周波数となる
  • Qプロパティは帯域幅を決定する
ローシェルビングフィルタ ハイシェルビングフィルタ・ピーキングフィルタ

図3 - 2 - v. フィルタの周波数特性

これらのフィルタでは, frequencyとgainプロパティが重要なパラメータです. また, Qプロパティはピーキングフィルタの帯域幅を決定することにも着目してください.

位相を変化させる

オールパスフィルタは特殊で, 位相を変化させる機能をもっています.

位相変化

図3 - 2 - w. 位相変化

x軸 (横軸) の物理量は時間ではなく, 位相 (位置) であることに注意してください.

また, オールパスフィルタ (BiquadFilterNodeインスタンスのtypeプロパティが, 'allpass' (7)) の場合, gainプロパティはフィルタの特性に影響しません.

オールパスフィルタ (All-Pass Filter)
  • すべての (All) 周波数成分を通過 (Pass) させる
  • frequencyプロパティは位相を変化させる周波数帯域の中心周波数となる
  • Qプロパティは位相変化の急峻さを決定する. Qプロパティが大きいほど急峻な位相変化となる

オールパスフィルタの詳細は, フェイザーのページで詳しく解説しています .

フィルタ (BiquadFilterNode) のまとめとして, デモ 15を試してみてください. BiquadFilterNodeインスタンスのtypeプロパティやfrequency / Q / gainプロパティを変化させたときに, スペクトルがどんな感じに変化するのか?を感覚で理解してみてください.

デモ 15

カスタム波形

OscillatorNodeインスタンスのtypeプロパティがとりうる値の1つとして文字列型 'custom' がありました.

表3 - 2 - f. OscillatorNodeインスタンスのtypeプロパティの値
Wave TypeStringNumber
正弦波sine0
矩形波square1
ノコギリ波sawtooth2
三角波triangle3
カスタム波形custom4

'custom' (カスタム波形) のみは特殊な仕様で, 直接値を設定するのではなく, OscillatorNodeインスタンスの setPeriodicWaveメソッドによって, オートマティックに 'custom' に設定されます. カスタム波形を生成するには, AudioContextインスタンスのcreatePeriodicWaveメソッドで波形テーブルを作成する必要があります.

波形テーブルの作成の理解のためには, スペクトルや倍音など音信号処理の知識が必要になるので, ここまであとまわしにしてきました.

createPeriodicWaveメソッド

まずは, 形式的な解説から先にします.

createPeriodicWaveメソッドには2つの引数を指定する必要があり, いずれの引数も サイズが0 ~ 4096以下のFloat32Array型の配列です. また, 2つの配列のサイズは同じである必要があります.

初期の仕様では, createPeriodicWaveメソッドは定義されておらず, その代わりに, createWaveTableメソッドを利用していました. したがって, サンプルコード 36のようにフォールバックを記述しておくと安全です.

サンプルコード 36


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// for legacy browsers
context.createPeriodicWave = context.createPeriodicWave || context.createWaveTable;

// for custom
// 1 (index 0) + 1 (Fundamental frequency) + 15 (harmonics)
var TABLE_SIZE = 17;

var reals = new Float32Array(TABLE_SIZE);
var imags = new Float32Array(TABLE_SIZE);

// Initialization
for (var i = 0; i < TABLE_SIZE; i++) {
    reals[i] = 0;
    imags[i] = 0;
}

Float32Arrayのインスタンスを生成したときに要素は0で初期化されるので, 初期化のループは任意です.

ここからは, 少し音信号処理の知識が必要になってきます. とりあえずカスタム波形が生成できればOKという場合はスルーしてください.

カスタム波形とフーリエ変換

createPeriodicWaveメソッドによる波形テーブルの作成は, 振幅スペクトルに関連する値を指定して作成します. 具体的には, 基本周波数と倍音の振幅比を指定するということです.

配列のインデックス1が基本周波数, インデックス2以降がそれぞれ, 2倍音, 3倍音…となります. インデックス0は0で固定です (ちなみに, imag[0]は必ず0にする必要があります) .

すなわち, 配列のサイズは, 1 (インデックス0) + 1 (基本周波数) + n (倍音の数) となります. サンプルコード 36では, 配列のサイズは17なので, 15倍音まで指定可能になります.

ところで, なぜ2つの配列が必要なのでしょうか?その理由は, フーリエ変換の演算結果にあります. 波形テーブルの作成は, 振幅スペクトルの視点で実行します. スペクトルを計算するには, フーリエ変換を利用します. そして, フーリエ変換の演算結果は, 複素数になります. 複素数は, 実部と虚部で構成されます. つまり, 引数に指定する2つの配列はフーリエ変換の演算結果である複素数の実部と虚部に対応しています (createPeriodicWaveメソッドの第1引数に指定する配列が実部, 第2引数に指定する配列が虚部に対応しています) .

それでは, 実際に値を設定します.

サンプルコード 37


/*
 * Add code to sample code 01
 */

// ....

// No use
reals[0] = 0;  // fixed
imags[0] = 0;  // fixed

// Fundamental frequency
reals[1] = 1;
imags[1] = 1;

// The 2nd harmonic
reals[2] = 0.5;
imags[2] = 0.5;

// The 3rd harmonic
reals[3] = 0.25;
imags[3] = 0.25;

// Create the instance of PeriodicWave
var periodicwave = context.createPeriodicWave(reals, imags);

サンプルコードでは, 基本周波数と3倍音まで設定しています. 振幅比を指定すればいいので, 基本周波数に1, 2倍音に0.5, 3倍音に0.25…という具合でOKです. また, インデックス0は0で固定します.

そして, 2つの配列を設定すれば, createPeriodicWaveメソッドの引数に指定して, PeriodicWaveインスタンスを生成します. この処理は配列の値を変更するたびに実行する必要があります.

setPeriodicWaveメソッド

PeriodicWaveインスタンスを生成すれば, あとはそれをsetPeriodicWaveメソッドの引数に指定するだけです. この処理もPeriodicWaveインスタンスを生成するたびに実行する必要があります.

また, 初期の仕様では, setPeriodicWaveメソッドは定義されておらず, その代わりに, setWaveTableメソッドを利用していました. したがって, サンプルコード 39のようにフォールバックを記述しておくと安全です.

サンプルコード 39


/*
 * Add code to sample code 01
 */

// ....

// Create the instance of OscillatorNode
var oscillator = context.createOscillator();

// for legacy browsers
oscillator.setPeriodicWave = oscillator.setPeriodicWave || oscillator.setWaveTable;

oscillator.setPeriodicWave(periodicwave);

console.log(oscillator.type);  // 'custom'

以上で, カスタム波形の生成が実装できました. カスタム波形によるサウンドを体感するために, デモ 16を試してみてください. デモ 16では, 15倍音まで設定可能になっています. 音がどう変化するか?とともに, 波形の変化もぜひ観察してみてください.

デモ 16

波形描画 まとめ

このセクションでは, 波形描画だけでなく, 周波数特性に関係するフィルタや, 倍音構造を設定して波形を生成するカスタム波形まで解説したので, コンテンツ量が多く大変だったと思います. フィルタとカスタム波形に関しては, とりあえず実装することができて, パラメータを変化させた場合に, 音がどんな感じに変化するのか?スペクトルがどう変化するのか?を感覚的につかめれば十分でしょう.

したがって, 波形描画に関するポイントをまとめておきます.

表3 - 2 - g. 波形描画のインターフェース
NodeDomainMethodType
AnalyserNode
(リアルタイム)
時間getByteTimeDomainDataUint8Array
周波数getByteFrequencyDataUint8Array
getFloatFrequencyDataFloat32Array
AudioBuffer
(オーディオデータ全体)
時間getChannelDataFloat32Array