私が今まで興味を持って触れたことをいろいろ書いていきたい。 たぶんここが一番ボリュームあると思う。 技術系や、音楽系がメインになるかな~?
MIDIをバイナリで読んだ話
一般的にMIDIファイルをC#などで読み込みたい時は専用のライブラリを使うことが多いと思う。 だけど、それらをあえて使わずにバイナリデータを直接読み込んで使いたい。 そんな話を備忘録も兼ねて書いてみる。ちなみに言語はC#。 なんで直接読むの? 理由はいろいろあるけど、思いつくのを書いてみる。 重そう、でかいファイルを扱えない、使う目的に合わせて最適化したい、自分で解析して扱ってみたい,,,etc その中でも一番なのが使う目的に合わせて最適化したいという部分。 MIDIと言っても通常のMIDIファイルではなく、Black MIDIと呼ばれる数千、数億ノーツにまで及ぶ巨大なMIDIファイルををプログラムで扱おうとしたとき、 おそらく普通のライブラリでは重かったりメモリ不足になったりすると思われる。 そこで読み込み方、データの保持の仕方、それらの処理の仕方などを自分で制御できれば、巨大なMIDIファイルでもちゃんと扱えそうだと思い、直接読む手段を選んだ。 まずはファイルを読み込む 最初扱ってた時はFile.ReadAllBytes()で全て配列に格納し、ループで1バイトずつ読むという方法を採用していた。 だけど、この方法では最大で2GBのファイルまでしか読めないということに気づいた。 次にStreamを使って読む方法を試し、これなら数MBずつ読んだりできるのでどれだけでかいファイルでも問題なく読むことができるようになった。 今のところ思いつく方法はこれくらいかな~。 MIDIの構造について バイト単位で読む以上、MIDIの構造を理解していないと何も始まらない。 そこで、私は以下のページを参考にしてMIDIの構造を理解し、プログラムで扱えるようになった。(作者様には感謝) Welcome to yyagi's web site. - SMF (Standard MIDI Files) の構造 https://sites.google.com/site/yyagisite/material/smfspec#TrackData ヘッダートラック まず最初に必ず来るのがヘッダートラックと呼ばれる、MIDIファイルの基本情報を書くための領域。 14バイト固定なので、ここのデータが特に不要の場合はいきなり15バイト目から読む場合も。 最初の4バイトは固定で必ず「4D 54 68 64」が来る、そうじゃない場合はMIDIファイルじゃないか壊れているだろう。 次の4バイトがこの次に続くヘッダートラックのデータ部分のバイト数、残りは6バイトで固定なのでここも必ず「00 00 00 06」となる。 次の2バイトがIDIのフォーマット指定、0が1トラックに全てを詰め込む方式で、1がマルチトラック、2は見たことないからわからん。 次の2バイトが全体のトラック数。MIDI全体を読まなくても、ここを見ればこのMIDIにはいくつトラックがあるかを把握することができる。 2バイトまでなのでMIDIファイル上での最大トラック数は65535トラックまでということになるのかな。 最後の2バイトがデルタタイムの指定。最上位ビットによって意味が変わるが、0の場合でしか見たことないので1はわからん。 0の場合は、残りのビットに分解能のデータが入る。分解能とは、MIDIの最小単位であるtickが1拍中にいくつ入るかを示したもの。 480と入っていた場合は1小節が1920tickに分割されることになる。 実データトラック ここからは実際にノーツデータが格納される実データトラックが続いて行くことになる。 最初の4バイトは固定で必ず「4D 54 72 6B」が来る。 次の4バイトにこの後続く実データが何バイトあるかを格納する。4バイトなので1トラックの最大データ数は約4GBとなる。 ここからは実データとなるが、大まかに以下の構造となっている。 最初の1~4バイト:可変長でデルタタイムが入る。デルタタイムとは直前のデータから次のデータまで何tick離れているかという情報。 次の1バイト:ステータスバイトと呼ばれるもので、上位4ビットに次の情報を示したMIDIイベント、下位4ビットにチャンネルが格納されている。 その次のバイト:ステータスバイトごとにデータ構造が変わるため、条件分岐して処理を分ける必要がある。 ランニングステータス 先ほどのステータスバイトは、一部省略できるものがある。 そもそもステータスバイトは上位4ビットがかならず8以上となり、最上位ビットが1になる。 そして、省略が許されているステータスバイトの次の1バイトは必ず最上位ビットが0となる。 なので、ステータスバイトを読んだ時に最上位ビットが0だった場合、ランニングステータスが適用されているということになるので、直前のステータスと同様だとして解釈する必要がある。 これらの構造を理解して置けば、MIDIファイルを読み書きできるということになる。 まずデルタタイムを読み、ステータスバイトを読み、それに合わせて次のデータを読み、次のデルタタイムに進む、、、といった流れの繰り返しで呼んでいくことになる。 処理速度を上げるための工夫 勉強したての頃は、わかりやすくするためバイトを判定するときは文字列化して比較していた。 midiData[]という配列にcntという変数でインデックス指定して読んでいた場合、 if (midiData[cnt].ToString().Substring(0, 1) == “B” && …というようなことをしていたため、まぁ遅い。 そこでいろいろ調べた結果ビットシフトやビット演算というものを知り、 if (midiData[cnt] » 4 == 11 && …のように置き換えたところ劇的に早くなった。体感数倍以上速くなったのでかなり驚いた記憶。 それ以外は分割読み込みするときに数MB単位にしたり、ProgressBarへの表示を間引いたりすることで処理速度を上げていた。 ...