音の鳴るブログ

鳴らないこともある

MMLトークン解析技法

便利なのでMMLを使うことが多くて、そのたびにMMLシーケンサーを書いている。

おおまかな流れとして

  • MML文字列をトークン配列化する
  • 再生している音が終ったら次のコマンドを参照して実行
  • 終るまで

のような感じなんだけど、良い書き方が分からない。たとえば c&d のようなポルタメントの処理 (ドの音からなめらかにレに遷移する) だと、c のコマンドの時点で &d が続くことを知っていないといけないとかあって、そのあたりを毎回試行錯誤して書き直している。良い方法あるのだろうけど..

それはそうと、MMLのコマンド切り出しに関しては洗練されてきた感があって、以下のようなコードを使っている。優先順に正規表現とオブジェクト化する定義を書くだけで新しいコマンドを追加できるのでかなり楽。

compile = (mml, MMLCommands)->
    commands = []
    checked = new Array(mml.length)
    for def in MMLCommands
        re = def.re
        while m = re.exec mml
            if not checked[m.index]
                for i in [0...m[0].length]
                    checked[m.index+i] = true
                cmd = def.func m
                cmd.index = m.index
                cmd.origin = m[0]
                commands.push cmd
            while re.lastIndex < mml.length
                if not checked[re.lastIndex]
                    break
                re.lastIndex++
    commands.sort (a, b)-> a.index - b.index
    commands
    
MMLCommands = [
    { re:/@w(\d*)/g, func:(m)->
        name:'@w', val:m[1]|0
    }
    { re:/@n(\d*)/g, func:(m)->
        name:'@n', val:m[1]|0
    }
    { re:/@(\d*)/g, func:(m)->
        name:'@', val:m[1]|0
    }
    { re:/t(\d*)/g, func:(m)->
        name:'t', val:m[1]|0
    }
    { re:/l(\d*)/g, func:(m)->
        name:'l', val:m[1]|0
    }
    { re:/v(\d*)/g, func:(m)->
        name:'v', val:m[1]|0
    }
    { re:/o(\d*)/g, func:(m)->
        name:'o', val:m[1]|0
    }
    { re:/[<>]/g, func:(m)->
        name:m[0]
    }
    { re:/([cdefgab])([-+]?)(\d*)(\.*)/g, func:(m)->
        name:'note', len:m[3]|0, dot:m[4].length,
        tone:{c:0,d:2,e:4,f:5,g:7,a:9,b:11}[m[1]] + ({'-':-1,'+':+1}[m[2]]|0)
    }
    { re:/r(\d*)(\.*)/g, func:(m)->
        name:'rest', len:m[1]|0, dot:m[2].length
    }
]
# これが
"t200l8 @6 @w60 v15o4 efga4.>a4 <c+dee4ede"

# こんな感じになる (実際は文字列じゃなくてオブジェクト)
[ 't200', 'l8', '@6', '@w60', 'v15', 'o4', 'e', 'f', 'g', 'a4.', '>', 'a4', '<', 'c+', 'd', 'e', 'e4', 'e', 'd', 'e' ]