MML (Music Macro Language)
MMLは、音楽記法をテキストで簡易的に扱うための記述言語です。例えば、「ド・レ・ミ」と四分音符で歌う場合には「C4 D4 E4」と書く事で表現できます。
経緯
先日、大晦日ハッカソン2014なるものを見つけ、mrubyをBlackfinボード(UCB-BF512-B)で動かすおひとりハッカソンを企画し実行したわけですが、実は色々な小道具を整備しないとmrubyに辿りつけないとわかり何故か寄り道。Windows、Mac OS、Linuxで動作可能なChaNさんのFM音源モジュールの簡易シミュレーターの流れもあって、MMLパーサーがあれば自由に自動演奏させられるなぁという考えから実装したい衝動に負けてしまいました。'A tiny MML parser'
'A tiny MML parser'は、Cで記述されたMMLパーサーです。特徴をざっと挙げると
- シンプルなAPI
- 可搬性に優れたソースコード (C89準拠)
- 外部ライブラリに非依存 (libcも不要)
- 小さなフットプリント
- シャープ、フラット、ダブル・シャープ、ダブル・フラットなどにも正しく対応可能
- 複数の符点音符にも正しく対応可能
- 三連符に対応
など、マイコンで使う事も考慮した設計と実装になっています。
特に、シャープやフラットが複数付く場合、符点が複数付く場合、三連符に対応可能な点は、実際の曲を簡易的に表現可能という意味で重宝する仕様になっています。三連符を疑似的に入力するのとか辛いじゃないですか・・・。
どういう風に使えるの?
'A tiny MML parser'を使うには、要求されている唯一のコールバック関数を実装するだけです。このコールバック関数は、「typedef void (*MML_CALLBACK)(MML_INFO *p, void *extobj);」と宣言されており、mml_fetch関数を呼ぶ度に一つのMMLコマンドが解釈されてコールバックが呼ばれる仕組みです。
ライブラリのインターフェースは以下に示すようにたったの三つです。
まず、初期化関数mml_initでコールバック関数を渡して初期化します。
この時、extobjに外部オブジェクトへのポインタを渡してやると、コールバック関数で受け取れます。
次に設定関数mml_setupで演奏時に必要となるオプションとMMLを渡します。
「オプションなんてわけわからん!」という人は「(MML_OPTION *)0」を渡して下さい。
最後に呼び出し関数mml_fetchでMMLのパースを実行します。
このmml_fetchを呼び出す度に、一つのMMLコマンドが解釈されます。
ちなみに、このライブラリは「何が書いてあるのか?」について解釈する機能を提供します。
「それをどうするのか?」については一切関知しません。
実際のシステムに導入する場合、これを意識して使う事が結構ポイントになってきます。
デモ
Blackfin上でFM音源を構成し、そのFM音源をMMLで操作している様子をデモ映像にしました。
このデモでは、ChaNさんのFM音源のコードを若干改造したものをミドルウェアとして使用しています。MMLパーサーも軽量ミドルウェアとして導入できたので、上位の実装は非常にシンプルな形で済ます事ができました。
ライセンス
ライセンスはMITです。
趣味でも業務でも、ライセンスの範囲で自由にお使い頂けます。
ダウンロード
'A tiny MML parser'のダウンロードは以下のページからどうぞ。
公開されているA tiny MML parserを自作アプリに入れ込もうとソースを改変しているときに、バグらしき部分を発見しましたので報告します。
返信削除以下のソース部分で、#if で記述している部分が該当部です。
共に、オリジナルではconvert_note_length_to_ticks()で長さを内部値に変換しようとしていますが、ここはすでに内部値に変換されているので不要です。また、この処理を通してしまうと該当するエラーになります。
変換をかけるとどうしても途中で変換が止まるので追跡していて発見しました。
一度ご確認ください。
・・・↓ここから・・・
static MML_RESULT get_note_ticks(MML *handle, char *text, int *ticks)
{
int note_length = LIBC_ATOI(text);
char *p;
int val;
MML_RESULT mr;
mr = convert_note_length_to_ticks(handle, note_length, &val);
if (mr != MML_RESULT_OK) {
return mr;
}
*ticks = val;
p = &text[0];
while (*p) {
if (*p == '.') {
#if 1
val/=2;
#else
note_length *= 2;
mr = convert_note_length_to_ticks(handle, note_length, &val);
if (mr != MML_RESULT_OK) {
return mr;
}
#endif
*ticks += val;
}
p++;
}
return MML_RESULT_OK;
}
static MML_RESULT get_note_ticks_default(MML *handle, int *ticks)
{
int val;
#if 1
MML_RESULT mr=MML_RESULT_OK;
val=handle->option.length;
#else
MML_RESULT mr;
mr = convert_note_length_to_ticks(handle, handle->option.length, &val);
#endif
*ticks = val;
return mr;
}
・・・↑ここまで・・・
これを公開していただいたおかげで開発がだいぶ楽になります(まだ音出し部の開発をしなければならないので先は長いのですが)。
ありがとうございます。
すみません、昨日バグを報告したものです。修正が間違ってました。
返信削除.lengthではなく.bticksです。
・・・↓以下修正部分・・・
static MML_RESULT get_note_ticks_default(MML *handle, int *ticks)
{
int val;
#if 1
MML_RESULT mr=MML_RESULT_OK;
val=handle->option.bticks; // .bticksはすでに内部値に変換済みなのでそのまま返せば良い
#else // オリジナルのバグ
MML_RESULT mr;
mr = convert_note_length_to_ticks(handle, handle->option.length, &val);
#endif
*ticks = val;
return mr;
}
・・・↑以上修正部分・・・
先日バグと言って報告したものです。
返信削除なんか修正方法が間違っているようです。
忘れてください。
申し訳ありませんでした。
LeDAさん:どのような内容であれフィードバックを頂けるのは大変ありがたいことです。ありがとうございます。ところで、本件はA tiny MML parserのバグでは無く、別の問題だったという認識で良いでしょうか?
返信削除返答有難うございます。
返信削除1つの問題は、テンポの調整をbticksの調整で行うか、外部の割り込み周期の変更で行うかで、最初は割り込みの周期を変更して調整していましたが(そう考えてました)、よくよく考えると「実機で割り込み周期をテンポごとに変更するのは問題があるのでは?」と思い至り、そのつもりでソースを読み直すとbticksの調整でテンポができるようになっていたので「問題ない」とわかった次第です。私の理解力不足です。
テンポ設定関数(bticksへの設定関数)があればわかりやすかったと思います。
ちなみに、その後このparserに大幅にMMLコマンドを追加してX68000にほぼ互換したものを作りました。音色変更、D.C/D.S/CODA/FINEなどの音楽記号、繰り返し回数に応じた演奏場所指定、完全な連符処理、和音、他処理と同期のためのコメント埋め込みなどを追加しました。
インタープリターのままでは時間的に処理しきれなくなったので中間コードを作るコンパイラーとインタープリターに分離しました。ソースが汚すぎて公開できるようなものではないですが。
少し誤解されているようなので補足しますね。
削除これは設計上非常に重要な点ですが、bticksはビートあたりの分解能を示す値でテンポとは無関係です。このあたりはVersion 0.5.0のパッケージをダウンロード頂き、arduino_note_exampleのnote.cppを読んで頂くと御理解頂けるかもしれません。
このnoteモジュールは、1分あたりのビート数(bpm)とビートあたりの分解能(bticks)を初期化時に与えて使用するモジュールです。このモジュールは、MMLの論理概念しか扱わないA tiny MML parserの世界から、実際のデバイスの世界(Arduino)へのブリッジを構成しています。
note.cppの内部関数get_note_lengthでは、発音対象ノートのティック数、1分あたりのビート数(bpm)、1ビートあたりの分解能(bticks)の三つのパラメータから実際に発音すべき時間数を算出しています。つまり、ここで初めて実際の世界とも言える時間軸に算出されます。
MMLのテンポコマンドを与えるとA tiny MML parserはコールバック関数でその事をユーザー・アプリケーション層に伝達しますが、それをどのように扱うのかはシステム依存になっているという事です。当該サンプルでは、arduino_note_example.inoに記述されたコールバック関数内部でnoteモジュールを再初期化しているところが見つかります。
追加した機能群はとても魅力的ですね!
削除是非公開して頂けると嬉しいです!
bticksの件、お教えいただいてありがとうございます。
返信削除ところがと言うのはなんですが、bticksでテンポを設定する、ということでとりあえず組み上げてしまったので、当面はそれで行こうかと思います。次回バージョンで見直したいと思います。
今は、tempo120でL4が0.5秒で480ticks、なので1bticksは1/960秒という感じに実時間変換しています。テンポに係るのは音符と休符だけなので、発声開始から消音までがこのタイミングで処理され、ほかは即時処理、という感じです。
実装したのはiOS上で、その上でMMLを書いたら演奏できるというものを作りました(すでに公開しているアプリへの機能追加です)。MMLの処理はこちらのMML parserをいただけたので比較的楽に出来たのですが(多謝です)、これがまたiOSの音源というか発声のためのシステムコールが、サンプリングした長い音楽を演奏するためのものであり、MMLからの発声=短い音を細切れに発声させるのに全く向いてないので、どえらく苦労しました。リアルタイム発声ができないのです。
必要な処理を音源側に持つかMML側に処理させるかの処理負担も大きく変更せざるを得なくて、特に「発声開始」と「消音」については何度もMML側〜音源側で移動してようやく音源側で落ち着いた、などということもありました。
取説はすでに書いて、ひっそり公開済みです(http://www.asahi-net.or.jp/~YY8A-IMI/ipad/xb/music.html)。よろしければご覧ください。
ソースの公開については、折を見て行えればと思います。