LoginSignup
2
0

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

Last updated at Posted at 2024-03-24

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

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

SMFSequencer.java

MXPianoRoll.java(次節で解説)

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0