MIDIのSysEX(F7始まり)を処理する (Java JDK20 0xf0 0xf7)

はじめまして。
お会いできて光栄です。

手始めに、「MIDIのSysEX(F7始まり)を処理する (Java JDK20 0xf0 0xf7)」について公開してみようと思います。

OralceJavaJDKのjavax.sound.midi.MidiMessageクラスは、
SysEXシステムエクスクルーシブメッセージに対応しているとうたっています。
F0ではじまるSysEXは問題なく送信できるようです。
しかし、F7ではじまる場合、うまく送信できません。

F0 + メッセージ
F7 + メッセージ2

と送っていますが、受け止め側が、

F0 + メッセージ F7
+ ごみ

と解釈するためです。

ですので、なんとかF7を消去しておくりたいということです。

F7ではじまるMIDIMessageでは、F7をのぞいて送信しています。
ただし、JDK20の前以前のJavaでは、このやりかたではクラッシュするようです。

対処するにあたっては、以下のキーワード検索した文章を組み合わせています。
stackoverflowやQiitaなどです。動作するようにまとめられましたので、公開しています。

midimessage java sysex f0 f7 crash java20

JDK20~JDK22までは動作確認しています。Windows10のみ確認しています。
将来のJDKではこのコードが必要なくなる可能性もあります。

では、私の場合の、プログラムコードをご紹介します。

ここに至るには、JavaMIDIMixerの作成を行ったことに発端しています。
　https://github.com/syntaro/javamidimixer

MidiMessageクラスのかわりに、以下を用いることで可能になります。

まず、使い方からです。
```test.java
    //送信するSysEX配列
    MIDIMessage[] _listMessage = _____; 
    //1つの場合でも、SysexSplitterに一度格納すると、F0F7のとり扱いがすっきりする
    //むしろこちらが重要かもしれない
    SysexSplitter splitter = new SysexSplitter();
    for (MIDIMessage message : listMessage) {
        byte[] data = message.getMessage();
        splitter.append(data);
    }
    //データが見つかれば
    if (splitter.isEmpty() == false) {
        //200文字単位で区切って（区切らなくても送信できるJava実装もある様子)
        ArrayList<byte[]> listPacket = splitter.splitOrJoin(200); 
        //連続して送信する
        for (byte[] segment : listPacket) {
            //ここで、F7で始まるSysEXの送信に成功する
            SplittableSysexMessage message = new SplittableSysexMessage(segment);
            receiver.send(message);
        }
    }
```

この記事の基本の、

