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

ごちゃログぴこっ

はなまるデジタル創作紀行(DTM、TAS、いろいろな技術)

MIDI(SMF)→MML変換ツール「PetiteMM」の紹介・アルゴリズム

このたび、ごちゃと loveemu さんで PetiteMM という変換ツールを発表しました。開発着手時に「既に優れたツールがあれば開発する必要はないのでは?」と思い各種MIDI2MMLプログラムを比較してみたものの、納得のいくものが見つかりませんでした。

PetiteMM と類似プログラムの性能は比較記事で紹介していますが、PetiteMM は譜面の簡素化と演奏精度の保持を両方がんばっちゃおうという欲張りコンバータです。

インストール・使用方法

PetiteMM からアーカイブファイルをダウンロードして適当なフォルダに展開します。

PetiteMM.bat に MIDI ファイルをドラッグアンドドロップすることで変換が可能です。「'java' は内部コマンドまたは外部コマンド~として認識されていません」と表示される場合、Java のインストールとパス設定が行われていない状態なので、無料Javaソフトウェアをダウンロードしてください。

コマンドオプション

より高度な変換を行う場合、オプション指定を伴うと効果的です。オプションの一覧はバッチファイルをダブルクリックして起動すると表示されます。

PetiteMM.bat の動作時にオプションを有効にしたい場合、PetiteMM.bat をテキストエディタで開き、最初の方にある PMMOPTS 変数にオプションを記述します。

@set PMMOPTS="--dots 2 --no-quantize"

現時点で利用可能なオプションは下記の通りです。

オプション 引数 説明
-o <filename> 出力ファイル名の指定(バッチファイルからは使用しない)
--dots <count> 付点の最大数制限、-1で制限なし(デフォルトは-1)
--timebase <TPQN> MMLの分解能、0でMIDIファイルに合わせる(デフォルトは48)
--input-timebase <TPQN> MIDIファイルの正しい分解能、0なら入力どおり(デフォルトは0、nsf2midiなど特殊用途向け)
--quantize-precision <length> クオンタイズ時の最小ノート長(2のn乗で指定)*1
--no-quantize クオンタイズによるノート長の調整を行わない。タイミングが正確になる代わりに複雑な音符が多くなる。
--octave-reverse オクターブ記号を大小反転する
--use-triplet 三連符らしき箇所を三連符記法に置換する(賢くなく、実用性に乏しい)*2

使い方は以上です。以降は PetiteMM の技術的アプローチに関する説明です。知らなくても特に問題ありませんが、中身を知っているとお望みの変換がしやすくなるかもしれません。

開発動機

既存のツールの出力に満足できなかったので開発しました。

わたしが時々使用していたのは tinymm という簡素なツールだったのですが、連符や細かいタイミングに対応していないため精度は低く、ソースコード非公開のため修正もできませんでした。別のツールを探していくつも試してみたのですが、同じような問題を抱えていたり、複雑なMMLが出力されたりして、満足感が得られませんでした。

  • タイミングのずれを最大限起こさないよう変換する
  • 演奏精度への影響を最小にしつつ、ノートを簡素化して出力する
  • MMLの内部仕様や方言に極力依存せず、幅広く利用できる
  • ソースコードが公開されており、誰でも改変できる

これらを満たしたツールを新たに創造しようとして作られたのが PetiteMM です。

PetiteMM の変換フロー

処理の詳細は後述しますが、以下が変換の主な流れです。

  1. プリプロセス・初期化
    1. MIDI: 1チャンネル/1トラックに分離 (主にフォーマット0への対応)
    2. MIDI: 分解能(TPQN)*3を変換向けの精度に変更し、デルタタイムを調整
    3. MIDI: ノートの位置と長さ、拍子情報(改行用)を収集してリスト化
    4. M ticks → N分音符変換テーブル(音長変換テーブル)を初期化
  2. 変換(ノート認識、クオンタイズ、拍子ごとに改行など)

MML変換の難しさ

MIDIMMLが難しい理由はいろいろありますが、理由の一つとして、解が一通りではないことが問題です。

q50 c4 d4
q25 c8 d8
q100
c8r8 d8r8
c16.r32 d16.r32

上記の2つは同じ演奏情報を異なる形で表現しています。上記の例は単純なのでどちらを正解としても良いように思えますが、現実のMIDIファイルには綺麗に割り切れない長さが多数登場するため、下の例のように q100 を前提として愚直に変換すると複雑な音符が出力されてしまいます。それゆえ、音符の長さや q の値を適当に調整しなければならないのですが、果たしてどう調整すれば良いかが新たな問題となってきます。本書では、この譜面簡略化のための処理をクオンタイズ(処理)と呼ぶことにします。

