音の鳴るブログ

鳴らないこともある

Web Audio系APIを使うときに注意する点

Web Music Developers JP Advent Calendar 2012 の 12日目です。

前回の記事では信号処理用の簡易インターフェイスを紹介しましたが、今日は内部でどういう処理を行なっているのかという話です。

といっても、音を作るための高度な信号処理の話ではなくて、各APIごとの簡単な使い方とブラウザによって使えるAPIが異なるだけでなく、同じAPIでもOSによって動作が異なったりバグっぽいのがあったりという状態なので、それらをできるだけ回避するためのバッドなノウハウ集といった具合、地獄の様相です。

Web Audio API

やり方

まず使えるかどうかを判断します。

if (typeof webkitAudioContext != "undefined") {
}

Web Audio APIでは new webkitAudioContext() で作られる AudioContext が全ての起点になります。このインスタンスからディレイやフィルターなどの様々なオブジェクトのインスタンスを生成し、それらを組み合わせて音を作っていくのですが、ここではそういった豊富な機能を使わずに自前で書くときの定石を紹介します。

手順

  • new webkitAudioContext() する
  • コールバック関数 onaudioprocess を用意する
  • BufferSourceNode -> JavaScriptNode -> context.destination という経路を作成する
  • 定期的に onaudioprocess が呼ばれるので信号を埋めていく

サンプルコード

var context = new webkitAudioContext();

SampleRate   = context.sampleRate; // サンプリングレートは固定
Channles     = 2;
BufferLength = 1024;

// コールバック関数 (これが定期的に呼ばれる)
var onaudioprocess = function(e) {
    var outL = e.outputBuffer.getChannelData(0),
        outR = e.outputBuffer.getChannelData(1);

    // ここで処理する (Float32Arrayが返ってくる想定)
    var stream = gen.process();
        
    // ここで信号を書き込む
    for (var i = 0; i < BufferLength; i++) {
        outL[i] = outR[i] = stream[i];
    }
    
};

// ノードの作成と接続
var bufSrc = context.createBufferSource();
var jsNode = context.createJavaScriptNode(BufferLength, 2, Channels);

bufSrc.noteOn(0);       // iOSだと
bufSrc.connect(jsNode); // これが超重要

jsNode.onaudioprocess = onaudioprocess;
jsNode.connect(context.destination); // onaudioprocess が呼ばれるようになる

注意1: typeof webkitAudioContext == "function" はダメ

typeof の結果が Chrome だと "function"Safari だと "object" で返ってきますので not undefined で判断します。

注意2: サンプリングレートは固定

サンプリングレートが環境に応じてAPI側で決められてしまうので、音を作る側で考慮する必要があります。

注意3: PC版とiOS版でちょっと違う

ポイントとなるのは BufferSourceNode で、PC版だと不要ですがiOS版だとこれが無いと JavaScriptNode が動き出しません。

注意4: Linux版は性能がめっちゃ悪い

まともに鳴りません。諦めるか別の方法を使ったほうが良いでしょう。

注意5: UIWebViewも性能悪い

一応 userAgent に Safari があるかないかで判断できるみたいだけど、、


Audio Data API

やり方

まず使えるかどうかを判断します。

if (typeof Audio == "function" && typeof new Audio().mozSetup == "function") {
}

Audio Data APIでは自分で信号処理を書くしかありません。

まず音を書き込むためのAudioインスタンスを作成してサンプリングレートなどの設定を行います。 あとはタイマー制御でひたすら信号を書き込んで音を作っていきます。

手順

  • new Audio() して
  • audio.mozSetup() して
  • audio.mozWriteAudio() する

Float32Array にステレオの場合は L, R, L, R... といった具合に交互にデータを入力してから mozWriteAudio() で書き込みます。また、Web Audio APIと違って自動的にコールバック関数が呼ばれたりしないので、自分でタイマー制御を行う必要があります。

サンプルコード

var audio = new Audio();

SampleRate   = 44100; // サンプリングレートは自分で決める
Channles     = 2;
BufferLength = 1024;

audio.mozSetup(Channels, SampleRate);

var interleaved = new Float32Array(BufferLength * Channels);

// コールバック関数 (これが定期的に呼ばれる)
var onaudioprocess = function() {
    // ここで処理する (Float32Arrayが返ってくる)
    var stream = gen.process();

    // ここで信号を書き込む (L, R, L, R... と交互に書き込む)
    for (var i = 0, j = 0; i < BufferLength; i++) {
        interleaved[j++] = interleaved[j++] = stream[i];
    }
    audio.mozWriteAudio(buffer);
};

// コールバック関数の呼び出しは自分で書く
var interval = BufferLength / SampleRate * 1000;
setInterval(onaudioprocess, interval);

注意1: Linux版にバグがある

以前 こういう記事 を書きましたが、どうも最新のLinux版Firerox(*1)の mozCurrentSampleOffset() は値が増えたり減ったりするため使えません(内部バッファのインデックスをそのまま返しているように見える)。なので、setIntervalの間隔を計算するのが当面は良さそうです。

注意2: タイマー制御の問題

setInterval を使ったタイマー制御をしている時に異なるタブを開くとタイマー精度が悪くなる(1sec間隔になる)問題があって、その場合音がブチブチと途切れてしまいます。回避策としてWebWorkerを使う方法があります。このライブラリ に書いてありますので、必要があれば参考にしてください。

*1 Firefox 17.0 - Mozilla Firefox for Ubuntu canonical - 1.0 で確認


その他

Flash経由で音を出す方法や、wavファイルを動的に作っては再生するという方法がありますが、ちょっと無理矢理感が高いです。興味があれば このあたり を参考にしてください。

あと、IE9にはTypedArrayがないみたいなので適当にでっちあげたりする必要があります。

// めっちゃ適当な Float32Array
Float32Array = Array;

まとめ

とりあえず出力部分はライブラリに任せるようにしたほうが幸福度は高いと思います。