```SplittableSysexMessage.java
/*
 * Copyright 2023 Syntarou YOSHIDA.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package jp.synthtarou.midimixer.libs.midi.sysex;

import java.io.ByteArrayOutputStream;
import java.util.logging.Level;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiMessage;
import jp.synthtarou.midimixer.libs.common.MXLogger2;

/**
 *　Java20以上で、SysEXを処理するJava標準MidiMessageのラッパー
 * F0で始まる場合、getStatus()をF0として、バイト配列はすべてを返します
 * F7で始まる場合、getStauts()をF7として、バイト配列は先頭のF7を含みません
 * このようにすると、JavaAPIは正しく処理します。
 * F0で始まるメッセージのあと、F7で始まるメッセージをおくると、
 * F7をひとつ前のメッセージの終端とうけとめられるので、F7を送らないという事です。
 * ただし、Java20以上が必要ですJDK20未満ではアプリがクラッシュしていました。
 * Windows10のみ動作確認すみ。
 * @author Syntarou YOSHIDA
 * @see jp.synthtarou.midimixer.libs.midi.sysex.SysexSplitter
 */
public class SplittableSysexMessage extends MidiMessage {

    /**
     * @param data
     * @throws InvalidMidiDataException
     */
    public SplittableSysexMessage(byte[] data) throws InvalidMidiDataException {
        super(new byte[2]);
        setMessage(data, data.length);
    }
    
    /**
     * メッセージをこのクラスとして加工して設定します。
     * @param data F0またはF7ではじまるバイト配列、最終バイトはF7である必要はない
     * @param dataLength バイト長 (先頭バイトをふくむ)
     * @throws InvalidMidiDataException
     */
    @Override
    protected void setMessage(byte[] data, int dataLength) throws InvalidMidiDataException {
        _status = data[0] & 0xff;
        int last = data[data.length - 1] & 0xff;
                
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        if (_status == 0xf0 && last == 0xf7) {
            _status = 0xf0;
            _offset = 0;
        }else if (_status == 0xf0) {
            _status = 0xf0;
            _offset = 0;
        }else if (_status == 0xf7) { 
            //終端文字は問わない
            _status = 0xf7;
            _offset = 1;
        } else {
            throw new InvalidMidiDataException("data not start f0 or f7");
        }
        out.write(data,  _offset, dataLength - _offset);
        byte[] trans = out.toByteArray();
        _length = trans.length;
        setMessagePlain(trans, trans.length);
    }
    
    /**
     * メッセージをこのクラスなりの加工をせず、スーパークラス形式で設定します。
     * @param data バイト配列
     * @param dataLength バイト長
     * @throws InvalidMidiDataException
     */
    protected void setMessagePlain(byte[] data, int dataLength) throws InvalidMidiDataException {
        super.setMessage(data, dataLength);
    }
    
    /**
     *　SplittableSysexMessageを複製します
     * @return 複製されたObject
     */
    public Object clone() {
        try {
            byte[] plain = getMessage();
            SplittableSysexMessage inst = new SplittableSysexMessage(new byte[2]);
            inst._status = _status;
            inst._length = _length;
            inst.setMessagePlain(plain, plain.length);
            return inst;
        }
        catch(InvalidMidiDataException ex) {
            MXLogger2.getLogger(SplittableSysexMessage.class).log(Level.WARNING, ex.getMessage(), ex);
            return null;
        }
    }
    
    /**
     * Receiverが受け付けるスタータスコード
     * @return F0またはF7
     * setMessageおよびコンストラクタで設定したステータス
     */
    @Override
    public int getStatus() {
        return _status;
    }
    
    /**
     * Receiverが受け付けるデータ長さ
     * @return getMessageで返されるバイト配列の長さ
     */
    @Override
    public int getLength() {
        return _length /* + _offset */;
    }
    
    /**
     * Receiverが受け付けるデータ
     * @return 送信するバイト配列、F0やF7で始まるかは問わない
     */
    @Override
    public byte[] getMessage() {
        byte[] raw = super.getMessage();
        if (raw.length != getLength()) {
            throw new IllegalArgumentException("raw.length " + raw.length + " != data.length " + getLength());
        }
        if (raw.length > 0 && (raw[0] & 0xff) == 0xf7) {
            Exception ex = new Exception("Something Wrong");
            MXLogger2.getLogger(SplittableSysexMessage.class).log(Level.WARNING, ex.getMessage(), ex);
        }
        return raw;
    }

    int _offset;
    int _status;
    int _length;
}
```

```SysexSplitter.java
/*
 * Copyright 2023 Syntarou YOSHIDA.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package jp.synthtarou.midimixer.libs.midi.sysex;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import jp.synthtarou.midimixer.libs.midi.smf.SMFInputStream;

/**
 *　SysEXメッセージを分割、結合するため
 * @see jp.synthtarou.midimixer.libs.midi.sysex.SplittableSysexMessage
 * @author Syntarou YOSHIDA
 */
public class SysexSplitter {

    /**
     * コンストラクタ
     */
    public SysexSplitter() {
        
    }

    /**
     * バイト配列を格納する
     */
    ByteArrayOutputStream _dataBody = new ByteArrayOutputStream();
    
    /**
     * SysEXデータを連結する
     * @param sysexData 連結するバイト配列
     *  F0かF7で始まる必要がある、終端文字F7の”手前"までを格納する
     *  F0でもF7でもない始まりの場合、そこまでスキップされる、エラーにはしない
     */
    public void append(byte[] sysexData) {
        SMFInputStream reader = new SMFInputStream(sysexData);

        int status = reader.read8();
        
        while (status != 0xf0 && status != 0xf7 && status >= 0) {
            status = reader.read8();
        }
        
        _endingCode = false;
        if (status == 0xf0 || status == 0xf7) {    
            if (_beginningStatusCode == 0) {
                _beginningStatusCode = status;
            }
            while (status >= 0) {
                status = reader.read8();
                if (status < 0) {
                    break;
                }
                
                if (status == 0xf7) {
                    _endingCode = true;
                    break;
                }
                _dataBody.write(status);
            }
        }
    }
    
    /**
     * SyeEXデータが格納されていないかテスト
     * @return 格納されていなければture
     */
    public boolean ieEmpty() {
        return _beginningStatusCode != 0xf0 && _beginningStatusCode != 0xf7;
    }
    
    /**
     * byte配列に分割する
     * @param maxLength パケットの最大長さ、極端に短い(10未満)場合、分割しない
     * @return 分割されてbyte配列。SplittableSysexMessgeを作ると送信可能
     */
    public ArrayList<byte[]> splitOrJoin(int maxLength) {
        if (maxLength < 10) {
            maxLength = 10000000;
        }

        ArrayList<byte[]> listResult = new ArrayList<>();

        byte[] data = _dataBody.toByteArray();
        ByteArrayOutputStream segment = new ByteArrayOutputStream();

        for (int i = 0; i < data.length; ++ i) {
            int ch = data[i] & 0xff;
            
            if (maxLength > 0 && segment.size() >= maxLength) {
                byte[] result = segment.toByteArray();
                if (result.length > 1) {
                    segment.write(ch);
                    result = segment.toByteArray();
                    listResult.add(result);
                }
                segment = new ByteArrayOutputStream();
            }
            else {
                /* なんか数字がきたらはじめる */
                if (segment.size() == 0) {
                    if (listResult.isEmpty()) {
                        /* 最初はF0はじまり */
                        segment.write(_beginningStatusCode);
                    }
                    else {                        
                        segment.write(0xf7);
                    }
                }
                segment.write(ch);
            }
        }
        
        if (segment.size() >= 1) {
            /* 最期は終端 */
            if (_endingCode) {
                segment.write(0xf7);
            }
            byte[] result = segment.toByteArray();
            listResult.add(result);
        }

        return listResult;
    }

    /**
     * スタータスコード
     */
    int _beginningStatusCode;
    /**
     * 終端F7に出会っているかどうか。その場合splitOrJoinはにF7を追加して出力する
     */
    boolean _endingCode;
}
```


