オーティオデータの再生

トップページでも述べたことですが, 単純にオーディオデータを再生するだけであればHTMLMediaElementを利用したほうが良いでしょう. その理由は, 実装が簡単で,機能面もオーディオプレイヤーとしての基本的な機能はサポートされています. さらに, クロスブラウザ対応においても, Web Audio APIよりはるかに楽でしょう.

Web Audio APIによるオーディオデータの再生について解説する目的とは, 単純にオーディオデータを再生するだけでなく, サウンドエフェクトを付加したり, 波形描画したりといったより高度なサウンド処理を実現することにあります. そのステップアップとして, まずはオーディオデータを再生することのみに主眼を置きます.

Web Audio APIによるオーディオデータの再生方法は2つあります.

  • AudioBufferSourceNodeクラスを利用する
  • MediaElementAudioSourceNodeクラスを利用する

つまり, オーディオリソースの取得方法が2つあり, それぞれに対応したクラスが定義されているということです. 2つの方法を詳しく解説していきます.

AudioBufferSourceNode

こちらの方法を利用する場合の, アルゴリズムの概要を記載します.

  1. 1. ArrayBuffer (バイナリデータ) を取得 (File API or Ajax)
  2. 2. ArrayBuffer (バイナリデータ) を参照 (decodeAudioDataメソッド)
  3. 3. 入力点を生成 (createBufferSourceメソッド)
  4. 4. 入力点と出力点を接続 (connectメソッド)
  5. 5. 音源のスイッチをON (startメソッド)

以下のセクションでこれらの詳細を解説していきます.

1. ArrayBufferを取得

まず, オーディオデータをArrayBufferとして取得します. ArrayBufferとは, 音声や動画, 画像などのバイナリデータの配列です. Arrayインスタンスの配列との違いは, バイナリデータを直接扱うためアクセスがより高速になります. しかしながら, Arrayインスタンスではないので, Arrayインスタンスのメソッド (pop, sort, mapなど) は利用できません (ArrayBufferの詳細はこちらを参照) .

ArrayBufferの取得処理は, File APIやAjax (XMLHttpRequest Level 2) に関することです. ローカルのデータであればFile APIを利用し, Web上のデータであればAjaxを利用して取得します. デモ 04で試してみてください.

ArrayBufferの取得

図1 - 2 - a. ArrayBufferの取得

デモ 04

Ajaxの場合, jQuery (の$.ajax, $.get関数など) は利用できません. なぜなら, XMLHttpRequestインスタンスのresponseTypeプロパティに 'arraybuffer' を指定する必要があり, (あくまで) 現状は, $.ajaxや$.get関数は, 'arraybuffer' に対応していない (XMLHttpRequest Level 2のAPIに対応していない) からです. ちなみに, XMLHttpRequest Level 2の詳細はこちらを参照してください.

2. ArrayBufferの参照

オーディオデータのArrayBufferに限らず, ArrayBufferのバイナリデータの配列に対して, 直接アクセス (参照) することはできません. バイナリデータにアクセスするためには, ArrayBufferViewオブジェクトを利用して, 型付きの配列としてアクセスします (ArrayBufferViewの詳細はこちらを参照) .

ここからは, Web Audio API限定のことなりますが, 具体的には, ArrayBufferをFloat32Array型の配列としてアクセスします. すなわち, バイナリデータの配列であるArrayBufferに対して, 32ビット浮動小数点数型としてアクセスします.

ArrayBufferView (Float32Arrayの場合)

図1 - 2 - b. ArrayBufferView (Float32Arrayの場合)

もっとも, Web Audio APIにおいては, ArrayBufferへのアクセスをより抽象化したレベルで実行するために, AudioBufferクラスのインスタンスを生成します. AudioBufferインスタンスには, getChannelDataメソッドが定義されており, このメソッドがオーディオデータの実体であるArrayBufferをFloat32Array型の配列としてアクセスすることを可能にします.

AudioBuffer

図1 - 2 - c. AudioBuffer

AudioBufferインスタンスのgetChannelDataメソッドによって, オーディオデータの実体へのアクセスが抽象化されていることに着目してください.

表1 - 2 - a. AudioBufferインスタンスのプロパティ
PropertyDescription
sampleRateオーディオデータのサンプリング周波数
lengthオーディオデータの実体であるFloat32Array型の配列のサイズ
durationオーディオの再生時間 (秒単位)
numberOfChannelsオーディオのチャンネル数
getChannelDataオーディオデータの実体であるFloat32Array型の配列を取得する

