読者です 読者をやめる 読者になる 読者になる

音の鳴るブログ

鳴らないこともある

Web Audio API用のMMLイベントシーケンサー wamml です

2014.08.20 名前を変更しました。 wamml => MMLEmitter

Web Audio API用のMMLイベントシーケンサーを作りました。

概要

音楽プログラムを大雑把に説明すると

  • 楽器 (音色)
  • 譜面 (音程とタイミング)

の二つの要素を解決するプログラムと言えます。MMLEmitter はこの 譜面 の部分のみを解決するライブラリです。AudioContext と MML を引数にシーケンサーを生成して起動すると、MML に記述したタイミングでイベントが発火するので、そこで音を出す処理を行います。Web Audio API 依存なので今のところ(サポートされないかぎり) IE では動作しません。あ、MML というのは楽譜を文字列で表現するための記法です。

Web Audio API で音を出す

まずは手始めに Web Audio API を使って、ボタンを押したら 1秒間 880Hz の三角波を鳴らすウェブ楽器を作ってみます。

var audiotContext = new AudioContext();

$("#button").on("click", function() {
  var osc  = audioContext.createOscillator(); // 音を出す部品を作る
  var amp  = audioContext.createGain();       // 音量を制御する部品を作る
  var when = audioContext.currentTime;        // 今の時間

  // 音を出す部品に周波数 880Hz を設定する
  osc.frequency.value = 880;
  // 波形を三角波にする
  osc.type = "triangle";
  // 音量の制御, 1秒で減衰させる
  amp.gain.setValueAtTime(0.25, when);
  amp.gain.linearRampToValueAtTime(0.0, when + 1.0);

  // 音を出す部品を起動させる (鍵盤を押すみたいなイメージ)
  osc.start(when);

  // こういう感じで各部品を接続する
  // osc(tri, 880Hz) -> amp(decay, 1sec) -> Web Audio API output
  osc.connect(amp)
  amp.connect(audioContext.destination);

  setTimeout(function() {
    // 終わったら接続を解除する
    amp.disconnect();
  }, 1000);
});

Web Audio API は結構低レベルなインターフェースしかなくて、それゆえに組み合わせれば可能性無限大ッ!!みたいな感じなんだけど、こういう感じでやっと楽器の部分を作ったあと、さらに譜面をどうするかを考えないといけないのが大変で、僕の場合は面倒になって setInterval と Math.random で適当に音を鳴らし続けるみたいなものしか書けませんでした。

Web Audio API の詳しい使いかたはここをざっと読めば分かるようになります。

MMLEmitter で演奏する

つぎに MMLEmitter を使ってボタンを押したらちょっとしたフレーズを演奏させてみます。

音を生成する部分はさっきのコードの流用です。ほとんど追記することなく、音を鳴らすだけのレベルから演奏といえるまでレベルアップします。このライブラリで解決するのはタイミング制御の部分だけなので、音を生成する部分は頑張って書かないといけませんが、そのぶん汎用性があります。

var audioContext = new AudioContext();

var mml= new MMLEmitter(audioContext, "t100 l8 cege [>eg<c]2");

mml.tracks[0].on("note", noteEventHandler); // 発音のタイミングで呼ばれる

$("#button").on("click", function() {
  mml.start();
});


/**
  * ここで Web Audio API を駆使して音を出す
  *
  * @param {object} event
  *   @param {float}    event.when      : 発音されるべき時間 (タイミング)
  *   @param {int}      event.midi      : MIDIノート番号
  *   @param {float}    event.duration  : 鳴っている時間
  *   @param {function} event.noteOff   : duration 経過後に呼ぶコールバックを設定するための関数
  *   @param {int}      event.chordIndex: 和音の場合のインデックス
  */
function noteEventHandler(e) {
  var osc  = audioContext.createOscillator(); // 音を出す部品を作る
  var amp  = audioContext.createGain();       // 音量を制御する部品を作る
  var when = e.when;  

  // MIDIノート番号 -> 周波数 変換
  osc.frequency.value = 440 * Math.pow(2, (e.midi - 69) * 1 / 12);
  // 波形を三角波にする
  osc.type = "triangle";
  // 音量の制御, duration で減衰させる
  amp.gain.setValueAtTime(0.25, when);
  amp.gain.linearRampToValueAtTime(0.0, when + e.duration);

  // 音を出す部品を起動させる (鍵盤を押すみたいなイメージ)
  osc.start(when);

  // こういう感じで各部品を接続する
  // osc(tri) -> amp(decay) -> Web Audio API output
  osc.connect(amp)
  amp.connect(audioContext.destination);

  e.noteOff(function() {
    // 終わったら接続を解除する
    amp.disconnect();
  }, 0.1); // duration 経過 + 0.1秒で実行される
}

アニメーションへの応用

note イベントで必ず音を出す必要はなくて、アニメーションの制御など、あらゆるタイミング制御に適用できます。その際の各フレームの単位は 11.61 (512 / 44100) msec 程度になります。

「あらゆるタイミング制御に適用できる」と書いたところで思ったけど、それならば Web Audio API への依存をなくすのも良いかも知れない。タイマーとしてしか使ってないし。

予定の機能

最近はやりの directive を導入したい。外部から動的に MML を操作したり、内部から外部へのトリガに使えると利用範囲はかなり広がるはず。一応構想としては vue.js みたいなやり方で、あと固有の機能として $ で始まる識別子は全トラックで共有されるみたいなのを考えている。途中まで作ったのだけど、どうなっていると最高に便利なのか検討が足りていない気がして最初のバージョンからは削除した。MML + directive で何かひらめいた人には助言をおねがいしたいです。

あと、MMLを拡張している部分のシンタックスがちょっと変わるかも知れないです。

おまけだょ

高校生のときに作った曲を思い出しながらMML化したのでおまけに貼っておきます。このページに貼り付けると聞けます。

t180 l8 q7 o5 ab<cd
  e2.d4 g4f4e4f4 d2.e8c8^2 > ab<cd
  e2.d4 g4f4e4f4 d2.e8e8^2 > ab<cd
  e2.d4 g4f4e4f4 d2.e8c8^2 > ab<cd >
  a2 ab<cd > a2 ab<cd> a2 r2 r1 <;
t180 l1 q8 o4 r2
  [fa<ce] [fgb<d] [egb<d] [ea<ce]
  [fa<ce] [fgb<d] [egb<d]2 [g+b<e]2 [a<cd]2 [ea<c]2
  [fa<ce] [fgb<d] [egb<d] [ea<ce]
  [fa<c] [gb<d] l8< r[ce][>b<d]r [ce][>b<d]r[ce] r[ce][>b<d]r [ce]>eee; 
t180 l8 q6 o3 r2
  f4r<f4rc4> ffbb<dd>gg e4r<e4r>b4  aa<ccee>aa
  f4r<f4rc4> ffbb<dd>gg e4r<e4r>g+4 aagagab<c>
  f4r<f4rc4> ffbb<dd>gg e4r<e4r>b4  aa<ccee>aa
  f4f4afff g4g4bggg eaeeaeea4 aaeaeee;