Web Audio API スタートガイド

HTML5 <audio> 要素の前に、ウェブの沈黙を解消するには Flash などのプラグインが必要でした。ウェブ上の音声にプラグインは不要になりましたが、音声タグを使用すると、高度なゲームやインタラクティブ アプリケーションの実装に大きな制限があります。

Web Audio API は、ウェブ アプリケーションで音声の処理と合成を行うための高レベルの JavaScript API です。この API の目的は、最新のゲーム オーディオ エンジンに搭載されている機能と、最新のデスクトップ オーディオ制作アプリケーションに見られるミキシング、処理、フィルタリングのタスクの一部を含めることです。ここではこの強力な API の使い方を 簡単に紹介します

AudioContext を使ってみる

AudioContext は、すべてのサウンドを管理して再生するためのものです。Web Audio API を使用してサウンドを生成するには、1 つ以上のサウンド ソースを作成し、AudioContext インスタンスによって提供されるサウンド デスティネーションに接続します。この接続は直接である必要はなく、オーディオ信号の処理モジュールとして機能する任意の数の中間 AudioNodes を経由できます。このルーティングについて詳しくは、ウェブオーディオの仕様をご覧ください。

AudioContext の 1 つのインスタンスで、複数の音声入力と複雑なオーディオ グラフをサポートできるため、作成するオーディオ アプリケーションごとに必要なのは 1 つだけです。

次のスニペットは AudioContext を作成します。

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

古い WebKit ベースのブラウザの場合は、webkitAudioContext と同様に、webkit 接頭辞を使用します。

AudioNode の作成や音声ファイルデータのデコードなど、Web Audio API の興味深い機能の多くは AudioContext のメソッドです。

サウンドを読み込んでいます

Web Audio API は、短~中程度の長さのサウンドに AudioBuffer を使用します。基本的なアプローチは、XMLHttpRequest を使用して音声ファイルを取得することです。

この API では、WAV、MP3、AAC、OGG などの複数の形式で音声ファイルのデータを読み込めます。さまざまな音声形式に対するブラウザ サポートは異なります

次のスニペットは、音声サンプルを読み込む方法を示しています。

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

音声ファイルのデータは(テキストではなく)バイナリであるため、リクエストの responseType'arraybuffer' に設定します。ArrayBuffers の詳細については、XHR2 に関する記事をご覧ください。

デコードされていない音声ファイルのデータを受け取ったら、後でデコードするために保持することも、AudioContext decodeAudioData() メソッドを使用してすぐにデコードすることもできます。このメソッドは、request.response に保存されている音声ファイルデータの ArrayBuffer を取得し、非同期でデコードします(メインの JavaScript 実行スレッドをブロックしません)。

decodeAudioData() が終了すると、デコードされた PCM オーディオ データを AudioBuffer として提供するコールバック関数が呼び出されます。

音を鳴らしています

シンプルなオーディオ グラフ
シンプルなオーディオ グラフ

1 つ以上の AudioBuffers が読み込まれたら、サウンドを再生できるようになります。犬の吠える声を含む AudioBuffer を読み込み、読み込みが完了したとします。次のコードでこのバッファを再生できます。

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

この playSound() 関数は、誰かがキーを押すか、マウスで何かをクリックするたびに呼び出すことができます。

noteOn(time) 関数を使用すると、ゲームやその他の時間の制約が厳しいアプリの正確な音声再生のスケジュールを簡単に設定できます。ただし、このスケジューリングを正しく動作させるには、サウンド バッファを事前に読み込む必要があります。

Web Audio API の抽象化

もちろん、この特定のサウンドを読み込むようにハードコードされていない、より一般的な読み込みシステムを作成することをおすすめします。オーディオ アプリまたはゲームで使用される短~中程度のサウンドを処理する方法は多数ありますが、ここでは BufferLoader を使用する 1 つの方法を紹介します(ウェブ標準の一部ではありません)。

BufferLoader クラスの使用例を次に示します。AudioBuffers を 2 つ作成し、読み込まれたらすぐに同時に再生してみましょう。

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

時間との闘い: リズムに合わせて音を再生する

Web Audio API を使用すると、デベロッパーは再生のスケジュールを正確に設定できます。これを確認するために、簡単なリズムトラックを設定してみましょう。最も広く知られているドラムキットのパターンは次のとおりです。

シンプルなロックドラムのパターン
シンプルなロックドラムのパターン

ハイハットを 8 分音符に キックとスネアを四半期ごとに交互に

kicksnarehihat のバッファを読み込んでいると仮定すると、これを行うコードは簡単です。

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

ここでは、楽譜の無制限ループの代わりに、1 回だけ繰り返します。関数 playSound は、次のように指定された時間にバッファを再生するメソッドです。

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

音の音量を変更する

サウンドに対して行う最も基本的な操作の 1 つは、音量の変更です。Web Audio API を使用すると、音量を操作するために AudioGainNode を介して音源を宛先にルーティングできます。

ゲインノードを含むオーディオ グラフ
ゲインノードを含むオーディオ グラフ

この接続は、次の手順で設定できます。

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

グラフを設定したら、次のように gainNode.gain.value を操作して、音量をプログラムで変更できます。

// Reduce the volume.
gainNode.gain.value = 0.5;

2 つのサウンド間のクロスフェード

