JavaScriptで波をつくろう。リアルタイム波形生成&再生
前のエントリでこんなことを書きました。
JavaScriptで波形データを読み書きすることができる。しかし再生するのは難しい。
HTML5のaudioタグとData URIを組み合わせればできないこともないが、コストが大きすぎる。
コストが大きいのは音声ファイルが大きいからです。50MBある波形をいちいち変換してられません。
でも小さい波形ならできるかもしれない! ということでやってみました。
つくったもの
http://yanagiatool.appspot.com/jsaudio/mmltest.html
シンプルなMMLプレイヤーです。JavaScript + HTML5。
ベロシティとかループとかはありませんが、かえるのうたは演奏できます。
演奏に使う波形は下のテキストボックスで制御できます。
実際のコードとかはgithubに。
大雑把なしくみ解説
「リアルタイムレンダリング」のほうは音符ひとつにつきWAVファイルをひとつ生成し、タイマーでスケジューリングして鳴らしてます。
WAVファイルは再生前に"作り置き"するのではなく、再生中に動的に生成しています。
「オフラインレンダリング」は再生前に全てのWAVファイルを生成、JavaScriptでそれらを合成してひとつの大きなWAVにしてからaudioタグに渡してます。
もう少し細かなしくみ解説
「リアルタイムレンダリング」はレンダリングコールバックを定義して、setIntervalで定期的に呼び出しています。
var Timer; function play(){ // ボタンが押されるとこの関数が呼ばれる ... // 再生開始の準備 Timer = setInterval(renderBar, // レンダリングコールバック 1000 * ((60.0 / Score.bpm) / (16 / 4))); // 16分音符の長さ }
コールバックの中で波形とaudioタグを生成して再生バッファに突っ込みます。音符ひとつにつきaudioタグひとつ。
var renderBuffer = []; function renderBar(){ // レンダリングコールバック var note; ... // 音符のコレクションから「今発音すべき音符」を探索してくる var signal = createSignal(note.duration, note.pitch); // 波形を動的に生成 var url = convertToURL(signal); // 波形にWAVEヘッダを付与、Base64エンコード var audio = new Audio(url); // audio要素を生成 document.getElementById("anywhere").appendChild(audio); // audio要素をhtmlに追加。この時点でurlからロードがはじまる renderBuffer.push(audio); // すぐには再生できないので、一旦バッファに貯めておく ... // 他に発音すべき音符がないか確認 setTimeout(playBuffer, 10); // 再生バッファを再生 } function playBuffer(){ for(var i = 0; i < renderBuffer.length; i++){ renderBuffer[i].play(); } renderBuffer = []; // バッファをクリア }
また、このアプローチだと再生が終わったaudioタグは不要になるので、適当なタイミングで要素を削除するコードも差し込みます。
audio.pause(); document.getElementById("anywhere").removeChild(audio); // HTMLからの参照を切る。これでGCされる
波形の生成について
今回使用している波形のフォーマットは以下の通りです。
- サンプリングレート 44.1kHz
- 量子化ビット 8bit
- モノラル
ページのユーザーコードは上のフォーマットの数値配列を返却することを期待しています。
数値配列は以下のコードでunsigned char(8bit)のバイナリに変換できます。
var signal = userSignal; var binary = ""; for(var i = 0; i < signal.length; i++){ binary += String.fromCharCode(signal[i]); } }
波形をData URIに変換する
波形バイナリにWAVヘッダを付加する
以下のコードで、44100Hzの8bitモノラルな波形バイナリにヘッダを付加できます。
var signals = "波形のバイナリ"; var header; header = "WAVEfmt " + String.fromCharCode(16, 0, 0, 0); header += String.fromCharCode(1, 0); // format id header += String.fromCharCode(1, 0); // channels header += String.fromCharCode(68, 172, 0, 0); // sampling rate header += String.fromCharCode(68, 172, 0, 0); // byte/sec header += String.fromCharCode(1, 0); // block size header += String.fromCharCode(8, 0); // byte/sample header += "data"; // data chunk label var siglen = signals.length; var sigsize; sigsize = String.fromCharCode((siglen >> 0 & 0xFF), (siglen >> 8 & 0xFF), (siglen >> 16 & 0xFF), (siglen >> 24 & 0xFF)); header += sigsize; var wavlen = header.length + signals.length; var riff = "RIFF"; riff += String.fromCharCode((wavlen >> 0 & 0xFF), (wavlen >> 8 & 0xFF), (wavlen >> 16 & 0xFF), (wavlen >> 24 & 0xFF)); wavefile = riff + header + signals;
このコードは
WAV ファイルフォーマット
http://www.kk.iij4u.or.jp/~kondo/wave/
を参考にして作成しました。thanks!
バイナリをBase64エンコード
楽をしたいので変換モジュールを探してきます。
今回は弾さんのbase64.jsを使いました。
javascript - Yet Another Base64 transcoder
http://blog.livedoor.jp/dankogai/archives/51067688.html
thanks!
これだけの知識があれば上のサンプルと同じものがつくれます!
やったね!
つくりながら思ったこと
リアルタイムレンダリングは厳しい
audioタグはこういう用途で使われることを想定していないようで、どのブラウザでもいまいち綺麗に鳴りません。
試した中ではFirefoxが一番うまくいっていましたが、アクティビティモニタでスレッド数を確認すると楽しいことに*1。
MacのSafariはプロセス間通信しまくりでダメダメでした。Appleは「Flashなんて必要ない」と言う前にaudioタグを改善するべきです。
オフラインレンダリングは使える
ブラウザのaudioタグがダメダメなら全部JavaScriptでやってしまえばいい、というのがオフラインレンダリング。
個人的な感触では、これ、使えます。いけます。
リアルタイムでやるよりも(波形合成のコストがある分)やや重いですが、オフラインならaudioタグに触る必要がないのでWeb Workersで処理できます。
少なくともユーザーにリアルタイムだと勘違いさせる程度の処理速度は実現できそうな感じです。
HTML5すごい! で終わらせたくない
こういう感じのエントリを書くと、「HTML5すげー、JavaScriptすげー」みたいな反応をよく聞きます。
でもそれだけで終わらせるのはもったいないです。
僕はJavaScript歴4ヶ月のビギナーですが、上のプログラムを6時間で書くことができました。
もしあなたが僕よりも長くJavaScriptを使っているのなら、より良いものをより短い時間で作ることができるかもしれません。
僕の知らないAjaxの世界で、もっと面白いことができるのかもしれません。
まとめ
Canvas、Video、AudioとJavaScriptでマルチメディア処理をする技術は出揃いました。
あとはクリエイターのアイディアと、少しのコーディングで"波"をつくることができます。
楽しくて、面白くて、みんなとつながる、大きな"波"をつくることができます。
準備はすべて整いました。
JavaScriptで、"波"をつくりましょう。
*1:どうやらaudioタグの数だけスレッドを生成しているようです。