そして, AudioBufferインスタンスを生成するには, AudioContextインスタンスのdecodeAudioDataメソッドを利用します.

サンプルコード 01


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';

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

        if (arrayBuffer instanceof ArrayBuffer) {
            // The 2nd argument for decodeAudioData
            var successCallback = function(audioBuffer) {
                /* audioBuffer is the instance of AudioBuffer */

                /* do something for playing the audio .... */
            };

            // The 3rd argument for decodeAudioData
            var errorCallback = function(error) {
                if (error instanceof Error) {
                    window.alert(error.message);
                } else {
                    window.alert('Error : "decodeAudioData" method.');
                }
            };

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

xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';  // XMLHttpRequest Level 2
xhr.send(null);

第1引数には, 取得したオーディオデータのArrayBufferを指定するだけです.

第2引数は重要なので少し詳しく解説します. まず, 前提としてコールバック関数を理解している必要があります. そんなの知ってるよという方はスルーしてください.

コールバック関数について

コールバック関数とは, ある関数の実行が終了したタイミングで呼び出される関数のことです. 少しくだいて表現すれば, (コールバック関数を渡された側の) 処理が終われば, (コールバック関数を) 実行するということです.

コールバックは, イベントドリブンプログラミングや非同期処理において頻繁に利用される技法です. したがって, クライアントサイドJavaScriptやNode.jsでは必須の技法となります.

decodeAudioDataメソッドの処理が成功すると, 第2引数に指定したコールバック関数が実行されます. そして, 第2引数に指定したコールバック関数の引数にはAudioBufferインスタンスが渡されるので, 次のステップ以降の処理を追加することにより, Web Audio APIでオーディオデータを再生することが可能になるというわけです.

第3引数は, 処理が失敗した場合のエラー処理をするコールバック関数を指定します. また, 次期仕様においては, このコールバック関数にはエラーオブジェクトが引数に渡されることになっています.

HTML5 ROCKSでは, AjaxでArrayBufferの取得から, AudioBufferインスタンスの生成までの処理を実行するライブラリが公開されています. このライブラリは, 複数の (ワンショット) オーディオを扱う場合に便利でしょう (HTML5 ROCKS >>ライブラリ解説 >>コード) .

createBufferメソッド

AudioContextインスタンスのcreateBufferメソッドを利用することによって, AudioBufferインスタンスを生成することも可能です. もっとも, このメソッドを実行するだけではオーディオデータの実体がないので, 生成したAudioBufferインスタンスのgetChannelDataメソッドでFloat32Array型配列への参照を取得して, オーディオデータを格納します.

createBufferメソッドには, チャンネル数・データサイズ・サンプリング周波数の3つの引数を指定します.

多くのケースにおいては, decodeAudioDataメソッドによってAudioBufferインスタンスを生成することがほとんどです. しかしながら, createBufferメソッドによってAudioBufferインスタンスを生成するケースもあります.

  • サウンドフォントのオーディオデータを利用する (sf2synth.jsなど)
  • 人工インパルス応答など数式から生成することが可能なオーディオデータを利用する

サンプルコード 02は実用性はまったくありませんが, createBufferメソッドのコード例です.

サンプルコード 02


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

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

var channel = 2;
var length  = 5 * context.sampleRate;  // 5 sec

var dataLs = new Float32Array(length);
var dataRs = new Float32Array(length);

for (var i = 0; i < length; i++) {
    dataLs[i] = Math.sin(2 * Math.PI * i * 440 / context.sampleRate);
    dataRs[i] = 2 * (Math.random() - 0.5);
}

// Create the instance of AudioBuffer (Synchronously)
var audioBuffer = context.createBuffer(channel, length, context.sampleRate);

// Set data
var float32ArrayLs = audioBuffer.getChannelData(0);
var float32ArrayRs = audioBuffer.getChannelData(1);

float32ArrayLs.set(dataLs);
float32ArrayRs.set(dataRs);

ちなみに, decodeAudioDataメソッドは非同期で実行されるのに対して, createBufferメソッドは同期的に実行されます. したがって, 配列サイズが大きい場合には, UIを長い時間ブロックしてしまうことには考慮が必要です.

3. 入力点を生成

生成したAudioBufferインスタンス自体は, サウンドの出力点であるAudioDestinationNodeに接続できません. なぜなら, connectメソッドを呼び出せないからです. そこで, AudioNodeクラスを (プロトタイプ) 継承し, かつAudioBufferインスタンスを格納するコンテナを利用します. それが, AudioBufferSourceNodeクラスです.

AudioBufferSourceNodeクラスはAudioNodeクラスを (プロトタイプ) 継承しているので, サウンドの出力点であるAudioDestinationNodeに接続することができます. さらに, 生成したAudioBufferインスタンスを格納するためのbufferプロパティが定義されています. このクラスを利用することにより, 入力点となるオーディオデータを出力点に接続できます. AudioBufferSourceNodeインスタンスを生成するには, AudioContextインスタンスのcreateBufferSourceメソッドを利用します.

サンプルコード 03


/*
 * Add code to sample code 01
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // Create the instance of AudioBufferSourceNode
    var source = context.createBufferSource();

    // Set the instance of AudioBuffer
    source.buffer = audioBuffer;

    // Set parameters
    source.loop               = false;
    source.loopStart          = 0;
    source.loopEnd            = audioBuffer.duration;
    source.playbackRate.value = 1.0;

    // ....

createBufferSourceメソッドでAudioBufferSourceNodeインスタンスを生成したあと, そのbufferプロパティにAudioBufferインスタンスを設定します.

表1 - 2 - b. AudioBufferSourceNodeインスタンスのプロパティ
PropertyDescription
bufferオーディオデータの実体となるAudioBufferインスタンス
playbackRateオーディオの再生速度とピッチを変更
loopループ再生するかどうか
loopStartループ再生する場合の, オーディオ開始位置 (秒単位)
loopEndループ再生する場合の, オーディオ終了位置 (秒単位)
startオーディオを再生する
stopオーディオを停止する
onendedstopメソッドを実行, または, 再生時間が経過したときに発生するイベントハンドラ

ちなみに, HTMLMediaElementのplaybackRateプロパティと違い, 再生速度を変更するとピッチ (音の高さ) も同時に変更されます. この理由は, 物理的に再生速度とピッチは互いに関連しているからです. ただし, 技術的にはピッチを変更せずに, 再生速度を変更することも可能で, そのアルゴリズムはタイムストレッチと呼ばれます.

4. 入力点と出力点を接続

ここまでの処理が完了すれば, あとは, AudioBufferSourceNodeインスタンスをAudioDestinationNodeに接続してから, 音源のスイッチをONにするだけです. そして, AudioBufferSourceNodeクラスもAudioNodeクラスを (プロトタイプ) 継承しているので, connectメソッドを呼び出すことが可能です.

ノード接続

図1 - 2 - d. ノード接続

サンプルコード 04


/*
 * Add code to sample code 01
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // Create the instance of AudioBufferSourceNode
    var source = context.createBufferSource();

    // Set the instance of AudioBuffer
    source.buffer = audioBuffer;

    // Set parameters
    source.loop               = false;
    source.loopStart          = 0;
    source.loopEnd            = audioBuffer.duration;
    source.playbackRate.value = 1.0;

    // AudioBufferSourceNode (Input) -> AudioDestinationNode (Output)
    source.connect(context.destination);

// ....

5. 音源のスイッチをON

AudioBufferSourceNodeインスタンスにもstart / stopメソッドが定義されています. また, これらのメソッドも初期の仕様では定義されておらず, それら代わりにnoteOn / noteOffメソッドを利用していたので, フォールバックを記述しておくほうが安全です.

サンプルコード 05


/*
 * Add code to sample code 01
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // Create the instance of AudioBufferSourceNode
    var source = context.createBufferSource();

    // Set the instance of AudioBuffer
    source.buffer = audioBuffer;

    // Set parameters
    source.loop               = false;
    source.loopStart          = 0;
    source.loopEnd            = audioBuffer.duration;
    source.playbackRate.value = 1.0;

    // AudioBufferSourceNode (Input) -> AudioDestinationNode (Output)
    source.connect(context.destination);

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

    // Start audio
    source.start(0);

    source.onended = function(event) {
        // Remove event handler
        source.onended     = null;
        document.onkeydown = null;

        // Stop audio
        source.stop(0);

        console.log('"on' + event.type + '" event handler !!');

        // Audio is not started !!
        // It is necessary to create the instance of AudioBufferSourceNode again

        // source.start(0);
    };

    // Trigger 'ended' event
    var trigger = function() {
        var event = document.createEvent('Event');
        event.initEvent('ended', true, true);

        if (source instanceof AudioBufferSourceNode) {
            source.dispatchEvent(event);
        }
    };

    // Stop audio
    document.onkeydown = function(event) {
        // Space ?
        if (event.keyCode !== 32) {
            return;
        }

        // Execute onended event handler
        trigger();

        return false;
    };

// ....

start / stopメソッドの引数には0を指定し, 即時にオーディオを開始 / 停止するようにしています (スペースキーを押した場合, endedイベントを発生させてオーディオを停止する).

また, stopメソッドを実行, もしくは, 再生時間が経過したときに発生するonendedイベントハンドラで終了処理を実行します. ちなみに, onendedイベントハンドラの第1引数には, 通常のイベントオブジェクトが渡されます (イベントオブジェクトの詳細はこちらを参照) .

これで, Web Audio APによるオーディオデータの再生が可能になります. このセクションのまとめとして, デモ 05を試してみてください. デモ 05では, AudioBuffreSourceNodeインスタンスのplaybackRateプロパティとloopプロパティも設定できるようにしています. オーディオファイルの形式は, .wav / .ogg / .mp3が少なくとも再生可能です.

playbackRateプロパティはAudioParamインスタンスなので, そのvalueプロパティに値を設定します.

デモ 05

デモ 05を試して気づいたかもしれませんが, AudioBufferSourceNodeインスタンスのstopメソッドを1度実行したあと, 再度startメソッドを実行しても再生されません. その理由は, AudioBufferSourceNodeは楽器音などのワンショットサンプルの再生に利用することを想定されているので, 使い捨てのノードという仕様になっているからです. したがって, オーディオを再開するには, 新たにAudioBufferSourceNodeインスタンスを生成する必要があります.

MediaElementAudioSourceNode

Web Audio APIにおいて, オーディオデータを再生する方法は2つあると解説しました. 1つは既に解説した, AudioBufferSourceNodeクラスを利用して再生する方法です. このセクションでは, もう1つの方法である, MediaElementAudioSourceNodeクラスを利用してオーディオデータを再生する方法について解説します.

MediaElementAudioSourceNodeクラスは, HTMLMediaElementをWeb Audio APIで扱うためのクラスです. HTMLMediaElementとは, HTML5で新たに追加されたaudioタグやvideoタグのことです. そして, このクラスのサブクラスとして, HTMLAudioElementとHTMLVideoElementが定義されています (HTMLMediaElementの仕様はこちらを参照).

このセクションでは, audioタグ (HTMLAudioElement) で解説を進めますが, videoタグ (HTMLVideoElement) でも同様に適用できます.

こちらの方法を利用する場合の, アルゴリズムの概要を記載します.

  1. 1. audioノードオブジェクトを取得
  2. 2. 入力点を生成 (createMediaElementSourceメソッド)
  3. 3. 入力点と出力点を接続 (connectメソッド)
  4. 4. 音源のスイッチをON

実質新しく理解する必要があるのは, 入力点を生成することのみです.

1. audioノードオブジェクトを取得

audioノードオブジェクトの取得方法は大きく以下の2つの方法に分類可能です.

  • Audioコンストラクタを呼び出す
  • DOMとして取得する

ただし, videoノードオブジェクトの場合は, コンストラクタ呼び出しによる方法は利用できません.

サンプルコード 06


/*
 * Use Audio constructor
 */

var audio = new Audio('sample.wav');

サンプルコード 07


<!-- HTML -->
<audio src="sample.wav"></audio>


// JavaScript
var audio = document.querySelector('audio');

サンプルコード 08


/*
 * Create audio tag
 */

// JavaScript
var audio = document.createElement('audio');
audio.setAttribute('src', 'sample.wav');

2. 入力点を生成

入力点を生成するには, AudioContextインスタンスのcreateMediaElementSourceメソッドを呼び出します. メソッドの引数には, 最初の処理で取得したaudioノードオブジェクトを指定します. メソッドの戻り値として, MediaElementAudioSourceNodeインスタンスを取得することができます. これが, 入力点の役割を担うことになります.

重要なポイントとなるのが, createMediaElementSourceメソッドの実行のタイミングです. メソッドの実行は, オーディオデータのロードが開始されて以降, すなわち, HTMLMediaElementで定義されているイベントのonloadstartイベント発生以降において実行する必要があります. 具体的にそのタイミング (イベント) としては, onloadstart, onloadedmetadata, onloadeddata, ondurationchange, oncanplay, oncanplaythrough…etc

もっとも, 最初に発生するイベントはonloadstartなので, 重要な理由がなければ, onloadstartイベントハンドラとして, createMediaElementSourceメソッドを実行すればよいでしょう. 少し説明が長くなりましたが, コードにすると簡潔です.

サンプルコード 09


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

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

var audio = new Audio('sample.wav');

audio.addEventListener('loadstart', function() {
    // Create the instance of MediaElementAudioSourceNode
    var source = context.createMediaElementSource(audio);
}, false);

3. 入力点と出力点を接続

入力点を生成すれば, 実質新たに説明することはありません. 入力点と出力点を接続したあと, 音源のスイッチをONにするだけです. MediaElementAudioSourceNodeクラスも, AudioNodeクラスを (プロトタイプ) 継承しているので, プロトタイプチェーンをたどり, connectメソッドを呼び出すことが可能です.

ノード接続

図1 - 2 - e. ノード接続

サンプルコード 10


/*
 * Add code to sample code 9
 */

// ....

// MediaElementAudioSourceNode (Input) -> AudioDestinationNode (Output)
source.connect(context.destination);

4. 音源のスイッチをON

最後は音源のスイッチをONにするだけです. スイッチの役割を担うのは, HTMLMediaElementのplayメソッド (停止は, pauseメソッド) です. また, controls属性をtrueにしているのであれば, デフォルトのインターフェースでも可能です.

一度オーディオを停止しても, MediaElementAudioSourceNodeインスタンスを再生成する必要はありません. その理由は, AudioBufferSourceNodeと異なり, 楽曲データなどの再生に利用することを想定されているので, 使い捨てのノードとなっていないからです.

サンプルコード 11


/*
 * Add code to sample code 10
 */

// ....

// Start audio
audio.play();

// ....

// Stop audio
audio.pause();

実際に試してみるとわかりますが, Web Audio APIにおいて, サイズの大きなオーディオデータ (楽曲データなど) を扱う場合には, こちらの方法を利用したほうが良いでしょう (仕様にもそのように記載されています) .

まとめとしてデモ 09を実行してみてください. デモ 09では, File APIによって取得したobject URLをsrc属性に設定しているので, 好きな楽曲を楽しむことが可能です. また, Web Audio APIによってオーディオデータが再生されていることを確認するため, エフェクターのディレイとフィルタを設定しています. エフェクターに関しては, まだ解説していませんが, 処理の概要は理解できると思います.

ちなみに, 仕様変更により, MediaElementAudioSourceNodeで利用するオーディオデータにはクロスオリジン制限がかかるようになりました. したがって, Data URLではなく, Object URLを利用することでこの問題に対処しています.

デモ 07

サウンドスケジューリング

AudioBufferSourceクラスは, 楽器音などのワンショットサンプルのオーディオデータの再生に利用することを想定されていると解説しました. ワンショットサンプルを利用する場合において, おそらく必要となってくるのがサウンドスケジューリングです.

AudioContextインスタンスのcurrentTimeプロパティを利用して, サウンドスケジューリングに応じた時刻をstart / stopメソッドの引数に指定することでスケジューリングを実装可能です. currentTimeプロパティは, AudioContextインスタンスの生成時点を基準時として, そこからの経過時間を秒単位で格納しています.

ちなみに, startメソッドの第2引数には, オーディオデータの開始位置を指定することが可能です. 第3引数には, 第2引数で指定した開始位置から残りの再生時間を指定します. startメソッドにこれらの引数を指定する場合は, フォールバックにnoteOnメソッドではなく, noteGrainOnメソッドを利用します.

サンプルコード 12は, ワンショットサンプルのオーディオデータを扱う場合の骨組みとなるコードです.

サンプルコード 12


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

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

// for legacy browsers
context.createGain = context.createGain || context.createGainNode;

// Create the instance of GainNode
var gain = context.createGain();

var base = 'demos/audio-oneshot/';
var urls = [
    (base + 'C.mp3'),
    (base + 'D.mp3'),
    (base + 'E.mp3'),
    (base + 'F.mp3'),
    (base + 'G.mp3'),
    (base + 'A.mp3'),
    (base + 'B.mp3'),
    (base + 'C1.mp3')
];

// for the instances of AudioBuffer
var buffers = new Array(urls.length);

// for the instances of AudioBufferSourceNode
var sources = new Array(urls.length);

// Create original event
var event = document.createEvent('Event');
event.initEvent('complete', true, true);

// Get ArrayBuffer by Ajax
var load = function(url, index) {
    var xhr = new XMLHttpRequest();

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

            if (arrayBuffer instanceof ArrayBuffer) {
                // The 2nd argument for decodeAudioData
                var successCallback = function(audioBuffer) {
                    // Get the instance of AudioBuffer
                    buffers[index] = audioBuffer;

                    // The loading instances of AudioBuffer has completed ?
                    for (var i = 0, len = buffers.length; i < len; i++) {
                        if (buffers[i] === undefined) {
                            return;
                        }
                    }

                    // dispatch 'complete' event
                    document.dispatchEvent(event);
                };

                // The 3rd argument for decodeAudioData
                var errorCallback = function(error) {
                    if (error instanceof Error) {
                        window.alert(error.message);
                    } else {
                        window.alert('Error : "decodeAudioData" method.');
                    }
                };

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

    xhr.open('GET', url, true);
    xhr.responseType = 'arraybuffer';
    xhr.send(null);
};

for (var i = 0, len = urls.length; i < len; i++) {
    load(urls[i], i);
}

document.addEventListener('complete', function() {

    this.addEventListener('keydown', function(event) {
        // Space ?
        if (event.keyCode !== 32) {
            return;
        }

        event.preventDefault();

        // Get base time
        var t0 = context.currentTime;

        for (var i = 0, len = buffers.length; i < len; i++) {
            // Create the instance of AudioBufferSourceNode
            sources[i] = context.createBufferSource();

            // for legacy browsers
            sources[i].start = sources[i].start || sources[i].noteGrainOn;  // noteGrainOn
            sources[i].stop  = sources[i].stop  || sources[i].noteOff;

            // Set the instance of AudioBuffer
            sources[i].buffer = buffers[i];

            // AudioBufferSourceNode (Input) -> GainNode (Master Volume) -> AudioDestinationNode (Output)
            sources[i].connect(gain);
            gain.connect(context.destination);

            sources[i].start((t0 + i), 0, sources[i].buffer.duration);
            sources[i].stop(t0 + i + sources[i].buffer.duration);
        }

    }, true);

}, false);

少しややこしいので, アルゴリズムの概要をまとめておきます.

1. すべてのAudioBufferインスタンスを取得したときに発生する独自イベントの定義

// ....

// Create original event
var event = document.createEvent('Event');
event.initEvent('complete', true, true);

// ....

2. ワンショットサンプルのAudioBufferインスタンスを取得

// ....

// for the instances of AudioBuffer
var buffers = new Array(urls.length);

// ....

// Get ArrayBuffer by Ajax
var load = function(url, index) {
    var xhr = new XMLHttpRequest();

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

            if (arrayBuffer instanceof ArrayBuffer) {
                // The 2nd argument for decodeAudioData
                var successCallback = function(audioBuffer) {
                    // Get the instance of AudioBuffer
                    buffers[index] = audioBuffer;

                    // ....
                };

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

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

    xhr.open('GET', url, true);
    xhr.responseType = 'arraybuffer';
    xhr.send(null);
};

for (var i = 0, len = urls.length; i < len; i++) {
    load(urls[i], i);
}

// ....

3. 2. が完了したら独自イベントを発行して, イベントリスナーを設定

var load = function(url, index) {

// ....

    xhr.onload = function() {

        // ....

                // The 2nd argument for decodeAudioData
                var successCallback = function(audioBuffer) {

                    // ....

                    // The loading instances of AudioBuffer has completed ?
                    for (var i = 0, len = buffers.length; i < len; i++) {
                        if (buffers[i] === undefined) {
                            return;
                        }
                    }

                    // dispatch 'complete' event
                    document.dispatchEvent(event);
                };

        // ....

    };

    // ....

};

// ....

document.addEventListener('complete', function() {

    this.addEventListener('keydown', function(event) {
        // Space ?
        if (event.keyCode !== 32) {
            return;
        }

        event.preventDefault();

        // ....

4. スケジューリングの基準となる時刻を取得

// ....

document.addEventListener('complete', function() {

    this.addEventListener('keydown', function(event) {

        // ....

        // Get base time
        var t0 = context.currentTime;

        // ....

5. AudioBufferSourceNodeインスタンスの生成・プロパティの設定・ノード接続

// ....

// for the instances of AudioBufferSourceNode
var sources = new Array(urls.length);

// ....

document.addEventListener('complete', function() {

    this.addEventListener('keydown', function(event) {

        // ....

        for (var i = 0, len = buffers.length; i < len; i++) {
            // Create the instance of AudioBufferSourceNode
            sources[i] = context.createBufferSource();

            // for legacy browsers
            sources[i].start = sources[i].start || sources[i].noteGrainOn;  // noteGrainOn
            sources[i].stop  = sources[i].stop  || sources[i].noteOff;

            // Set the instance of AudioBuffer
            sources[i].buffer = buffers[i];

            // AudioBufferSourceNode (Input) -> GainNode (Master Volume) -> AudioDestinationNode (Output)
            sources[i].connect(gain);
            gain.connect(context.destination);

            // ....
        }
    }, true);

}, false);

6. 基準の時刻から, 1秒ごとにオーディオを0秒の位置から開始するようにスケジューリング

// ....

document.addEventListener('complete', function() {

    this.addEventListener('keydown', function(event) {

        // ....

        for (var i = 0, len = buffers.length; i < len; i++) {

            // ....

            sources[i].start((t0 + i), 0, sources[i].buffer.duration);

            // ....
        }
    }, true);

}, false);

7. 再生時間が経過すれば停止するようにスケジューリング

// ....

document.addEventListener('complete', function() {

    this.addEventListener('keydown', function(event) {

        // ....

        for (var i = 0, len = buffers.length; i < len; i++) {

            // ....

            sources[i].stop(t0 + i + sources[i].buffer.duration);
        }
    }, true);

}, false);

サウンドスケジューリングの本質的な処理はごくわずかで, 4. 6. 7. のみです. 些細な点では, フォールバックにnoteGrainOnを利用していることぐらいでしょう.

また, AudioBufferSourceNodeは使い捨てのノードなので, イベント発生のたびにインスタンスを生成していることにも着目してください.

デモ 08は, サンプルコード 12を改良して, スケジューリングの時刻を変更できるようにしていますが, 本質的な処理は変わらないので, 実際に試してみて, ワンショットサンプルのサウンドスケジューリング設定の基本を習得してみてください.

デモ 08

ちなみに, ワンショットサンプルのオーディオデータを利用する場合には, すべてのピッチ (音の高さ) に対応するオーディオデータを作成するのは大変です. データのロードもオーディオデータが多くなるほど時間を要します. そこで, playbackRateプロパティを利用してピッチを変更することによって, 1つのピッチに対応するオーディオデータから様々なピッチに対応させることが可能です.

表1 - 2 - c. playbackRateプロパティとピッチ
playbackRatePitch
0.1253オクターブ低い
0.2502オクターブ低い
0.5001オクターブ低い
1.000ピッチは変わらない
2.0001オクターブ高い
4.0002オクターブ高い
8.0003オクターブ高い

ガベージコレクション

AudioBufferSourceNodeは, 使い捨てのノードという仕様であることを解説しました. つまり, インスタンスの生成と破棄を繰り返すことが想定されています. したがって, AudioBufferSourceNodeインスタンスに割り当てられたメモリの解放が実行される条件については理解しておく必要があります.

AudioBufferSourceNodeインスタンスに限らず, Web Audio APIが定義するクラスのインスタンスにおいては, 以下の5つの条件すべてにあてはまる場合, ガベージコレクションの対象になります.

  • 参照が残っていない
  • オーディオが停止している
  • サウンドスケジューリングが設定されていない
  • ノードが接続されていない
  • 処理すべきデータが残っていない

つまり, 何らかの形で利用されているノードはガベージコレクションの対象とならないということです.

AudioBufferSourceNodeにおいて, ガベージコレクションが実行される条件で重要なのは, 最初の3つです. その理由は, AudioBufferSourceNodeは他のノードの出力先 (接続先) として利用されることがないこと, また, 最後の条件はConvolverNodeなどにおいて考慮すべきことであり, AudioBufferSourceNodeでは無関係だからです.

したがって, このセクションでは, 最初の3つの条件に関して解説します.

参照が残っていない

これは, ガベージコレクションが実装されている言語 (Javaなど) と同じことです.

サンプルコード 13は, ガベージコレクションが実行されます. その理由は, インスタンスの生成と同時に, 以前のインスタンスへの参照が破棄されて, かつ, インスタンス生成以外の処理 (サウンドの開始やスケジューリングの設定) をしていないからです (ただし, 新しく生成されたインスタンスへの参照は残ります) .

サンプルコード 13


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

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

// for the instance of AudioBufferSourceNode
var source = null;

window.setInterval(function() {
    // Create the instance of AudioBufferSourceNode
    source = context.createBufferSource();
}, 0);

オーディオが停止している

オーディオが停止していない, すなわち, オーディオを再生しているAudioBufferSourceNodeインスタンスは, 参照が残っていなくても, ガベージコレクションの対象となりません.

サンプルコード 14は, ガベージコレクションが実行されずに, メモリがしだいに不足していく例です. その理由は, コールバック関数実行のたびに, 以前のAudioBufferSourceNodeインスタンスへの参照は破棄されますが, それに対応するオーディオが停止していないからです.

サンプルコード 14


/*
 * Add code to sample code 01
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // for the instance of AudioBufferSourceNode
    var source = null;

    window.setInterval(function() {
        // Create the instance of AudioBufferSourceNode
        source = context.createBufferSource();

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

        // Set the instance of AudioBuffer
        source.buffer = audioBuffer;

        // Loop ON
        source.loop = true;

        // AudioBufferSourceNode (Input) -> AudioDestinationNode (Output)
        source.connect(context.destination);

        // Start audio
        source.start(0);
    }, 0);
};

// ....

ループ再生でなければ, (他の条件が満たされているうえで) オーディオの再生時間が経過すると, 停止状態と認識されて, 自動的にガベージコレクションは実行されますが, それでも, startメソッドとstopメソッドは一対という考え方は重要です. なぜなら, AudioBufferSourceNodeインスタンスの状態遷移を設計するためにも必要となるからです.

ちなみに, 状態遷移の設計が必要な理由は, start / stopメソッドの多重呼び出しや, startメソッドを実行せずに, stopメソッドを実行するとエラーが発生するからです (サンプルコード 15, 16) .

サンプルコード 15


/*
 * Add code to sample code 01
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // Create the instance of AudioBufferSourceNode
    var source = context.createBufferSource();

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

    // Set the instance of AudioBuffer
    source.buffer = audioBuffer;

    // AudioBufferSourceNode (Input) -> AudioDestinationNode (Output)
    source.connect(context.destination);

    // Start audio
    source.start(0);

    // Error !!
    source.start(0);
};

// ....

サンプルコード 16


/*
 * Add code to sample code 01
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // Create the instance of AudioBufferSourceNode
    var source = context.createBufferSource();

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

    // Set the instance of AudioBuffer
    source.buffer = audioBuffer;

    // AudioBufferSourceNode (Input) -> AudioDestinationNode (Output)
    source.connect(context.destination);

    // source.start(0);

    // Error !!
    source.stop(0);
};

// ....

サウンドスケジューリングが設定されていない

サウンドスケジューリングが設定されているAudioBufferSourceNodeインスタンスは, 参照が残っていなくても, ガベージコレクションの対象となりません.

サンプルコード 17は, 時間が経過するほどオーディオ開始が少しずつ遅延するようにサウンドスケジューリングを設定しているので, AudioBufferSourceNodeインスタンスへのメモリの割り当てが増加し, メモリが不足していきます.

サンプルコード 17


/*
 * Add code to sample code 01
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // for the instance of AudioBufferSourceNode
    var source = null;

    var counter = 0;

    window.setInterval(function() {
        // Create the instance of AudioBufferSourceNode
        source = context.createBufferSource();

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

        // Set the instance of AudioBuffer
        source.buffer = audioBuffer;

        // AudioBufferSourceNode (Input) -> AudioDestinationNode (Output)
        source.connect(context.destination);

        // Start audio
        source.start(context.currentTime + counter++);
    }, 0);
};

// ....

オーディオデータの再生 まとめ

このページでは, Web Audio APIでオーディオデータを再生するための方法について解説してきました.

単純にオーディオデータを再生するだけであれば, HTMLMediaElementを利用したほうが良く, より高度な処理をオーディオデータに適用したい場合にWeb Audio APIを利用するというのが前提でした.

Web Audio APIでオーディオデータを再生するには2つの方法があり,

  • ArrayBufferから生成できるAudioBufferとAudioBufferSourceNodeを利用する
  • HTMLMediaElementとMediaElementAudioSourceNodeを利用する

いずれの方法にも共通する処理をまとめると以下のようになります.

  1. 1. オーディオデータを取得 (ArrayBuffer or HTMLMediaElement)
  2. 2. 入力点を生成
  3. 3. 入力点と出力点を接続
  4. 4. 音源のスイッチをON

また, それぞれの最適な利用ケースは以下のようになるでしょう (リバーブに関しては, エフェクターのページで解説します) .

表1 - 2 - b. Web Audio API オーディオデータの再生
CaseSourceNode
ワンショットArrayBuffer -> AudioBufferAudioBuffreSourceNode
楽曲データHTMLMediaElementtMediaElementAudioSourceNode
リバーブ
(エフェクター)
ArrayBuffer -> AudioBufferConvolverNode

サウンドスケジューリングにおける重要なポイントは以下の2点です.

  • 時刻の取得 (AudioContextインスタンスのcurrentTimeプロパティ)
  • start / stopメソッドの引数

また, ガベージコレクションや状態遷移を設計するうえで重要なのは, 以下の2点です.

  • 何らかの形で利用されているノードはガベージコレクションの対象とならない
  • startメソッドとstopメソッドは一対