「すーぱーおくてっと!」でユーザー音色を使用できるようにするときに一番悩んだポイントだったので記事にしておく。ユーザー音色のデータをどうやってMIDIで転送するか、という話です。
MIDIの仕様の説明は省きます、気になる人はMIDI1.0の規格書が全文無料で公開されているので読んでください。分かりやすいし、MIDIパーサの実装例もあるので大変良い資料です。
さて、MIDIは上位1bit(MSB)をステータス/データの判別に使用しているため値として使用できるのは7bitです。しかし転送したいデータは8bit/1バイトなので足りません。そこで次の二つの方法が一般的に考えられます。
- 7bitに収まるように転送するデータを工夫する
- バイナリデータを送りたいのでこれは無理
- ASCIIテキストであればできるような気もする(BASE64とか)
- 8bitのデータを分割して収める
- 今回はこっち
後者の方法を今回は取ります。ネットの海を眺めていると8bitデータを4bit/4bitに分割して送るスタイルが多いような気がしました。が、これは次の理由から使わないことにしました。
- データの量が増えすぎる
- 1音色のデータは最大1KBに達するため、単純にデータが2KBに膨れ上がり転送に時間がかかる
- Windowsは一度に送れるMIDIメッセージの長さが決まっており(1回あたり256バイト)あんまり長くなると良くない(気がする)
- 3bit分無駄になる
- ステータス1bit+データ4bit=5bitとなり空白の3bitが生まれる
- 通信ステータスとかの管理に使うという手もあるかもしれないが、3bitだと正直微妙
- ありきたりで面白くない
- (殴
- 昔このフォーマットで実装したことがある
そこで今回はもっと効率の良い(そして実装がそこまで面倒ではない)方法を考えることにしました。
最初に提示したMIDI1.0の規格書を眺めているとちょうどいいフォーマットを見つけました。例えば次のようなデータがあったとします。
AAAAaaaa BBBBbbbb CCCCcccc DDDDdddd EEEEeeee FFFFffff GGGGgggg(7bytes)
まず、先ほど挙げた4bit/4bit分割の場合は次のようになります。
0000AAAA 0000aaaa 0000BBBB 0000bbbb 0000CCCC 0000cccc 0000DDDD 0000dddd 0000EEEE 0000eeee 0000FFFF 0000ffff 0000GGGG 0000gggg(14bytes)
14bytesになりました。
次に元データをこのような方法で変換してみます。
- 最初のbyteに続く7bytesのsign-bit(MSB)を格納する
- 続く7bytesのデータは下位7bitのみを格納する
- データ長が7bytesの倍数でない場合にはsign-bitの格納部分に0埋めをする(データは0埋めしない)
すると次のようになります。
0ABCDEFG 0AAAaaaa 0BBBbbbb 0CCCcccc 0DDDdddd 0EEEeeee 0FFFffff 0GGGgggg(8bytes)
これによりデータ長は8bytesになり元データと比較して12%の増加で済みます。
大昔MIDIの規格書を穴が開くほど読んでいたころの微かな記憶をもとに思いつきましたが今見たら全く同じことが78ページに書かれていました。
これだけだと本当に規格書と書いていることが同じなので実装例を示しておきます。もっとうまく実装できると思うけどあくまで一例ってことで。チェックサムとかも入れていない、実際には入れたほうがいいかもしれない。
エンコード(8bit->7bit)
int encode(uint8_t* dest,uint8_t* src,size_t length){
int block = 0;
int counter = 0;
for (int i = 0; i < length; i++) {
if(counter % 7 == 0){
for(int j = 0;j < 7;j++){
if(src[counter + j] & 0x80){
dest[block * 8] |= (1 << (6 - j));
} else {
dest[block * 8] &= ~(1 << (6 - j));
}
}
block++;
}
dest[block + counter++] = src[i] & 0x7F;
}
return counter + block;
}
destには格納先の配列へのポインタ、srcは変換対象の配列へのポインタ、lengthは元データの長さ。ここで、destの配列長は十分な長さが事前に確保されていることが前提です。
なんか無限に実装うまくいかなくてしばらく頭を抱えていた。
デコード(7bit->8bit)
int decode(uint8_t* dest,uint8_t* src,size_t length){
int counter = 0;
for (int i = 0; i < length; i++) {
if (i % 8 == 0) {
/* Head */
for (int j = 0; j < 7; j++) {
if (src[i] & (1 << (6 - j))) {
dest[counter + j] = 0x80;
} else {
dest[counter + j] = 0x00;
}
}
} else {
dest[counter++] |= src[i];
}
}
return counter;
}
じつはデコードのほうが処理が簡単です。引数の意味はさっきと同じ。
サンプルコード
int main(void){
uint8_t original[] = {0x55,0xAA,0xFF,0x80,0x7F,0x55,0x52,0x34,0x7F};
uint8_t encoded[32];
uint8_t decoded[32];
printf("original Length:%d\n",sizeof(original));
printf("Original:");
for(int i = 0;i < sizeof(original);i++){
printf("%02X ",original[i]);
}
printf("\n");
int length = encode(encoded,original,sizeof(original));
printf("encoded Length:%d\n",length);
printf("Encoded :");
for(int i = 0;i < length;i++){
printf("%02X ",encoded[i]);
}
printf("\n");
length = decode(decoded,encoded,length);
printf("decoded Length:%d\n",length);
printf("Decoded :");
for(int i = 0;i < length;i++){
printf("%02X ",decoded[i]);
}
printf("\n");
return 0;
}
stdio.hとstdint.hをincludeする必要があります。単にエンコードしたデータをデコードしているだけ。
あとはこれをXGパラメータセットのデータ部分に乗せるだけで音色転送の処理は完了。バイナリデータが送れれば画像もサンプリング音声も送れるので夢が広がリング。特に画像はデータ量も多いので恩恵を受けやすいんじゃないかなと思います。
以上。