JavaScriptでリアルタイムに音を鳴らす方法を3つほど
最近、JavaScriptで動くMMLシーケンサーを作っています。
SiONのMMLを
このあたりのMMLは結構聴ける(適宜コピペしてお試しください)
- http://mmltalks.appspot.com/m/RaKRP
- http://mmltalks.appspot.com/m/DIZpo
- http://mmltalks.appspot.com/m/jVIoz (FM音源が惜しい)
で、
で、表題の件ですがJavaScriptでリアルタイムに音を鳴らすには以下の3つの方法があります。
簡単なサンプルをあげています → サンプル ソース
*Operaでは何故か動きません。(MMLシーケンサーの方は何故か動く)
2011/09/18 追記
OSX Lion Chrome 14で Web Audio API がクラッシュする問題が解決されているみたいです。
(OSX版Google 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; };