Tuesday, November 27, 2012

JavaScriptで音を作る

こんにちは。清水です。

この前、個人的な趣味で「TanTone」というWebブラウザ上で効果音を作れるサイトを作りました。波の形や周波数を決めたり、複数の波を組み合わせたりして、こんな音こんな音こんな音を作ることができます。詳しい使い方はヘルプにて。

この記事ではTanToneの技術的な話として、JavaScriptで音(WAVEファイル)を作る方法を紹介します。

ちなみに、この方法だとGoogle ChromeとSafariでしか動きませんでした。

大まかに書くと
  • WAVEファイルのバイト列を作る
  • BASE64でエンコード
  • audioタグにdata URLスキームとして設定
ということをしています。

以下はそれを簡単に実装したサンプルです。
<html>
  <head></head>
  <body>
    <input type="button" value="Play" id="play-button">
    <audio id="audio" autoplay="autoplay"></audio>
    <script type="text/javascript">
      var base64Encode = function(bytes) {
          var base64Characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
          var bytesLength = bytes.length;
          var encoded = '';
          var paddingLength = 3 - bytesLength % 3;
          if (paddingLength == 3) paddingLength = 0;
          var rest = 0;
          for (var i = 0; i < bytesLength; ++i) {
              var shiftLength = (i % 3) * 2 + 2;
              encoded += base64Characters.charAt((rest << (8 - shiftLength)) | (bytes.charCodeAt(i) >> shiftLength));
              rest = bytes.charCodeAt(i) & (0xff >> (8 - shiftLength));
              if (i % 3 == 2) {
                  encoded += base64Characters.charAt(rest);
                  rest = 0;
              }
          }
          if (bytesLength % 3 != 0) encoded += base64Characters.charAt(rest << (paddingLength % 3 * 2));
          for (var i = 0; i < paddingLength; ++i) encoded += '=';
          return encoded;
      };
      var bytesFromInt = function(value, bytes) {
          if (value == undefined || bytes == undefined || isNaN(value) || value == Infinity) throw 'Invalid value: ' + value + ' or bytes: ' + bytes;
          var intValue = Math.floor(value);
          var result = '';
          for (var i = 0; i < bytes; ++i) {
              var byte = (intValue & (0xFF << (i * 8))) >> (i * 8);
              result += String.fromCharCode(byte);
          }
          return result;
      };
      window.onload = function() {
          document.getElementById('play-button').onclick = function() {
              var samplesPerSecond = 44100;
              var channels = 2;
              var bytesPerSample = 2;
              var bytes = '';
              var formatPartLength = 16;
              var dataLength = samplesPerSecond * channels * bytesPerSample;
              bytes += 'RIFF';
              bytes += bytesFromInt(4 + 4 + 4 + formatPartLength + 4 + dataLength, 4);
              bytes += 'WAVE';
              bytes += 'fmt ';
              bytes += bytesFromInt(formatPartLength, 4);
              bytes += bytesFromInt(1, 2);
              bytes += bytesFromInt(channels, 2);
              bytes += bytesFromInt(samplesPerSecond, 4);
              bytes += bytesFromInt(samplesPerSecond * bytesPerSample * 8 * channels, 4);
              bytes += bytesFromInt(bytesPerSample * 8 * channels, 2);
              bytes += bytesFromInt(bytesPerSample * 8, 2);
              bytes += 'data';
              bytes += bytesFromInt(dataLength, 4);
              var waveFraction = 440;
              for (var sample = 0; sample < samplesPerSecond; ++sample) {
                  var value = Math.sin(Math.PI * 2 * sample / samplesPerSecond * waveFraction) * 32767 * 0.5;
                  for (var channel = 0; channel < channels; ++channel) {
                      bytes += bytesFromInt(value, 2);
                  }
              }
              document.getElementById('audio').src = 'data:audio/wav;base64,' + base64Encode(bytes);
          };
      };
    </script>
  </body>
</html>

HTMLとしてまるっと保存すると使えるハズです。"Play"というボタンを押すと「ラ」の音(440Hzの正弦波)が1秒間鳴ります。

バイト列を作る

バイト列を作りaudioタグに設定するまでの処理は
document.getElementById('play-button').onclick
の中で行っています。この中の
bytes += 'RIFF';
の行からバイト列を作り始めています。バイト列は文字列として作成し、数値データは真ん中くらいにある bytesFromInt 関数でバイト列に変換しています。

WAVEファイルのフォーマットについては近藤正芳さんのページ「wav ファイルフォーマット」を参考にしました。とてもわかりやすいです。

今回作るデータはサンプリングレート44.1kHz、ステレオ、16ビットで440Hzの正弦波を1秒間含むものです。

bytes += 'data';
辺りまではfmtチャンクの作成です。実際に波形データを作っているのはこの下の
var waveFraction = 440;
for (var sample = 0; sample < samplesPerSecond; ++sample) {
    var value = Math.sin(Math.PI * 2 * sample / samplesPerSecond * waveFraction) * 32767 * 0.5;
    for (var channel = 0; channel < channels; ++channel) {
        bytes += bytesFromInt(value, 2);
    }
}
のループです。1秒間のデータを作るのでsamplesPerSecond(サンプリングレート = 44100)回ループし、波形の情報をchannels(ステレオなので2)個ずつ追加していっています。

このサンプルで作っているのは正弦波と呼ばれる単純な波です。これを作るには時間に対してsinを適用するだけです。それをやってるのが
var value = Math.sin(Math.PI * 2 * sample / samplesPerSecond * waveFraction) * 32767 * 0.5;
このコードです。
Math.sin(Math.PI * 2 * sample / samplesPerSecond * waveFraction)
で今の時間におけるsinの値を計算します。

16ビットPCMの値は-32768から32767になり、0が無音です。なので算出したsinの値(-1から1)に32767をかけて16ビットの値に変換します。それだけだと音量が大きいので今回は0.5をかけて半分にしています。

これを左右の2つずつ44100回追加すれば完成です。

audioタグにセット

作成したバイト列をBASE64エンコードして、先頭に "data:audio/wav;base64," を追加したものをaudioタグのsrcにセットすると、出来上がった音をaudioタグで再生することができます。audioタグにautoplayを設定しておくとsrcをセットした時にすぐ音を鳴らすことができます。


かなり妙な方法で実装しましたが、ちゃんとAudio API的なのを使うともっと簡単にできるのかも知れません。。。

No comments:

Post a Comment