StandardMIDIFileからピアノロールを表示する その1：SMFの解析

StandardMIDIFileからピアノロールを表示する その1：SMFの解析

今日は、MIXRecipeで使われているピアノロールについて解説してみます。

MIXRecipeはこちらです

https://github.com/syntaro/javamidimixer/releases
https://github.com/syntaro/javamidimixer/tree/main/mixrecipe

![スクリーンショット 2024-03-24 10.16.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518713/7c47b8d8-feab-8963-152d-6201bacbf284.png)

Java標準の、
https://docs.oracle.com/javase/jp/11/docs/api/java.desktop/javax/sound/midi/Sequencer.html

ではピアノロールの表示ですこし不便でしたので、SMFファイルの解析からやっています。

クラス群の要件

・SMFのイベント（音符）を時間範囲を指定して取り出せること
・SMF独特の時間の概念は、1/1000秒単位に変換して、音符を並び替えて管理すること

この要件をみたすためには、Java標準クラスでは物足りない、逆に複雑化してしまいます。
C＃でも実装したいので、C#にないものは再発明しておきたかったのもあります。

SMFファイルの構造は以下の順番になっています。

・4文字=マジックナンバー"MThd"
・32ビット=ヘッダーの長さが(バイト数)=固定値6が格納されている
・16ビット=SMFファイルタイプ
・16ビット=トラック数
・16ビット=解像度（後述します）

```readReso.java
            int res = smfStream.read16();
            if (res >= 0x8000) {
                res = 0xffff0000 + res;
            }
            if (res > 0) {
                // delta = 四分音符あたりの解像度
                _smpteFormat = -99999;
                _fileResolution = res;
            } else {
                // delta = 秒の分数
                _smpteFormat = (res >> 8) * -1;
                _fileResolution = res & 0xff; // フレーム内の分解能
            }
```

・もしヘッダーの長さが7以上になっていれば、-6したバイト数分=まだ読んでない分スキップします。

それに以下が繰り返されます
・4文字=マジックナンバー"MTrk"を探します
・32ビット=トラックの長さ(バイト数)
・トラックデータ

トラックデータを別のストリームとして切り離して処理しています。
トラックデータは以下の繰り返しです
・可変長数値=経過時間（Tick）
・ステータスコード=0x80以上を読み取った場合のみです。
（もし0x7f以下の場合、前回よみとった、ステータスコードを再利用します。）
・ステータスコードに関連するMIDIメッセージ（2バイト～3バイト）
（2バイト= ステータス0x80, 0x90, 0xa0, 0xb0, 0xe0）
（3バイト= ステータス0xc0, 0xd0)
（可変バイト=ステータス0xf0)
　可変バイトのMETAデータ、RESETデータは、構造が少しことなります。
  0xf0 + 8ビットタイプコード + 可変長データ長さ + データ

この繰り返しですが、さて、可変長の数値についての説明が必要ですね。

```readVariable.java
        long value = 0; // the variable-lengh int value
        int currentByte = 0;
        //4ステップ以上のデータはないが
        do {
            currentByte = read8();
            if (currentByte < 0) {
                return -1;
            }
            value = (value << 7) + (currentByte & 0x7F);
        } while ((currentByte & 0x80) != 0);
        return value;
```

最上位ビットが立っていれば、それ以外の7ビットを、数値として、
7ビットを、いくつかつなげるということです。

次にタイミングのTickについです。メッセージは、Tickを保持した状態でリストとして保管されています。
リストはソートしておきます。
　Tick＞格納された順序>プログラムチェンジ＞その他ノートなど

Tickからマイクロ秒に変換するのですが、テンポが曲中に出現したら、
それ以降はそのテンポで、マイクロ秒に変換していきます。

参考にさせていただいたQiita
・タカヨシ様のQiita
　https://qiita.com/takayoshi1968/items/8e3f901539c92a6aac16
・McbeEringi様のQiita
　https://qiita.com/McbeEringi/items/17c24d78fcec2907ef95

 Tickとミリ秒を保持したクラスを並び替えておけば、計算は簡単なようです。

```setTiming.java
        _tempoArray = new SMFTempoArray(this);
        for (SMFMessage seek : list) {
            seek._millisecond = _tempoArray.calcMicrosecondByTick(seek._tick) / 1000;
            if (seek.getStatus() == 0xff && seek.getData1() == 0x51) {
                _tempoArray.addMPQwithTick(seek.getMetaTempo(), seek._tick);
            }
            _listMessage.add(seek);
        }
```

として、ミリ秒を埋めていきます。使用するクラス↓