どのような結果を良しとするかは、目的や感性にも依存します。簡潔な譜面を求めていれば細かすぎる休符は邪魔になるでしょうし、逆に演奏精度を求めているのにノートが大幅に簡略化されてしまっては困ります。連符が使えないシステム向けに連符を綺麗に変換しても役に立ちません。そのため、良質なクオンタイズのためには簡略化の程度を示すパラメータを設けることが不可欠であると考えました。PetiteMM のオプションは決して万能ではありませんが、これらの要求への柔軟性を与えるものとなっています。

その他、下記のような問題が変換処理のハードルを上げているのではないでしょうか。

  • MIDI ファイルの構造(1トラックに和音や複数チャンネルを持てる点など)
  • MML の方言の多さ(クオンタイズqやタイなどの基本要素ですら相違がある)
  • M ticks → N部音符 への変換、付点・連符への対応方法(これらも解が複数存在する)

これらの諸問題に対する解法を一切知らなかったので、わたしは石油王を目指す間も惜しんで対応アプローチを自ら考えました。

MIDIMMLロスレス変換はじつは難しくない?

難しいのか難しくないのかはっきりして欲しいところですが、難しくないのです。MIDIファイルを必ず 1 tick のずれもなく変換することは常に可能です(tick表記は使用しない)。

任意の M ticks が必ず N分音符 の組み合わせで表現できることを証明します。

  1. 分解能(TPQN)が T のとき、4分音符の長さは T ticks である … (I)
  2. M分音符をN等分したノートは (M×N)分音符 である(例:2分音符を4分割 → 8分音符) … (II)
  3. (I) (II) より、1 tick は (4×T)分音符 である … (III)
  4. ノート同士はタイで任意の数だけ結合できる … (IV)
  5. (III) (IV) より、任意の M ticks は (4×T)分音符 をタイでM個結合すれば表現できる

サーモン! MIDIファイルを一切のずれなく変換するのは簡単なことだったのです! たとえ連符があっても関係ありません!

分解能調整の必要性

現実には証明通りにMIDIファイルを処理するのは問題があります。例えば分解能960のファイルを読み込むと、最小で c960 というノートに変換されますが、これほど細かい音符を扱えるMMLは稀なはずです。なぜ扱えないかと言えば、個々のMML音楽再生エンジンにも内部的な分解能があり、最小の音符の長さが決められているからです*4

この問題を解決するのは簡単で、MIDIファイルの分解能がMMLの分解能に一致するように前処理を行えば良いです。分解能の変更はtick単位で実施する処理なので、必要最低限しか精度が落ちることはありません。これで短すぎる未対応音符の存在は消えます。

副次的な作用として、MMLの分解能を適当な因数で割ったより小さな分解能にすれば、出現するノートの種類をさらに制限することができます。分解能を8にすれば、32分音符より細かい音符が出現することはありません。

音長変換テーブルによる長さ変換

短い音符をタイで結合すれば良いと述べましたが、4分音符の表現するのに c96^c96^c96… などとタイを書き連ねたりしたら、ユーザーはわたしの家にくさや爆弾を仕掛けるかもしれません。

M ticks → N部音符の変換方法は一通りではありません。c4. とも c4^c8 とも c8^c4 とも書けます。ノート同士をタイで結合しないと表現できない長さもあります。それらを単純な計算式で求めるのはおそらく困難でしょう。

この問題は変換用のルックアップテーブルを用意することで難易度をぐっと下げられます。テーブルを有限長にするためには M の最大値を既定しなければいけませんが、じつはその必要はありません。

  • 単一のノートで表現可能な最大長は全音符×2個分である(全音符に付点を∞個付けた際の長さ)
  • 全音符×2個分以上の長さは、通常 c1^c1^何か で表現されると考えられる
  • 分解能(TPQN)が T のとき、全音符×2個分の長さは T×4×2 ticks である

すなわち、用意するテーブルの長さは T×4×2 ticks 分で良いのです。長い音符については全音符×2個分で割った余りについての長さ表現をテーブルから取得して、頭に必要な数の全音符を付ければ良いのです。グッバイ!くさや!

次なる問題はテーブルの内容と構築方法ですが、tick→音符変換のシンプルな解は次のようなものであると考えられます。

  • タイを使わず単一のノートで表現できる解が最も良い
  • ノート2つの組み合わせはその次に良い、3つはその次に良い、etc.

よって、次の手順でテーブルを構築します。

  1. N=1~T×4 のN分音符とその付点音符で表現可能な要素を埋める*5
  2. テーブル内容がすべて埋まるまで下記のような処理を繰り返す
    1. 単一ノートと単一ノートをタイ(^)で結合して表現可能な要素を埋める
    2. ノート^ノート と単一ノートをタイ(^)で結合して表現可能な要素を埋める
    3. ノート^ノート^ノート と単一ノートをタイ(^)で結合して表現可能な要素を埋める
    4. and so on...

