簡易MMLプレーヤー

  • 投稿日:
  • by
  • カテゴリ:

javax.sound.midiを使うと割と簡単に音階を鳴らせることがわかったので、簡単なMML(Music Macro Language)プレーヤーを作ってみた。BASICのPLAY文でお馴染みの、ドレミファソラシドがCDEFGABCで表されるあれだ。

Simple MML Playerの起動ページへ
ソースコード

とりあえず、CDEFGとでも入れてPlayを押すと、何か鳴ると思う。

以下の文字をサポートしている。(大文字と小文字の区別は無し、[]は省略可、xは数字を表す)
・C,D,E,F,G,A,B: ドレミファソラシド
 形式:(C,D,E,F,G,A,Bのどれか)[+/-][x][.][&]
 +/-は#/♭、xは長さ(全音符の何分の1か)、"."は符点(長さ1.5倍指定)、&はタイ/スラー
 例)C4 --- 四分音符のド、D+2. --- 符点二分音符の#レ
  B2&B8 --- 二分音符+八分音符の長さのシ
・R: 休符
 形式:R[x][.]
 xは長さ(全休符の何分の1か)、"."は符点
・O[x]: オクターブ(デフォルトは4)
・L[x]: 長さ省略時の音符/休符の長さ(1-64、デフォルトは4)
・V[x]: 音の大きさ(0-100、デフォルトはチャンネル0が100、チャンネル1が75)
・T[x]: テンポ、1分当たりの4分音符の数(デフォルトは120)
・@[x]: 音色(0-127、デフォルトは0(グランドピアノ))(参考資料
・<,>: オクターブを1つ下がる/上がる
・(): 和音
 形式:([C/D/E/F/G/A/B/+/-/>/<]の羅列)[x][.][&]
 例)(CEG)4 --- 四分音符の長さでドミソを同時に鳴らす
  (A>C+E) --- ラ、1つ上の#ド、ミを同時に鳴らす
・","(カンマ): チャンネル区切り(チャンネルは2つのみ)
 例)C8D8E8D8C4,E4F4G4 --- チャンネル0でドレミレド、チャンネル1でミファソ

もちろん独自仕様である。いわゆる作者 is Godである。

1行の中に","で区切って2つのMMLを書くと、それぞれが別のチャンネルで同時に鳴る。
同じ行の2つのMMLは必ず同時に始まる。一方が短い場合、長い方が終わるまで次の行が演奏されない。

エラー処理は極めて曖昧である。というか、ほとんど何もしていない。不正な文字列を入力や30和音などの無茶な指定をした場合にきちんと鳴るかどうかは無保証である。

ブラウザ上でなく、ローカルのファイルシステムから実行すると、演奏する前にMIDIデータ(SMF形式)がsimple_mml.midというファイルに保存される。

スライドバーの音量設定は、次の演奏から有効になる。


以下は、javax.sound.midiに関する覚え書きである。
・概略
SequenceにTrackを作って、Track#add()で時系列のMidiEventを登録して、Sequencerに渡してSequencer#start()を呼ぶとMIDIシーケンスが演奏される。
MidiEventは、ShortMessageのNOTE_ONがある音階の鳴り始め、NOTE_OFFが鳴り終わりである。ピアノの1つの鍵を押すのと離すのに相当する。音階は0〜127の整数で、60が基準のド、61がド#、62がレ…である。

・テンポについて
JDK 1.6のドキュメント(API spec, jdk-6-docs/technotes/guides/sound/)にもMIDI 1.0の仕様書(MIDI 1.0 Spec, SMF spec)にも見つけられなかったので正しいやり方かどうかわからないが、筆者のSun JRE1.6の環境では、javax.sound.midiでもmeta eventのFF 51で四分音符の長さを設定できるようである(デファクトスタンダードの類?)。MetaMessageで登録する場合は、type=0x51とし、 dataはbig endianの3バイトで、四分音符の長さをμ秒で指定する。
このメタイベントを使う場合は、Sequenceを作る時のdivisionTypeをPPQとする。
このメタイベントを使う以外の方法としては、divisionTypeをSMPTE_30、resolutionを100とかにしてtickの長さを絶対時間にして(この引数だと1 tickが1/3000秒になる)テンポに応じて音の長さをtickの整数倍で指定していく方法しか思いつかなかった。この方法だと誤差が大きいし、tickの値がすぐ大きくなるのでMIDIシーケンスのデータが長くなってしまうので、好ましくない。

・演奏停止について
Sequencer#stop()を呼んでも、既に鳴り始めた音は鳴り止まないことがある。確実に音を止めるために、Sequencer.close()を呼ぶようにした。
また、シーケンスが終了しても最後の音が鳴り続けることがあるので、シーケンスの最後にメタイベントのFF 2F(トラック終端)を付けて、それをMetaEventListenerで受けて、音を止めるようにした。


やはり何でもテキストエディタで編集できる方が良い。