はじめまして。
お会いできて光栄です。
手始めに、「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クラスのかわりに、以下を用いることで可能になります。
まず、使い方からです。
//送信する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);
}
}
この記事の基本の、
/*
* 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;
}
/*
* 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;
}
では、よいハッキングライフを!
#もし、エラーがありましたら、環境をそえてコメントいただけますと、幸いです。
動作テストは以下で行えます
/*
* 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));
}
}
}