これで M ticks → N分音符 を簡潔な表現にて変換することができます。

音長変換テーブルの内部形式ですが、単純な変換には音程を置換可能な文字列形式であれば問題ありません。しかし、クオンタイズを行おうと思うと、タイでつながれた個々のノートの長さや、付点を分解したときの最小の長さなどを知りたくなってきますので、文字列表現を保持すると同時に、tick数の組み合わせを保持しておくことをおすすめします。

クオンタイズ処理

上述のアプローチで入力に忠実なMIDIMML変換を行うことはできますが、半端な長さのノートが出現すると複雑なノートが出力されてしまいます。前処理で分解能を低くしておけばノートも単純になるのですが、発音開始タイミングも粗くなってしまうのはやや悩ましいところです。

より自然な譜面表現とは、よく紙の楽譜で見るような形だと思います。下記のような特徴があります。

  • 音符や休符は、M分音符の2等分または3等分の長さで表現される(Mは2の冪乗数、5連符などは一般的でない)
  • 演奏上の非常に短い休符などは記さず長い音符として扱い、スタッカートなどの奏法記号で補う

よって、このような処理としました。

  • ノートの発音開始タイミングは変更せず、終了タイミングのみを調整する。
    • 開始タイミングは変更時の感覚的な影響が大きいため保持する。開始・終了の両方を調整したければ入力分解能を変更する。
  • ノートの長さを、M分音符の1/2や1/3などの綺麗な近似値に変更する(Mは2の冪乗数)
  • 非常に短い音符・付点表現を伴う場合は採用せず、より簡素になる近似値を採用する。
    • ただし発音間隔がそもそも短いなど、すべての候補が対象外になる場合は、クオンタイズ精度指定を無視して採用する。

より具体的な処理内容はおよそ下記のとおりです。

  1. 調整前のノートのtick数(理想長:精度面で理想的な長さ)、次のノートまでのtick数(最大長:ノートが取り得る最大長)を取得する。
  2. 長さ情報が全音符を超える場合、全音符で割った余りに変換する(全音符の数は記憶しておき、最後に付加する)
  3. 理想長以上の長さのM分音符(Mは2の冪乗数)のうち、長さが最短となるMを求める。
  4. 理想長をM分音符で割り、長さの割合を求める(整数表現の誤差などがなければ、値の範囲は (0.5,1.0] となる。)
  5. 0.5, 1.0 などの割合一覧から、長さの割合の近似値を選択する
    1. 割合一覧は必ず 0.5, 1.0 を含む
    2. 連符向けに 2.0/3.0 も含む
    3. 付点向けに 0.75, 0.875,... を含む(割り切れる場合に限る、クオンタイズ精度にも依存する)
  6. M分音符×長さの近似値+退避した全音符 を調整後の長さとする(最大長を超える場合は最大長とする)

長い音符ほど相違が大きくなる特徴がありますが、代わりに表現はより簡潔になります。

頭のなかで様々な方法を模索したのち上記の方法に落ち着いたのですが、考えてみればノートの長さを2と3の公倍数の粗い分解能に調整するのと、ほぼ変わらないような気もしてきました。様々なケースが考えられるため実際のところはわかりませんが、クオンタイズに関しては再考の余地があるような気がしてなりません。

上記はMMLのqコマンドの値を一切考慮しない方法ですが「qコマンドの値の変化が少なくなる結果を良しとする」方法もあると思います。探索がより複雑になりそうなので今回は試みませんでしたが、うまく処理できれば結果は期待できるかもしれません。

その他

変換後のMMLは単純な文字列ではなく、コマンド文字列のリストで保持することをおすすめします。最適化や順序変更が行いやすいからです。

あとがき

改めて書きながら読み返してみると、特別に斬新でもなんでもないような気がしてきました。ただ、それでも良い結果が出せている(と思う)のを見ると、この情報もまんざら無駄でもないのではとも思います。

本書が似たようなことに挑戦する人の一助になればうれしく思います。

ほめて!ほめて!おいしいスイーツおごって!オレンジジュース!

*1:あくまでクオンタイズ時の基準を示すもので、--quantize-precision で16分音符を指定しても、発音タイミングが細ければさらに短い休符等が出現することはある。細かい音符を完全に排除したい場合は --timebase で調整を行う。

*2:--use-triplet を使わない場合でも c12 などの三連符相当の音符は出現する。これを防ぎたい場合は --timebase で3の倍数を指定しなければ良い。

*3:TPQN は Ticks Per Quarter Note の略で、4分音符あたりのtick数のこと。

*4:例えばピコカキコでおなじみの FlMML の分解能は 384 です。N分音符までしか扱えないシステムの分解能はN/4になります。

*5:表現可能な要素とは、すなわち長さをtick単位で求めた際の余りが0のノートである。