
この記事はUEC Advent Calendar 2024の15日目の記事となります。
前日の記事はもっちゃんさんの「大学構内で雑にコスプレし続けた話」です。
この記事を書いているもっちゃんをボイスチャットで見守りながら自分の記事を書いていましたので、早く書けとせかしていたところ、もっちゃんが書き上げてしまったので今逆にせかされています。ぎえ~~~。
今年もその2が生えております。
前日の記事はきゅ~さんの「財布の盗難には気を付けよう!」です。
財布を盗まれた話がかなりリアルに語られております。私は財布にあまり現金を入れていませんのでよく「私は財布落としても盗まれるものはない」と冗談半分で言っていますが、証明書の再発行は確かに面倒なので財布を落とさないように気を付けようと思った次第です。
こんにちは、へるくんです。最近、バイナリを読むのが周りで流行っているようです。pngを読んでいる人もいれば、flacを読んでいる人もいますね。
というわけで、「電通大生もすなるバイナリ解読といふものを、私もしてみむとて、するなり」という言葉もありますので、今回は流行りに乗ってバイナリを読んでみたという記事です。ついでにアウトプットがてら拙い文章で解説もしたいと思います(できるだけ噛み砕いてわかりやすくできたらと思います)。
何のバイナリを読むの?
今回はタイトルにもある通りSMFというファイルのバイナリを読みます。SMFはStandard Midi Fileの略で、要するにmidiに関するファイルフォーマットです。
これを読もうと思った主な理由は2つありまして、
- midi規格書をはじめとする記事がネット上に多いため、バイナリ解読入門としてちょうど良い
- midiのピアノロールWebアプリを作る予定なのでちょうど良い
というものがあります。あと、私はDTMが趣味なので、これを機にmidiに関する理解を深めたいというのもあります。
解析の準備
まず、解析をするSMFファイルを作らないとなりませんので、Signalというものを使用して適当に音階を打ち込んでいきます。 SignalはWeb上で動く簡易的なDAWみたいなもので、メモ程度に何か音階を打ち込みたい時にとても便利です。 今回は簡単に説明したいのでこんな感じに打ち込みました。

