JavaScriptでリアルタイムに音を鳴らす方法を3つほど

最近、JavaScriptで動くMMLシーケンサーを作っています。


SiONMMLパク参考にしているので、MMLTalks(http://mmltalks.appspot.com/)あたりのMMLをコピペすると、それなりに鳴ってくれる場合があります。思い通りになっていない部分がかなり多いですが、、

このあたりのMMLは結構聴ける(適宜コピペしてお試しください)

で、

で、表題の件ですがJavaScriptでリアルタイムに音を鳴らすには以下の3つの方法があります。

  • Audio Data APIを使う(Firefox 5.0以降)
  • Web Audio APIを使う(Chrome, Safari)
  • wavデータを直接放り込む(上記に加えてOperaも動く)


簡単なサンプルをあげています → サンプル ソース
*Operaでは何故か動きません。(MMLシーケンサーの方は何故か動く)



2011/09/18 追記
OSX Lion Chrome 14で Web Audio API がクラッシュする問題が解決されているみたいです。
(OSXGoogle Chrome 14.0.835.163で確認)



注意
現状、OSX Lion の Chrome を使っている人は Web Audio API を使うとタブがクラッシュします。

準備

とりあえず鳴らす音を作るためのクラス。本質的な部分ではないのでかなり適当。要は2つのサイン波が適当にミックスされて鳴る。サンプルでプチプチ鳴るのはここの作りがマズイから。

var samplerate = 48000, channel = 1, stream_length = 4096;

var sinwave = function(frequency) {
    this.phase = 0.0;
    this.phaseStep = frequency / samplerate;
};

sinwave.prototype.next = function() {
    var retval = Math.sin(2 * Math.PI * this.phase);
    this.phase += this.phaseStep;
    return retval;
};

var StreamGenerator = function() {
    this.gen1  = new sinwave(440);
    this.gen2  = new sinwave(660);
    this.phase = new sinwave(880);
};

StreamGenerator.prototype.next = function() {
    var stream = [];
    var i, imax;
    var g1 = this.gen1, g2 = this.gen2;
    var v1 = this.phase.next() / 2.0 + 0.5, v2 = 1.0 - v1;
    for (i = 0, imax = stream_length; i < imax; i++) {
        stream[i] = (g1.next() * v1 + g2.next() * v2);
    }
    return stream;
};

Audio Data APIを使う

対応ブラウザ: Firefox 5.0以降
仕様等についてはこちら→ https://wiki.mozilla.org/Audio_Data_API

セットアップ

Audio を new して mozSetup でチャネル数とサンプリング周波数を指定する。

audio = new Audio();
audio.mozSetup(channel, samplerate);
再生

mozWriteAudio に Float32Array を突っ込んで呼び出す。
mozWriteAudioはノンブロックなメソッドなので、ストリーム再生みたいな用途の場合は setInterval で定期的に呼び出してはstreamを与えてやる。

stream = new Float32Array();
audio.mozWriteAudio(stream);

ステレオのデータは、stream配列に交互にデータを入れる。

stream = new Float32Array([L, R, L, R, L, R, .... ]);
ここがイライラする

setIntervalで呼び出しまくるのは面倒くさいのでどうにかしてほしい。

簡単な例

準備で作ったジェネレータをつかって再生するためのクラス。

/**
 * Audio Data API用のプレイヤー (Firefox 5.0-)
 */
var MozPlayer = function() {
    this.audio = new Audio();
    this.audio.mozSetup(channel, samplerate);
    this.timerId = null;
    this.isPlaying = false;
};

MozPlayer.prototype.play = function(gen) {
    var self = this;
    if (this.timerId === null) {
        this.timerId = setInterval(function() {
            var s = new Float32Array(gen.next());
            self.audio.mozWriteAudio(s);
        }, stream_length / samplerate * 1000);
    }
    this.isPlaying = true;
};

MozPlayer.prototype.stop = function() {
    if (this.timerId !== null) {
        clearInterval(this.timerId);
    }
    this.isPlaying = false;
};

Web Audio APIを使う

対応ブラウザ: Chrome, Safari (ただしどちらもOSX版では現状使えない)
仕様等についてはこちら→ https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html


Chromeの場合は、アドレスバーに about:flags と入力した先で、ウェブオーディオを有効にすると使えるようになる。
Safariの場合は、それ用のSafariがあってダウンロードできたんだけど、今はできない。そのうちできるようになるでしょう。

セットアップ

webkitAudioContext を new して createJavaScriptNode を呼び出す。
サンプリング周波数は固定で、stream_length はあまり大きい値は設定できないみたい。
(準備のときに samplerate = 48000, stream_length = 4096 にしているのはこれに合わせているため)

context = new webkitAudioContext();
node = context.createJavaScriptNode(stream_length, 1, channel);
console.log(context.sampleRate);
再生

node.connect(context.destination) を呼ぶと、
再生のタイミングで node.onaudioprocess が呼び出されるので、その中で arguments[0].outputBuffer.getChannelData して
そいつにデータを代入する。onaudioprocess は定期的に呼び出されるので楽チン。

node.onaudioprocess = function(event) {
  var data = event.outputBuffer.getChannelData(0);
  var s = gen.next();
  var i = data.length;
  while (i--) data[i] = s[i];
};
node.connect(context.destination);
簡単な例

準備で作ったジェネレータをつかって再生するためのクラス。

/**
 * Web Audio API用のプレイヤー (Chrome, Safari)
 */
var WebkitPlayer = function() {
    this.context = new webkitAudioContext();
    this.node = this.context.createJavaScriptNode(stream_length, 1, channel);
    this.isPlaying = false;
};

WebkitPlayer.prototype.play = function(gen) {
    var self = this;
    this.node.onaudioprocess = function(event) {
        var data = event.outputBuffer.getChannelData(0);
        var s = gen.next();
        var i = data.length;
        while (i--) data[i] = s[i];
    };
    this.node.connect(this.context.destination);
    this.isPlaying = true;
};

WebkitPlayer.prototype.stop = function() {
    this.node.disconnect();
    this.isPlaying = false;
};

wavデータを直接放り込む

対応ブラウザ: Chrome, Opera, Firefox(変な感じになる)
詳しくはここに書いてある→ http://d.hatena.ne.jp/yanagia/20100323/1269334226

セットアップ

とくになし

再生

このあたり http://www.kk.iij4u.or.jp/~kondo/wave/ を参考にWAVファイルヘッダと再生バイトデータを作る。

wav = btoa(wavheader + bytes); // ややこしい処理
audio = new Audio("data:audio/wav;base64," + wav);
audio.play();

Audio Data APIと同じくsetIntervalなりで定期的に呼び出してやる必要がある。
ステレオのデータは、Audio Data APIと同じようにbytes内のデータが交互になるようにする。

ここがイライラする

これってストリーム再生じゃなくて細かいwavファイルを大量につくって再生しているだけなんで醜い。しかも合間合間でブチブチなったりする。ブチブチを少なくするためにファイルごとの再生時間を長くするともっさりする。嫌い。


あと、これはJavaScriptの問題なんだけど、たとえば3秒ごとに再生したくて

setInterval(func, 3 * 1000);

とすると、音が出始めるまでに3秒かかる。

func();
setInterval(func, 3 * 1000);

ってやってみると、最初と二回目の再生のタイミングがずれて気持ち悪い感じになる。
それと、OperaでFloat32Array使えないのどうにかなりませんかね。

簡単な例

準備で作ったジェネレータをつかって再生するためのクラス。

/**
 * Audioタグを使うプレイヤー (Chrome, Firefox, Opera)
 */
var HTML5AudioPlayer = function() {
    this.audio = null;
    this.timerId = null;
    this.isPlaying = false;
};

HTML5AudioPlayer.prototype.wavheader = function(samples) {
    var waveBytes = samples * channel * 2,
        l1 = waveBytes - 8,
        l2 = l1 - 36,
        retval = String.fromCharCode(
            0x52, 0x49, 0x46, 0x46, // 'RIFF'
            (l1 >>  0) & 0xFF, (l1 >>  8) & 0xFF,
            (l1 >> 16) & 0xFF, (l1 >> 24) & 0xFF,
            0x57, 0x41, 0x56, 0x45, // 'WAVE'
            0x66, 0x6D, 0x74, 0x20, // 'fmt '
            0x10, 0x00, 0x00, 0x00, // byte length
            0x01, 0x00, // linear pcm
            channel, 0x00, // channel
            (samplerate >>  0) & 0xFF,
            (samplerate >>  8) & 0xFF,
            (samplerate >> 16) & 0xFF,
            (samplerate >> 24) & 0xFF,
            ((samplerate*channel*2) >>  0) & 0xFF,
            ((samplerate*channel*2) >>  8) & 0xFF,
            ((samplerate*channel*2) >> 16) & 0xFF,
            ((samplerate*channel*2) >> 24) & 0xFF,
            0x04, 0x00, // block size
            0x10, 0x00, // 16bit
            0x64, 0x61, 0x74, 0x61, //'data'
            (l2 >>  0) & 0xFF, (l2 >>  8) & 0xFF,
            (l2 >> 16) & 0xFF, (l2 >> 24) & 0xFF);
    return retval;
};

HTML5AudioPlayer.prototype.play = function(gen) {
    var self = this;
    var itercount = 20;
    if (this.timerId === null) {
        this.timerId = setInterval(function() {
            var bytes = [], s, x, wav;
            var i, imax, j, jmax;
            for (i = 0, imax = itercount; i < imax; i++) {
                s = gen.next();
                for (j = 0, jmax = s.length; j < jmax; j++) {
                    x = (s[j] * 32767.0) >> 0;
                    bytes.push(String.fromCharCode(x & 0xFF, (x >> 8) & 0xFF));
                }
            }
            wav = btoa(self.wavheader(bytes.length) + bytes.join(''));
            self.audio = new Audio("data:audio/wav;base64," + wav);
            self.audio.play();
        }, stream_length * itercount / samplerate * 1000);
    }
    this.isPlaying = true;
};

HTML5AudioPlayer.prototype.stop = function() {
    if (this.timerId !== null) {
        clearInterval(this.timerId);
    }
    if (this.audio !== null) {
        this.audio.pause();
        this.audio = null;
    }
    this.isPlaying = false;
};
まとめ

ちょっと音を出したいだけなんだけど、やり方がいろいろあるのはだるいので一つにしてほしいですね。
僕は Web Audio API が好みなんだけど、前述のとおり OSX Lion で動かないので、非常に悲しい。

Firefox Chrome Opera Safari IE9.0
Audio Data API 使える 使えない 使えない 使えない 使えない
Web Audio API 使えない 使え(る│た) 使えない 使えた 使えない
HTML5 Audio 使える 使える 使える 使えない? 知らん