わたしが最近ひいき目に(?)しているDAWとして、Tracktion社のWaveformを使っているのですが、今日はこのTracktionまわりのhackを紹介します。*1
他所で作ったMIDIファイルをTracktionに取り込む時*2、たまにTracktionの挙動が不審で、予期しない謎の音楽が生成されることがあります。最近わたしが経験したものでは、METAイベントのテンポと拍子設定が入り乱れる音楽を取り込むと、テンポがめちゃくちゃになる(しかも説明が困難なかたちで)というものでした。
MIDIなんて使ってるやつそんなおらんやろ?と思われそうな気もしますが、Tracktionをはじめ大抵のDAWのMIDIトラックやらインストゥルメンタルトラックやら呼ばれるものは、そして各種オーディオプラグインは、内部的にMIDIメッセージのやり取りで成り立っているので、この辺の問題のインパクトはそれなりにあります。この問題の場合は、テンポと拍子の扱いが「根本的に何かおかしい」可能性があります。
それはそれでせっかくgithubで公開されているのでissueとして登録したのですが、Tracktionを使った作業自体は継続したいわけです。なのでTracktionにはバグレポートしつつ、Tracktionが正常に動作するようなSMFを作って取り込もうと考えました。
バグの原因の探し方
バグの原因については先のgithub issueでちょくちょく追求した結果をコメントしているのですが、まずこの問題はtracktion_engineにも含まれるロジック部分にあるだろうと考えました。DAW上ではエディットを開いている画面でMIDIファイルの取り込みを指示する場面を追いかけます*3。
(ちなみにこのスクショはWaveform9で撮ったものなのですが、Waveform10ではこの周辺のUIが微妙に変わっているので、そのつもりで読み進めてください。)
"Import an audio or MIDI file" という項目なのですが、これはOSS化されていないGUIのリソースなのでソースコードには含まれていません。しかし取り込みを続行するとダイアログが出現します。
このダイアログのメッセージなら出てくるのではないか?と探します。
/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ファイルの内容を読み書きできるライブラリを作りました。
ただ、まだTracktionが正常にロードできるファイルをゼロから作り出して書き出す方法がわかっていないので、既存のデータを読んで加工する程度の使い方しかできません。今これを掘り下げる時間が無いので現状有姿です。
別にコレに特化したライブラリを作らなくても、一般的なDOMやXPath/XSLTなどを使えるツールでいくらでも加工できる…と言いたいところなのですが、ここにはひとつ罠があって、*.tracktionedit
はXML 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で自前でやったほうがマシだと判断しました