Tracktion/Waveformに変拍子の含まれるMIDIファイルをインポートして加工する

わたしが最近ひいき目に(?)しているDAWとして、Tracktion社のWaveformを使っているのですが、今日はこのTracktionまわりのhackを紹介します。*1

他所で作ったMIDIファイルをTracktionに取り込む時*2、たまにTracktionの挙動が不審で、予期しない謎の音楽が生成されることがあります。最近わたしが経験したものでは、METAイベントのテンポと拍子設定が入り乱れる音楽を取り込むと、テンポがめちゃくちゃになる(しかも説明が困難なかたちで)というものでした。

f:id:atsushieno:20190212155715p:plain
bogus imports of tempo and time signature

MIDIなんて使ってるやつそんなおらんやろ?と思われそうな気もしますが、Tracktionをはじめ大抵のDAWMIDIトラックやらインストゥルメンタルトラックやら呼ばれるものは、そして各種オーディオプラグインは、内部的にMIDIメッセージのやり取りで成り立っているので、この辺の問題のインパクトはそれなりにあります。この問題の場合は、テンポと拍子の扱いが「根本的に何かおかしい」可能性があります。

それはそれでせっかくgithubで公開されているのでissueとして登録したのですが、Tracktionを使った作業自体は継続したいわけです。なのでTracktionにはバグレポートしつつ、Tracktionが正常に動作するようなSMFを作って取り込もうと考えました。

バグの原因の探し方

バグの原因については先のgithub issueでちょくちょく追求した結果をコメントしているのですが、まずこの問題はtracktion_engineにも含まれるロジック部分にあるだろうと考えました。DAW上ではエディットを開いている画面でMIDIファイルの取り込みを指示する場面を追いかけます*3

f:id:atsushieno:20190212151120p:plain
Import an audio or MIDI file

(ちなみにこのスクショはWaveform9で撮ったものなのですが、Waveform10ではこの周辺のUIが微妙に変わっているので、そのつもりで読み進めてください。)

"Import an audio or MIDI file" という項目なのですが、これはOSS化されていないGUIのリソースなのでソースコードには含まれていません。しかし取り込みを続行するとダイアログが出現します。

f:id:atsushieno:20190212151834p:plain
tracktion dialog

このダイアログのメッセージなら出てくるのではないか?と探します。

/sources/tracktion_engine$ grep -nR "Do you want to import tempo and time signature"
modules/tracktion_engine/selection/tracktion_Clipboard.cpp:227:                                                                            TRANS("Do you want to import tempo and time signature changes from the MIDI clip?"),
Binary file examples/projects/StepSequencerDemo/Builds/LinuxMakefile/build/StepSequencerDemo matches

なるほど確かにあります。この辺からこの関数を呼び出しているコードなどを漁っていると、pasteMIDIFileIntoEditという「現在位置にMIDIファイルの内容をペーストする」関数に行き着いて、MidiList::readSeparateTracksFromFile()という関数が実際の解析を行っている部分らしいことがわかります。これが先のgithub issueの最初のコメントでリンクしたコードになっています。C++のコードをある程度読めれば何とかなります*4。こんな感じで問題の箇所をざっくり掘り当てます。

ちなみに、バグの原因を特定できても、DAW全体をビルドできるわけではないので、修正を作ってpull requestを作るところまではなかなか至らないかもしれません。多分tracktion側も外部からのpull requestを受け付けていないと思います(JUCEもそんな感じです)。

MIDIファイルでは、テンポの設定と拍子の設定はMETAイベントとして記述されます。METAイベントは他にもいろいろあるのですが、テンポと拍子は演奏時間にダイレクトに影響する情報なので、MIDI演奏処理系ではこれらを取り出して処理することになりますし*5、Tracktionでもまずこれらのみを抽出して処理しています。

