初めてのWebAudio: スーパーマリオのテーマソング


この記事はWanoグループアドベントカレンダーの21日目です。
投稿された日付が違う気もしますが、気のせいです。気のせいです...。

さて、だいぶ前にこんな本を買っていました。
HTML5 + Web Audio API によるオーディオデータプロセッシング 「音」の理論から生成、分析、オリジナル電子楽器の開発まで

この本、買った時になんとなく軽く斜め読みだけしてその後はずっと放っていたんですが、やっぱりちゃんとWebAudioやりたいってことで、このアドベントカレンダーに乗じて軽くいじってみました。
テーマは、とりあえずみんな大好きスーパーマリオのテーマソングをWebAudioを使って流してみようかと思います。

ちなみに先にゲロっておくと、最終的に音を出すところまではいきましたが、時間がなくていかんせん全部の音の再生ができていません。
まぁ、のんびりやろう。

ファミコンの音

ファミコンの音って何が出せるの?ってことで、調べてみました。
参考にしたのはニコニコ大百科の記事なんですが、誰が見るんだってぐらいめちゃくちゃ詳しい。

音源

ディスクシステムのやつとか拡張音源とかいろいろあるんですが、マリオはファミコン本体がもつ音源を使っているので、その音源について。

出せる音は次の4種類。
* 矩形波のオシレータが2つ
* 三角波のオシレータが1つ
* ノイズ音源が1つ
* 録音した音を圧縮したやつ(DPCM)の再生で1つ

ファミコンは上記合計4つのチャンネルがあります。
同時に4つの音を出せるわけですね。
このうち、マリオのテーマソングは、音の感じからおそらく次のようになっているかと思われます。

  • メインメロディ - 矩形波
  • ベース - 矩形波
  • ドラム - ノイズ

ここら辺の音を、WebAudioAPIを使って出せば良いわけです。

WebAudioAPI

OscillatorNode

WebAudioAPIを使って簡単な音を作るには、OscillatorNodeを使います。
OscillatorNodeを使えば、指定した波形、指定した周波数を、指定したタイミングで音を出すことができます。

使い方は次の通りです。

var audioCtx = new AudioContext();
var osc = audioCtx.createOscillator();  // オシレータノード作成
osc.connect(audioCtx.destination);      // AudioContextに接続
osc.type = 'sine';                      // 波の種類のタイプ(例はサインカーブ)
osc.frequency.value = 440;              // 周波数(Hz)
osc.start(audioCtx.currentTime);        // 出力開始
osc.stop(0.1);                          // 0.1秒鳴らして止める

ね?簡単でしょう?

GainNode

何も考えずにオシレーターで音を出すと、最大音量でうるさい上に音が割れたりするので、GainNodeを使って音量を調整します。

使いかたは次の通りです。

var audioCtx = new AudioContext();
var gainNode = audioCtx.createGain();   // ゲインノード作成
gainNode.gain.value = 0.2;              // 音量(0〜1)

var osc = audioCtx.createOscillator();  // オシレータ作成
osc.connect(gainNode);                  // オシレータとgainNodeを接続
gainNode.connect(audioCtx.destination); // 出力先をgainNodeに接続

イメージとしては、オシレータから出た音がゲイン調整のフィルターを通って出力されるイメージです。
(なので出力先であるaudioCtx.destinationをgainNodeに接続する)

ね?簡単でしょう?

ScriptProcesserNode

さて、ここまででマリオのテーマの構成音のうち、メロディとベースラインはできそうです。
問題はドラムのノイズ音です。

WebAudioではノイズ生成機がありません。
なのでどうするかというと、ScriptProcessorNodeというのを使って、自前で時刻ごとの波形を指定してノイズを作ります。

ScriptProcessorNodeは、各時刻ごとの振幅を指定することで音を作るNodeです。
振幅をランダムに設定することでノイズを生成するわけですね。

これは以下のようにすることで作れます。

var node = context.createScriptProcessor(1024, 1, 1); //バッファーとチャンネルの指定
node.onaudioprocess = function (e) {
  var output = e.outputBuffer.getChannelData(0);
  for (var i = 0; i < output.length; i++) {
    output[i] = Math.random();                        //各時刻ごとの振幅(ノイズなのでランダム)
  }
};
node.connect(context.destination);

ね?簡単でしょう?
と思ったらそうじゃなかった。

なんかニコニコ大百科の記事を読むと、ノイズ周波数という言葉が出てきます。
ノイズなのに周波数とはいかに。

どうやらファミコンはノイズ周波数というものを指定することで、特定の周波数に偏ったノイズ?を出すようになっているみたいです。

確かにマリオのテーマのドラムを聞くと、2種類の音が使われていますね。
これどうやって生成するんだ。

ちょっとこれ、時間がなくて調べきれていません。
時間ができたら改めて調べてみます。

メロディラインの通りに音を流す

さて、とりあえずここまでの知識でだいたいのメロディラインは作れそうです。
今回は時間がなかったので、いったんメインメロディだけ流してみることにします。
音の周波数やタイミングは、このサイトを参考にしました。

var marioScore = [
    {frequency:660,length:100,delay:150},
    {frequency:660,length:100,delay:300},
    {frequency:660,length:100,delay:300},
    // 以下略
];

window.AudioContext = window.webkitAudioContext||window.AudioContext;
var audioCtx = new AudioContext();
var time;

function _createSound(f, type, playingDuration, stoppedDuration){
    var gainNode = audioCtx.createGain();
    gainNode.gain.value = 0.2;

    var osc = audioCtx.createOscillator();
    osc.connect(gainNode);
    gainNode.connect(audioCtx.destination);

    osc.type = type;
    osc.frequency.value = f;
    osc.start(time);
    time += playingDuration / 1000;
    osc.stop(time);
    time += stoppedDuration / 1000;
}

function play() {
    time = audioCtx.currentTime;
    for (var i=0; i < marioScore.length; i++){
        _createSound(marioScore[i].frequency, 'square', marioScore[i].length, marioScore[i].delay);
    }
}

実際のデモはこちら

一応それっぽくなってますね。
なんかたまにリズムがずれたり音程がずれている気がするけどきっと気のせい。

おわりに

とりあえずWebAudioを触ってみましたが、そんなに複雑なこともなく、割と気軽にさわれそうな感じがします。
どちらかというと、音楽的センスとかが問われそうな感じ。

最初に紹介した本は、WebAudioの各APIのリファレンス以外に、他にも音の解析方法とか、シンセの作成とか色々な情報が載っているので、WebAudioに興味のある方はぜひ買っておくと良いと思います。