```SMFTempoArray.java
    import jp.synthtarou.midimixer.mx36ccmapping.SortedArray;
    
    /**
     *
     * @author Syntarou YOSHIDA
     */
    public class SMFTempoArray extends SortedArray<SMFTempo> {
    
        SMFParser _parent;
    
        /* デフォルトのMPQを記録 */
        public SMFTempoArray(SMFParser parser) {
            _parent = parser;
            SMFTempo tempo = new SMFTempo();
            tempo._mpq = 500000; //120BPM
            tempo._microsecond = 0;
            tempo._tick = 0;
            add(tempo);
        }
    
        /* Tick以前の、最後のSMFTempoを返す */
        SMFTempo backSeekByTick(long tick) {
            SMFTempo ret = get(0);
            for (int x = 1; x < size(); x++) {
                SMFTempo seek = get(x);
                if (tick >= seek._tick) {
                    ret = seek;
                }
            }
            return ret;
        }
    
        /* Tickから、Microsecondを求める */
        public long calcMicrosecondByTick(long ticks) {
            SMFTempo tempo = backSeekByTick(ticks);
    
            double deltaTick = ticks - tempo._tick;
            double deltaMicrosecond =  deltaTick * tempo._mpq / _parent._fileResolution;
    
            long ret = tempo._microsecond + (long)deltaMicrosecond;
            return ret;
        }
    
        /* TickベースでMPQを追加 （順番通りに追加する必要がある）  */
        public void addMPQwithTick(long mpq, long tick) {
            SMFTempo tempo = backSeekByTick(tick);
    
            double deltaTick = tick - tempo._tick;
            double deltaMicrosecond = deltaTick * tempo._mpq / _parent._fileResolution;
    
            if (deltaTick == 0) {
                //上書き
                tempo._mpq = mpq;
                return;
            }
            if (tempo._mpq == mpq) {
                //変化なし
                return;
            }
            
            SMFTempo newTempo = new SMFTempo();
            newTempo._mpq = mpq;
            newTempo._tick = tick;
            newTempo._microsecond = tempo._microsecond + (long)deltaMicrosecond;
    
            add(newTempo);
        }
    
        /* Microsecond以前の、最後のSMFTempoを返す */
        SMFTempo backSeekByMicroSecond(long us) {
            SMFTempo ret = get(0);
            for (int x = 1; x < size(); x++) {
                SMFTempo seek = get(x);
                if (us >= seek._microsecond) {
                    ret = seek;
                }
            }
            return ret;
        }
    
        /* Microsecondから、Tickを求める */
        public long calcTicksByMicrosecond(long microSecond) {
            SMFTempo tempo = backSeekByMicroSecond(microSecond);
            
            double deltaMicroSecond = microSecond - tempo._microsecond;
            double deltaTick = deltaMicroSecond * _parent._fileResolution / tempo._mpq;
    
            return tempo._tick + (long)deltaTick;
        }
    
        /* MicrosecondベースでMPQを追加 （順番通りに追加する必要がある） */
        public void addMPQwithMicrosecond(long mpq, long microSecond) {
            SMFTempo tempo = backSeekByMicroSecond(microSecond);
    
            double deltaMicroSecond = microSecond - tempo._microsecond;
            double deltaTick = deltaMicroSecond * _parent._fileResolution / tempo._mpq;
    
            if (deltaMicroSecond == 0) {
                //上書き
                tempo._mpq = mpq;
                return;
            }
            if (tempo._mpq == mpq) {
                //変化なし
                return;
            }
    
            SMFTempo newTempo = new SMFTempo();
    
            newTempo._mpq = mpq;
            newTempo._tick = tempo._tick + (long)deltaTick;
            newTempo._microsecond = microSecond;
    
            add(newTempo);
        }
    }
```

あとは、ミリ秒がきたら、音をならす、次のミリ秒まで待機するといった具合で再生できますが、
ピアノロールの場合、音のないタイミングでも、音を予知してスクロールする必要がありますので、
前回のペイントから、なんとかミリ秒たっていたら、スクロールイベントを発生させるという、
待機ルーチンになります。これは、Y軸の長さ（解像度）と表示する時間の幅によって、設定されるミリ秒です。

ピアノロールクラスには、setTiming(long milliSeconds)を公開しておいてもらえばできます。

StandardMIDIFileからピアノロールを表示する2に続きます。

SMFParser.java

https://github.com/syntaro/javamidimixer/blob/main/mixrecipe/src/jp/synthtarou/libs/smf/SMFParser.java

SMFSequencer.java

https://github.com/syntaro/javamidimixer/blob/main/mixrecipe/src/jp/synthtarou/libs/smf/SMFSequencer.java

MXPianoRoll.java（次節で解説）

https://github.com/syntaro/javamidimixer/blob/main/mixrecipe/src/jp/synthtarou/midimixer/mx00playlist/MXPianoRoll.java