この中で「同じタイミングで存在しているイベントは(処理しても無駄なので)後のイベントだけを処理する」というロジックが含まれているのですが、ここで拍子とテンポを同時に変更していると一方が無視されるように見えたので、とりあえず「これおかしくね?」と指摘して後はTracktionの中の人に任せることにしました。

問題を切り分けるためにいろいろな条件でSMFを生成する

さてバグの追及がひと段落したので、次は問題が生じないようなSMFの条件を探し出す作業です(Tracktionを使った作業自体は進めないと困るわけで)。MMLで生成したSMFを取り込んでいたので、MML中で「テンポと拍子を同時に変更している箇所」を全部洗い出して書き換える…のは面倒なので、MML中でテンポ指定命令を上書きして「1ステップ後にずらす」ようにしました。自作MMLコンパイラはこういうハックが簡単に出来て良い…

#macro t n:number { r%1 TEMPO $n r%-1 }

さてこれで直るかな?と思って再度インポートしてみましたが、やっぱり直らないんですね。原因が違ったか…というわけで、もう少し大胆に「拍子変更を全部消す」内容にして試してみたら、さすがに今度は正しくインポートされました。

ということは、もしかして、そもそも拍子設定が含まれている曲のテンポは全般的におかしいことになるんじゃないか…と思って先のtracktionのコードを見直したら、(github issueでも追記しましたが)やっぱりおかしい、テンポ値の意味が拍子の変更で変わるところがある…というのを発見したのでした。

tracktionのデータを直接書き換えて問題のあるMIDIインポートを回避する

4/4拍子でないものを4/4で打ち込み続けるというのは割と苦痛です。普通の音楽では拍子の変更など滅多に発生しないのですが、今回の曲はこれが割と頻繁にありました(わたしがそういうジャンルに傾倒しているせいですが…)。

B    MARKER "Section B"
    [ r1  BEAT7,8r2..BEAT4,4 ]3  r1  BEAT3,4r2.BEAT4,4
    [ r1  BEAT7,8r2..BEAT4,4  r1  BEAT3,4r2.BEAT4,4 ]2
C   MARKER "Section C"
    t120
    [ BEAT3,4r2.r2.  BEAT4,4r1BEAT7,8r2..]2
    BEAT3,4r2.r2.  BEAT4,4r1BEAT7,8r2..
    BEAT3,4r2.r2.  t_120,80,0,1..,8 BEAT4,4r1BEAT7,8r2..
D   MARKER "Section D"
    t125 [ BEAT3,4r2. BEAT9,8r1r8  BEAT3,4r2. BEAT7,8r2..]2

今回のバグはtracktion_engine部分にありますが、わたしが必要としているのはWaveformという完成されたDAW製品で、しかも次のリリースまで待っていられるほど時間が無いので、今あるリソースだけで何とか作業できるようにしなければなりません。どうすれば良いでしょう…?

実は、*.tracktionプロジェクトファイルはフォーマット不明のバイナリ形式なのですが、その中のEditをあらわす*.tracktioneditファイルはXML形式なので、これにテキストエディタなどで手を加えることで、データを加工することができます。内容はもちろん独自形式なので、ある程度解読する作業が必要になりますが、所詮XMLなので特別に難しいことはあまりないです。特にトラックデータなどSMFとあまり変わらない内容です。

(ちなみにVocaloid V3の.vsqxなんかも似たような感じで解析できます。V3はもともとSMFの派生フォーマットだったV2の.vsqと同じような情報を含んでいるはずです。V5もJSONになっただけだろうと思っています。踏み込んでいませんが。)

*.tracktioneditにどんな情報が含まれているのかを調べる目的も兼ねて、.NETで*6このtracktioneditファイルの内容を読み書きできるライブラリを作りました。

github.com

ただ、まだTracktionが正常にロードできるファイルをゼロから作り出して書き出す方法がわかっていないので、既存のデータを読んで加工する程度の使い方しかできません。今これを掘り下げる時間が無いので現状有姿です。

