JavaScriptで波をつくろう。リアルタイム波形生成&再生

前のエントリでこんなことを書きました。

JavaScriptで波形データを読み書きすることができる。しかし再生するのは難しい。
HTML5のaudioタグとData URIを組み合わせればできないこともないが、コストが大きすぎる。

コストが大きいのは音声ファイルが大きいからです。50MBある波形をいちいち変換してられません。
でも小さい波形ならできるかもしれない! ということでやってみました。

基本的なアイディア

  1. 波形データをつくる(数値の配列)
  2. 波形をバイナリ列に変換する
  3. バイナリ列にWAVヘッダを付加する
  4. Base64エンコード
  5. audioタグのsrc属性に指定
  6. audioを再生

つくったもの

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!

エンコードした文字列に "data:audio/wav;base64" を付加

concatするだけです。

これだけの知識があれば上のサンプルと同じものがつくれます!

やったね!

つくりながら思ったこと

リアルタイムレンダリングは厳しい

audioタグはこういう用途で使われることを想定していないようで、どのブラウザでもいまいち綺麗に鳴りません。
試した中ではFirefoxが一番うまくいっていましたが、アクティビティモニタでスレッド数を確認すると楽しいことに*1
MacSafariはプロセス間通信しまくりでダメダメでした。Appleは「Flashなんて必要ない」と言う前にaudioタグを改善するべきです。

オフラインレンダリングは使える

ブラウザのaudioタグがダメダメなら全部JavaScriptでやってしまえばいい、というのがオフラインレンダリング
個人的な感触では、これ、使えます。いけます。
リアルタイムでやるよりも(波形合成のコストがある分)やや重いですが、オフラインならaudioタグに触る必要がないのでWeb Workersで処理できます。
少なくともユーザーにリアルタイムだと勘違いさせる程度の処理速度は実現できそうな感じです。

HTML5すごい! で終わらせたくない

こういう感じのエントリを書くと、「HTML5すげー、JavaScriptすげー」みたいな反応をよく聞きます。
でもそれだけで終わらせるのはもったいないです。
僕はJavaScript歴4ヶ月のビギナーですが、上のプログラムを6時間で書くことができました。
もしあなたが僕よりも長くJavaScriptを使っているのなら、より良いものをより短い時間で作ることができるかもしれません。
僕の知らないAjaxの世界で、もっと面白いことができるのかもしれません。

まとめ

Canvas、Video、AudioとJavaScriptでマルチメディア処理をする技術は出揃いました。
あとはクリエイターのアイディアと、少しのコーディングで"波"をつくることができます。
楽しくて、面白くて、みんなとつながる、大きな"波"をつくることができます。


準備はすべて整いました。
JavaScriptで、"波"をつくりましょう。


*1:どうやらaudioタグの数だけスレッドを生成しているようです。