ここで、少し複雑なシナリオを考えてみましょう。複数のサウンドを再生しながら、それらのサウンド間でクロスフェードを行いたいとします。これは、2 つのターンテーブルがあり、ある音源から別の音源にパンできるようにしたい、DJ のようなアプリケーションではよくあるケースです。

これは、次のオーディオ グラフで行うことができます。

2 つのソースがゲインノードで接続されたオーディオ グラフ
2 つのソースがゲインノードで接続されたオーディオ グラフ

これをセットアップするには、次の関数を使用して、2 つの AudioGainNodes を作成し、ノードを介して各ソースを接続するだけです。

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

イコール パワー クロスフェーディング

単純な線形クロスフェード アプローチでは、サンプル間を移動すると音量が下がります。

線形クロスフェード
線形クロスフェード

この問題に対処するために、等しい電力曲線を使用します。この曲線では、対応するゲイン曲線が非線形で、より高い振幅で交差しています。これにより、オーディオ リージョン間の音量の落ち込みが最小限に抑えられ、レベルがわずかに異なるリージョン間でのクロスフェードがより均一になります。

イコール パワー クロスフェード。
イコール パワー クロスフェード

再生リストのクロスフェード

もう 1 つの一般的なクロスフェーダー アプリケーションは、音楽プレーヤー アプリ用です。 曲が変更されたときに、現在のトラックをフェードアウトし、新しいトラックをフェードインして、不快なトランジションを回避します。そのためには、未来へのクロスフェードをスケジュールします。setTimeout を使用してこのスケジューリングを行うこともできますが、これは正確ではありません。Web Audio API では、AudioParam インターフェースを使用して、AudioGainNode のゲイン値などのパラメータの将来の値をスケジュール設定できます。

したがって、プレイリストが指定されていて、現在のトラックの再生が終わる少し前に、現在再生中のトラックのゲインを減少させ、次のトラックのゲインの増加をスケジュールすることで、トラック間を遷移できます。

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

Web Audio API には便利な RampToValue メソッドのセットが用意されており、linearRampToValueAtTimeexponentialRampToValueAtTime など、パラメータの値を段階的に変更できます。

遷移時間の関数は、組み込みの線形関数と指数関数(上記のように)から選択できますが、setValueCurveAtTime 関数を使用して値の配列で独自の値曲線を指定することもできます。

音声に単純なフィルタ効果を適用する

BiquadFilterNode を含む音声グラフ
BiquadFilterNode を含む音声グラフ

Web Audio API を使用すると、あるオーディオ ノードから別のオーディオ ノードにサウンドをパイプで渡すことで、場合によっては複雑なプロセッサ チェーンを作成し、サウンドフォームに複雑なエフェクトを追加できます。

そのための 1 つの方法は、音源と音源の間に BiquadFilterNode を配置することです。このタイプのオーディオ ノードでは、グラフィック イコライザーやさらに複雑なエフェクトの作成に使用できるさまざまな低次フィルタを実行できます。主に、サウンドの周波数スペクトルのどの部分を強調し、どの部分を緩和するかを選択します。

サポートされているフィルタの種類は次のとおりです。

  • ローパス フィルタ
  • ハイパス フィルタ
  • バンドパス フィルタ
  • ローシェルフ フィルタ
  • ハイシェルフ フィルタ
  • ピーキング フィルタ
  • ノッチ フィルタ
  • オールパス フィルタ

すべてのフィルタには、ゲイン、フィルタを適用する周波数、品質係数を指定するパラメータが含まれています。ローパス フィルタは、低い周波数範囲を維持しますが、高い周波数を破棄します。ブレークポイントは周波数値によって決定され、Q 係数は単位なしであり、グラフの形状を決定します。ゲインは、ローシェルフ フィルタやピーキング フィルタなど、特定のフィルタにのみ影響し、このローパス フィルタには影響しません。

サウンドサンプルからベースのみを抽出するシンプルなローパス フィルタをセットアップしましょう。

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

一般に、人間の聴覚も同じ原理で動作するため、対数スケールで動作するように周波数コントロールを調整する必要があります(つまり、A4 は 440 Hz、A5 は 880 Hz です)。詳しくは、上記のソースコードのリンクの FilterSample.changeFrequency 関数をご覧ください。

最後に、このサンプルコードを使用すると、フィルタを接続または接続解除して、AudioContext グラフを動的に変更できます。node.disconnect(outputNumber) を呼び出すと、AudioNode をグラフから切断できます。たとえば、グラフのルートをフィルタを経由せずに直接接続に変更するには、次のようにします。

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

さらに詳しく

ここまで、音声サンプルの読み込みや再生など、API の基本について説明しました。ゲインノードとフィルタを使用したオーディオ グラフを作成し、一般的なサウンド効果を有効にするためにサウンドとオーディオ パラメータの調整をスケジュール設定しました。これで、便利なウェブ オーディオ アプリケーションを作成する準備が整いました。

すでに多くのデベロッパーが Web Audio API を使用して優れた作品を作成しています。アイデアのヒントを得られるかもしれません。私のお気に入りは

  • AudioJedit: ブラウザ内のサウンド スプライシング ツールで、SoundCloud のパーマリンクを使用します。
  • ToneCraft: 3D ブロックを積み重ねて音声を作成するサウンド シーケンサー。
  • Plink: Web Audio と Web Sockets を使用した共同音楽制作ゲームです。