別にコレに特化したライブラリを作らなくても、一般的なDOMやXPath/XSLTなどを使えるツールでいくらでも加工できる…と言いたいところなのですが、ここにはひとつ罠があって、*.tracktioneditXML Namespace仕様 (Namespaces in XML)に準拠していません。なので、たとえば.NETのXmlReader.Create()を使って読み込もうとすると失敗します。(上記のntractiveでは.NET 1.1時代のXmlTextReaderを使っています。) 根本的にはJUCEの問題です。

さて、今回問題になっているテンポと拍子の設定は、TEMPOSEQUENCEという要素に含まれています。4/4から9/8に変更しながらテンポを緩やかに変更するSMFを取り込むと、こんな感じになります。bpm属性の値がおよそ半分になっていることがわかります。startBeat属性は、演奏データの先頭からのデルタタイムをquarter note単位で表したものになります。

    <TEMPO startBeat="28.0" bpm="115.00002875000720337084" curve="1.0"/>
    <TEMPO startBeat="72.0" bpm="57.50001437500360168542" curve="1.0"/>
    <TEMPO startBeat="76.0" bpm="115.00002875000720337084" curve="1.0"/>
    <TEMPO startBeat="80.0" bpm="57.50001437500360168542" curve="1.0"/>

拍子の変更を取り除くとこうなります。bpmの値が正常な範囲で動いています。

    <TEMPO startBeat="28.0" bpm="115.00002875000720337084" curve="1.0"/>
    <TEMPO startBeat="127.0" bpm="120.0" curve="1.0"/>
    <TEMPO startBeat="174.0" bpm="117.14288503402025298783" curve="1.0"/>
    <TEMPO startBeat="174.0" bpm="114.2857142857142775938" curve="1.0"/>

拍子設定はXML中でどう表現されるかというと、TEMPOSEQUENCE要素の中に、TEMPO要素の後にTIMESIG要素がずらっと並ぶかたちになります。

    <TIMESIG numerator="4" denominator="4" startBeat="0.00000000000000000000"/>
    <TIMESIG numerator="4" denominator="8" startBeat="4.00000000000000000000"/>

ということは、拍子設定なしでMIDIファイルを正常に取り込んだ*.tracktioneditファイルに、手作業で後からTIMESIG要素を追加してやれば、当初期待していた通りの結果が生成できる、というわけです。ただ、拍子設定を取り込んで壊れているeditに含まれるTIMESIG要素のstartBeatの値はデタラメになるので、自分で計算し直さないといけません。面倒ですね…SMFを解析するライブラリを使って、TIME SIGNATUREメタイベントを全部デルタタイム付きで取得して、このXML要素のリストを生成するプログラムを書くと良いでしょう。

最後は面倒になってきたので考え方だけでまとめとしますが、ともあれ、これでTracktionの取り込みがおかしいとしても打ち込み作業で致命傷を受けずに済むと思います。tracktioneditファイルはXMLなので手作業で補正できるということを覚えておくと、他の問題があった場合にもたぶん役に立つと思います。

*1:会社名がTracktionで現行のDAW製品名がWaveformなのだけど、しばらく前まではTracktionという製品で、GPLで公開されているエンジンもtracktion_engineなので、以降もTracktionと書きます。

*2:ここで何度か書いているから気付いた人もいると思いますが、私の場合は自作のMMLコンパイラで打ち込んだものを取り込んでいます

*3:ちなみにプロジェクト選択画面(メインウィンドウのProjectsタブ)でインポートするとMIDIトラックデータは何も取り込まれないという謎挙動になるので、こっちは使いません

*4:全体的にJUCEモジュールなので実際のソースコードに辿り着くまでにヘッダファイルの海を泳がなければならない場面がちょいちょいありますが…

*5:先日書いたMIDIプレイヤーのマーカージャンプの話が良い例です

*6:すぐ後で言及しますが、JUCEのXMLまわりの実装が古臭いので使いたくないという気持ちもあり、XMLをいじるだけなら.NETで自前でやったほうがマシだと判断しました