なんの変哲もないCメジャーですね。
打ち込んだものを保存してエクスポートすると、.mid
という拡張子のものができました。これがSMFファイルです。
SMFの主な構造
まず、SMFにはFormat0, Format1, Format2の3種類のフォーマットがあります。
Format0は全てのチャンネルデータが一つのトラックにまとめられているフォーマットです。
Format1はマルチトラックフォーマットで、楽器ごとにトラックが分けられて保存されていると言えばイメージがつくと思います。おそらくこのフォーマットが最も使用されているはずですので、今回はFormat1について話していく予定です。
Format2は正直よくわからない上にあまり普及もしていないので説明は割愛します(というか、誰か教えて下さい)。
ところで、「トラック」という音楽やってない人からするとよくわからないであろう単語が出てきました。音楽におけるトラックは、各楽器や音のデータの情報をひとつずつに分けたものです。DAWの画面でいうこれです(多分)。
SMFのバイナリは一番最初に1つのheaderチャンク(headerに関する情報がある部分)があり、その後に複数のTrackチャンク(トラックに関する情報がある部分)が続いていくような構造が基本的です。Trackチャンクには、音階情報や後述するmetadata情報など、数々の情報が含まれています。
それでは、先ほど作ったSMFのバイナリを早速見ていきましょう。
4D 54 68 64 00 00 00 06 00 01 00 02 01 E0 4D 54 726B 00 00 00 17 00 FF 03 00 00 FF 58 04 04 02 18 0800 FF 51 03 07 A1 20 00 FF 2F 00 4D 54 72 6B 00 0000 7F 00 B0 79 00 00 FF 03 00 00 B0 0A 40 00 B0 0764 00 B0 0B 7F 00 B0 65 00 00 B0 64 02 00 B0 06 4000 B0 65 00 00 B0 64 01 00 B0 06 40 00 B0 26 00 00B0 65 00 00 B0 64 00 00 B0 06 0C 00 E0 00 40 00 B001 00 00 C0 00 00 90 30 64 87 40 80 30 00 00 90 3464 87 40 80 34 00 00 90 37 64 87 40 80 37 00 00 9030 64 00 90 34 64 00 90 37 64 87 40 80 30 00 00 8034 00 00 80 37 00 00 FF 2F 00
4D 54 68 64
(utf-8に変換するとMThd)がheaderチャンク、4D 54 72 6B
(utf-8変換するとMTrk)がTrackチャンクの開始合図となっております。
今回のバイナリを見ると、4D 54 72 6B
がある箇所が2箇所あるため、trackは2つだということがわかります。
では、headerから順にバイナリを紐解いていきたいと思います。
Headerのバイナリを解析してみる
header部分のバイナリはこのようになっています。
4d 54 68 64 00 00 00 06 00 01 00 02 01 e0
4d 54 68 64
は先ほどもあった通り、このチャンクタイプがheaderだということを表しています。
その後の00 00 00 06
はheaderチャンクの長さを表しています。16進数表記である06を10進数に直しても6ですね。というわけで、この先6byte分がheaderチャンクの部分だということです。
次は00 01
の部分です。これはSMFのフォーマットタイプを表しています。今回はFormat1であるということがここでわかります。
00 02
はトラック数を表しており、今回は2トラックあることがわかります(4d 54 72 6b
がある箇所を数えるよりも、headerのこの部分を見た方がトラックの個数はわかりやすいと思います)。
最後の01 E0
は1拍あたり何tickかという情報(要するに時間分解能)を表しており、今回は16進数表記である1e0を10進数に直した480tickとなっています。
ところで、いきなり「tick」というよくわからない単位が登場しました。これはとても重要な単位ですので、説明したいと思います。
tickとは
tickというのは、1拍をさらに細かく分割した単位となってます。今回は1拍当たり480tickとなっていますので、1tickは480分の1拍となります。
この先、Trackチャンクを紐解くにあたって、「前回のイベントから何tick後(デルタタイム)にこれこれのイベントを実行」という構造が頻繁にあります。 この時の「何tick後」を表す方法が少し特殊なので先に説明します。
例えば、2拍後(すなわち960tick後)は、普通なら03 C0
だと思いますが、実は違います。87 40
と表記します。
どういうことかというと、「128*7 + 64 = 960」という計算になります。
この「128*7」の7に0x80を加えた値が「87」に反映されており、余りの64がもう1byte先の40
(10進数64を16進数に直すと40)となります。
これは可変長数値表現というもので、こうすることでより長いデルタタイムを表現することが可能です。詳しくは調べてみて下さい(一応Wikipediaの記事貼っておきます)。
Metadataを解析してみる
Trackチャンクは最初の8byteを除いて「デルタタイム + イベント」という構造が繰り返されています。
MetadataはTrackチャンクに入っており、今回は1つめのTrackチャンクにMetadataがあるので紐解いていきます。
1つめのTrackチャンクのバイナリはこのようになっています。
4D 54 72 6B 00 00 00 17 00 FF 03 00 00 FF 58 0404 02 18 08 00 FF 51 03 07 A1 20 00 FF 2F 00
4D 54 72 6B
は先ほどから言っている通り、チャンクタイプが何かを表しています。
00 00 00 17
はこのTrackチャンクの長さを表しており、今回はこの後に0x17 = 23byte続いていることになります。
この後から「デルタタイム + イベント」という構造が繰り返されます。
とりあえず、00 FF 03 00
を見てみましょう。最初の00
はデルタタイムすなわち前回のイベントから何tick後にこの後のイベントを実行するかを表しています。
今回は0tick後ということになります。
FF
はこのイベントがMetadataイベントであることを表しています。その後の03
がMetadataイベントの種類を示しており、03はトラックの名前であることを表しています。
その後の00
はこのメタイベントのデータがこの先何byteまで続くかを表しています。今回は1byteも続かないようです。
メタイベントは基本的に「ff + Metaイベントの種類 + 長さ + データ」というような構造になっています。
メタイベントの種類はこんな感じになっています。詳しくはmidi1.0規格書を読んでください。
バイナリ | メタイベント内容 | 備考 |
---|---|---|
FF 00 02 ssss | シーケンス番号 | ssss は上位バイトが先にストアされた16bitの値 |
FF 01 len text | テキストイベント | len はメタイベント中でその後に続くデータ長text はテキストデータ |
FF 02 len text | コピーライト表示 | |
FF 03 len text | トラック名 | |
FF 04 len text | インストゥルメント名 | |
FF 05 len text | 歌詞 | |
FF 06 len text | マーカー | |
FF 07 len text | キューポイント | |
FF 20 01 cc | MIDIチャンネルプリフィックス | |
FF 2F 00 | エンドオブトラック | トラックチャンクの終了合図 |
FF 51 03 tttttt | テンポ | tttttt は1拍あたりのマイクロ秒数(μs)の16進数表示 |
FF 54 05 hr mn se fr ff | SMPTEオフセット | あまりよくわかってないが、知らなくても問題なさそう |
FF 58 04 nn dd cc bb | 拍子 | nn は分子dd は分母で2の累乗(ddが02の場合は分母は2^2で4)cc はmidiクロック数bb は4分音符一つ分と同じ32分音符の数 |
FF 59 02 sf mi | 調号 | sf =-7は7フラット、sf =-1は1フラットsf =0はC調、sf =1は1シャープsf =7は7シャープ、mi =0は長調mi =1は短調 |
FF 7F len data | シーケンサー固有のメタイベント |
多分この表とmidiの規格書があればMetaDataは読めると思います。FF 2F 00
がトラックチャンク終了の合図です。
1つ目のトラックチャンクはMetaData情報のみで終わりです。
もう一つのTrackチャンクを見てみる
さて、2つ目のトラックチャンクを見てみます。
4D 54 72 6B 00 00 00 7F 00 B0 79 00 00 FF 03 00 00 B00A 40 00 B0 07 64 00 B0 0B 7F 00 B0 65 00 00 B0 64 0200 B0 06 40 00 B0 65 00 00 B0 64 01 00 B0 06 40 00 B026 00 00 B0 65 00 00 B0 64 00 00 B0 06 0C 00 E0 00 4000 B0 01 00 00 C0 00 00 90 30 64 87 40 80 30 00 00 9034 64 87 40 80 34 00 00 90 37 64 87 40 80 37 00 00 9030 64 00 90 34 64 00 90 37 64 87 40 80 30 00 00 80 3400 00 80 37 00 00 FF 2F 00
4D 54 72 6B
はもはやいつものやつです。
00 00 00 7F
を見ると、今回のTrackチャンクは0x7f=127byte先まで続いていることが分かります。
00 FF 03 00
は先ほども出ましたメタデータ情報で、トラック名を表しています。相変わらず長さは0なので今回も無視です。
ところで、ここまではFF
(メタデータ)のイベントしか扱ってませんでしたが、もちろんFF
以外もあります。
以下がイベントの種類となっています。
バイナリ | イベント内容 | 備考 |
---|---|---|
8n kk vv | Ch.n+1 ノートオフ | n はチャンネル番号-1、kk はノート番号 |
9n kk vv | Ch.n+1 ノートオン | n はチャンネル番号-1、kk はノート番号vv =00の時は実質ノートオフ |
An kk vv | Ch.n+1 ポリフォニックキープレッシャー | n はチャンネル番号-1、kk はノート番号vv はベロシティ |
Bn cc vv | Ch.n+1 コントロールチェンジ | cc はコントロールナンバーチャンネル番号でコントロールナンバーccに値vvを送信 |
Cn pp | Ch.n+1 プログラムチェンジ | nはチャンネル番号、音色をpp に変更 |
Dn vv | チャンネルプレッシャー | vv はプレッシャー情報 |
En mm ll | ピッチベンド | mm はLSB、ll はMSB |
FF data | メタデータイベント | data はメタイベント内容 |
コントロールチェンジ
さて、これらのイベント内容を把握したところで、次の部分の00 B0 0A 40
という部分を見てみましょう。
00
はいつもの前回のイベントから0tick後に行うイベントという意味です。
B0
とあるのでCh.1のコントロールチェンジであることが分かります。
コントロールチェンジのコントロールナンバーはそこそこ種類があります。コントロールナンバー一覧はこの記事を見た方が早いと思われます。
ちなみに今回は0A
がコントロールナンバーですので、パンに関するコントロールナンバーであることが分かります。
40
は10進数で64ですので、パンの値は真ん中であることが分かります。
この先もしばらくコントロールチェンジイベントが続きますので、上記の参考記事を基にバイナリを読み進めてみてください。
プログラムチェンジとピッチベンド
プログラムチェンジに関するバイナリを読み進めると、00 E0 00 40
という部分が出てきますが、これはピッチベンドです。00
はLSB、40
はMSBとなっています。
また読み進めていくと、00 C0 00
という部分が出てきますが、これがチャンネル1へのプログラムチェンジです。要するに音色を指定しています。
今回は00
ということなのでアコースティックピアノが指定されていることが分かります。
チャンネルのノートオン、ノートオフ
お待たせしました。ようやく音階の情報がここで出てきました。
00 90 30 64
から紐解いていきます。
00
はいつものデルタタイムです。90
はチャンネル1のノートオンイベントです。要するに、「音を出すよ」ということです。
30
はどの音を出すかを表しています。これはドの音を出そうとしていますね。
64
はベロシティです。まあ、音の強さだと思ってくれれば大丈夫です。今回はベロシティは100です。
次の部分を見てみましょう。
87 40 80 30 00
です。
まず。87 40
はデルタタイム(前回のイベントから何tick後のイベントか)です。これはちょっと前に説明した通り、「128*(0x87-0x80) + 0x40 = 960」ということで、2拍後に実行するイベントであるということが分かります。
80
はチャンネル1のノートオフです。要は「音を鳴らすのをやめるよ」ということです。
30
なので、ドの音を消すことになります。00
はベロシティです。音を消すので00以外あり得ません。
こんな感じの要領で残りのバイナリを紐解いていくと、先ほど打ち込んだとおりの音階になっていることが分かると思います。
そして、最後に00 FF 2F 00
でこのトラックチャンクはおしまいです。
SMFのパーサを作ってnpmパッケージにしたい
「ここまでSMFを読み解いてお前どうするの?」という話ですが、現在SMFのパーサをゆっくり作成している途中です。 これをnpmパッケージにして自分が今一番作りたいピアノロールWebアプリを作ることができたらと思っています。
ただ、課題もありまして、どうしてもノートオンとノートオフの処理が大変です。元々SMFはMIDIのリアルタイム信号を扱っているので、ノートオンされた音がノートオフされる保証がないです。 そのため、音階を処理する際に返す形として
export type Note = { scale: Scale; //音階 octave: Octave; timing: number; // 音が鳴るタイミング length: number; //音を鳴らす長さ velocity: number;}
とするべきか
export type Note = { scale: Scale; //音階 octave: Octave; timing: number; // イベントタイミング noteType: NoteType; //NoteTypeは'noteon'か'noteoff' velocity: number;}
にすべきか悩んでいます。 前者の場合、ノートオフされていない音があった瞬間にバグを踏んでしまいます。後者はその心配はないものの、この先ピアノロールを実装する際に面倒なことになりそうです。
今のところは前者にする予定で作っていますが、実際どうなるかは分かりません。 完成までゆっくり見守っていただければと思います。
終わりに
バイナリを読むのはあまりにも
苦行すぎる!!!
明日の記事はkanaruさんの「TerraformでProxmoxの自宅鯖を管理する」です。