LoginSignup
1
0

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

Last updated at Posted at 2024-03-22

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

手始めに、「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));
        }
    }
}
1
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
1
0