では、よいハッキングライフを！
＃もし、エラーがありましたら、環境をそえてコメントいただけますと、幸いです。

動作テストは以下で行えます
```SysexSplitTest.java
/*
 * Copyright (C) 2024 Syntarou YOSHIDA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package example;

import java.util.ArrayList;
import java.util.List;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.SysexMessage;
import jp.synthtarou.midimixer.libs.midi.sysex.SplittableSysexMessage;
import jp.synthtarou.midimixer.libs.midi.sysex.SysexSplitter;

/**
 *
 * @author Syntarou YOSHIDA
 */
public class SysexSplitTest {

    public static int AMERICA_FROM = 0x01;
    public static int AMERICA_TO = 0x1F;

    public static int AMERICA_SEQUENCIAL = 0x01;
    public static int AMERICA_CLARITY = 0x1F;

    public static int EUROPE_FROM = 0x20;
    public static int EUROPE_TO = 0x3F;

    public static int EUROPE_PASSAC = 0x20;
    public static int EUROPE_WERCI = 0x3B;

    public static int JAPAN_FROM = 0x40;
    public static int JAPAN_TO = 0x5F;

    public static int JAPAN_KAWAI = 0x40;
    public static int JAPAN_ROLAND = 0x41;
    public static int JAPAN_KORG = 0x42;
    public static int JAPAN_YAMAHA = 0x43;
    public static int JAPAN_CASIO = 0x44;
    public static int JAPAN_KAMIYA_STUDIO = 0x46;
    public static int JAPAN_AKAI = 0x47;
    public static int JAPAN_JAPAN_VICTOR = 0x48;
    public static int JAPAN_MEISOSHA = 0x49;
    public static int JAPAN_HOSHINO_GAKKI = 0x4A;
    public static int JAPAN_FUJITSU = 0x4B;
    public static int JAPAN_SONY = 0x4C;
    public static int JAPAN_NISSIN_ONPA = 0x4D;
    public static int JAPAN_TEAC = 0x4E;
    public static int JAPAN_SYSTEM_PRODUCT = 0x4F;
    public static int JAPAN_MATSUSHITA_ELECTRIC = 0x50;
    public static int JAPAN_FOSTEX = 0x51;

    public static int OTHER_FROM = 0x60;
    public static int OTHER_TO = 0x7C;

    public static int SPECIAL_FROM = 0x7D;
    public static int SPECIAL_TO = 0x7F;

    public static int SPECIAL_NONE_REALTIME = 0x7E;
    public static int SPECIAL_REALTME = 0x7F;

    public static int DEVICE_ALL_CALL = 0x7F;

    public static byte[] makeUniversalSysex(int id, int device, byte[] data) {
        byte[] sysex = new byte[data.length + 4];
        sysex[0] = (byte) 0xf0;
        sysex[1] = (byte) id;
        sysex[2] = (byte) device;
        for (int i = 0; i < data.length; ++i) {
            sysex[i + 3] = data[i];
        }
        sysex[sysex.length - 1] = (byte) 0xf7;
        return sysex;
    }

    public static final int HANDSHAKE_OK = 0x7f;
    public static final int HANDSHAKE_NOTOK = 0x7e;
    public static final int HANDSHAKE_CANCEL = 0x7d;
    public static final int HANDSHAKE_WAIT = 0x7c;
    public static final int HANDSHAKE_EOF = 0x7b;

    public static byte[] makeHandshake(int id, int device, int handshake, int packet) {
        byte[] sysex = {
            (byte) 0xf0,
            (byte) 0x7e,
            (byte) device,
            (byte) handshake,
            (byte) packet,
            (byte) 0xf7
        };
        return sysex;
    }

    public static String dumpHex(byte data) {
        String x = Integer.toHexString((int) data & 0xff);
        if (x.length() < 2) {
            x = "0" + x;
        }
        return x;
    }

    public static String dumpHexArray(byte[] data) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < data.length; ++i) {
            if (i != 0) {
                buf.append(", ");
            }
            buf.append(dumpHex(data[i]));
            if ((i % 16) == 15) {
                buf.append("\n  ");
            }
        }
        return buf.toString();
    }

    public static void main(String[] args) {
        byte[] pool = new byte[100];
        for (int i = 0; i < 100; ++i) {
            pool[i] = (byte) i;
        }
        byte[] sysex = makeUniversalSysex(JAPAN_FROM, 15, pool);
        System.out.println("from " + dumpHexArray(sysex));
        SysexSplitter splitter = new SysexSplitter();
        splitter.append(sysex);
        List<byte[]> splitted = splitter.splitOrJoin(20);
        for (byte[] seg : splitted) {
            System.out.println("split : " + dumpHexArray(seg));
        }

        compareSame(sysex, splitted, 0);
        compareSame(sysex, splitted, 1);
        compareSame(sysex, splitted, 2);
    }

    public static byte[] rebuildByPath(byte[] data, int rebuildType) {
        try {
            if (rebuildType == 1) {
                SysexMessage message = new SysexMessage(data, data.length);
                return message.getMessage();
            }
            if (rebuildType == 2) {
                SplittableSysexMessage message = new SplittableSysexMessage(data);
                byte[] data1 = message.getMessage();
                if (data1[0] == (byte)0xf0 || data1[0] == (byte)0xf7) {
                    return data1;
                }
                else {
                    //F7始まりのメッセージは先頭のF7を前の終端と間違えられるので、
                    //F7をスキップして送るようになっている
                    byte[] data2 = new byte[data1.length + 1];
                    for (int i = 0; i < data1.length; ++ i) {
                        data2[i + 1] = data1[i];
                    }
                    data2[0] = (byte)0xf7;
                    return data2;
                }
            }
        } catch (InvalidMidiDataException ex) {
            ex.printStackTrace();
            return new byte[] { (byte)0xff, (byte)0xff };
        }
        return data;
    }

    public static void compareSame(byte[] sysex, List<byte[]> splitted, int rebuildType) {
        SysexSplitter splitter = new SysexSplitter();
        for (byte[] seg : splitted) {
            seg = rebuildByPath(seg, rebuildType);
            splitter.append(seg);
        }
        List<byte[]> result = splitter.splitOrJoin(0);

        byte[] from = sysex;
        byte[] to = result.get(0);
        ArrayList<String> fail = new ArrayList<>();

        if (from.length != to.length) {
            fail.add("from.length = " + from.length + ", to.length = " + to.length);
        } else {
            for (int i = 0; i < from.length; ++i) {
                if (from[i] != to[i]) {
                    String fail1 = "from[" + i + "] = " + dumpHex(from[i]);
                    String fail2 = "to[" + i + "] = " + dumpHex(to[i]);
                    fail.add(fail1 + ", " + fail2);
                }
            }
        }

        if (fail.isEmpty()) {
            System.out.println("Match");
        } else {
            System.out.println("Fail");
            System.out.println(fail);
            System.out.println("reconnnect " + dumpHexArray(to));
        }
    }
}
```