MIDIプレイヤが出来たら、今度は再生する曲を作るものがほしくなる。というわけで週末からMMLコンパイラの作業を再開した。
こっちは少なからず複雑なプログラムになるので(一般的なソースコードパーサとはだいぶ性質が異なることは以前にぱらぱらと書いた)、とりあえず処理モデルを明確にしてひとつずつ片付けていくことにした。処理モデルはこんな感じになる:
- ソーステキストを解析して、内容を1行ずつトラック、マクロ、その他変数定義などに区別する。
- トラック定義とマクロ定義、変数定義から、トークンのリストを生成する。トークンの意味は多重に解釈されうるので、ここでは複雑な解析を行わない。
- トラック定義、マクロ定義、変数定義に含まれるトークンリストから、命令と式を解析してツリー(もどき)構造にする。命令はマクロ呼び出しまたはプリミティブ命令。マクロ定義自体が、引数を包含するかたちで定義されないので、引数の数はチェックされない。
- 変数定義を展開する。
- マクロを参照している命令ツリーをプリミティブ命令のツリーに展開する。
- 計算を要する式と変数参照を全て展開し、イベントのリストにする。
- イベントリストからSMFを生成する。
で、ツリーもどき構造の生成までとりあえずやっつけたのだけど、その間デフォルト取り込みするMMLを大幅に書き換える必要があった(基本的にほとんどの命令は)。多くは(MML中の)バグ潰しと、文法的な矛盾の解消だ。
一番分かりやすい例を挙げるなら、ツリー構築を実装するまで、MMLの中ではマクロ呼び出しのあとに ( ) がついているものが多々存在していた。CC(0, $val) といったものである。しかし次のような例を考えると、これは全く正しくないことが分かる:
- #macro ( $val:number=4 { __LET("defaultvelocity", $defaultvelocity - $val) } // define macro '(' as "velocity down"
- 1 cde(cde)cde(cde) // track 1
'(' は、一般的な用法に合わせて、音量を小さくするコマンドとして定義されていて、上記の例でもそのような意図で使用されている。マクロの引数を明示するために使われてはならないのだ。(ただ、代入のようなプリミティブな命令では、とりあえず()を要求するかたちにしておいた。)
また、現在の文法では、マクロはいわゆる関数ではなく、従って値を返すものではないのだけど、そのことを忘れてマクロで関数のようなものを定義していた箇所がいくつかあった(たとえば剰余算MODをマクロで定義していた)。これが使えなくなるとけっこう困るので、暫定的に「計算を行う」プリミティブ命令を追加し、それを「適用」する式を作ることで無理矢理対応したりもしている。
そんな感じで、実装する前に設計した文法は、かなり画餅だった部分があって、これからも多々変更されなければならなくなるだろうと思う。マクロをどう展開するかは、まだ闇の中だ。いずれにしろ、MML文法はかなり頭でっかちに考案していたこともあって、実装を書いてみると、いろいろ改善されて、面白いといえば面白い。
Leave a comment