Alesis QS MIDI Sysex 数据转换

Posted

技术标签:

【中文标题】Alesis QS MIDI Sysex 数据转换【英文标题】:Alesis QS MIDI Sysex Data Conversion 【发布时间】:2008-12-01 05:11:25 【问题描述】:

我的目标是将来自 Alesis 合成器的字节码流转换为人类可读的格式。我需要能够获取“程序转储”并读取组成补丁名称的 10 个字符串。

为了从合成器接收“程序转储”,我通过 MIDI-OX 向合成器发送了以下命令:

F0 00 00 0E 0E 01 73 F7

我要求它向我发送程序 73 的转储。

我收到了这个:

F0 00 00 0E 0E 00 73 00 60 24 0B 27 27 01 64 1E 19 19 05 23 19 1E 2A 41 0D 23 46 19 1E 06 00 47 0D 23 30 6C 18 63 30 6C 18 40 3F 0A 67 1B 16 20 40 00 60 18 00 18 06 05 0C 2B 41 13 70 05 30 40 31 63 70 05 00 40 31 63 70 05 00 40 31 63 00 4C 2A 51 00 46 7F 78 18 40 0F 40 31 40 31 04 30 0C 00 30 6C 03 30 3C 0F 00 00 05 0A 0F 14 19 1E 23 28 2D 72 00 76 34 3C 54 42 19 46 0C 33 3C 0C 00 0E 1B 46 60 58 31 46 61 58 31 00 7F 14 4E 37 6C 74 13 00 40 31 00 30 0C 0A 18 56 02 27 60 0B 60 00 63 46 61 0B 00 00 63 46 61 0B 00 00 63 46 01 18 55 22 01 0C 7F 71 31 00 1F 00 63 00 63 08 60 18 00 60 58 07 60 18 1E 00 00 0A 14 1E 28 32 3C 46 50 5A 64 01 0C 2D 15 29 05 36 0C 19 66 78 18 00 1C 36 0C 41 31 63 0C 43 31 63 00 7E 29 1C 6F 58 00 01 02 00 63 00 60 18 14 30 2C 05 4E 40 17 40 01 46 0D 43 17 00 00 46 0D 43 17 00 00 46 0D 03 30 2A 45 02 18 7E 63 63 00 3E 00 46 01 46 11 40 31 00 40 31 0F 40 71 3D 00 00 14 28 3C 50 64 78 0C 21 35 49 03 58 4C 71 31 1C 6C 18 32 4C 71 31 00 38 6C 18 02 63 46 19 06 63 46 01 7C 53 00 60 18 53 37 6C 70 0D 03 40 31 28 60 58 0A 1C 01 2F 00 03 0C 1B 06 2F 00 00 0C 1B 06 2F 00 00 0C 1B 06 60 54 0A 05 30 7C 47 47 01 7C 00 0C 03 0C 23 00 63 00 00 63 1E 3C 63 18 00 00 28 50 78 20 49 71 19 42 6A 12 07 F7 

MIDI-OX 告诉我它收到了 408 个字节。

这符合规范:

"单个程序转储发送400个数据字节,对应350个 字节的程序数据。与标头一起传输的总字节数 程序转储是 408。程序转储中每个参数的位置是 在下一节中显示。”

“程序转储”应由以下值组成:

F0 00 00 0E 0E 00 <program#> <data> F7

这意味着数据应该以“00 60”开头并以“07 F7”结尾。

现在我应该能够将这 400 字节转换为该程序的“350 字节的打包参数数据”。按照“程序数据格式”,程序名称的 10 位数字应位于打包数据中的某处。补丁 73 被称为“BlowDeTune”或“PanBristle”,不完全确定它是从 0 开始还是从 1 开始。

那么你如何进行转换呢?规范的第一页给出了传输格式,但是我不明白怎么解压。

谁能帮忙?

Alesis QS MIDI Sysex 规范在这里:

http://www.midiworld.com/quadrasynth/qs_swlib/qs678r.pdf

MIDI-OX 可以在这里找到:

http://www.midiox.com/

【问题讨论】:

【参考方案1】:

你很幸运,因为几年前我玩过 Midi(用我的 Atari ST 520),所以我对这个话题有足够的兴趣来调查一下......

为了记录,我找到了MIDI System Exclusive Message 格式,根据您为合成器提供的参考。 我一开始以为打包算法在this page中有描述,但是在实现了它的解码和创建垃圾之后,我发现我错了......我会给出这个代码以防万一它在其他地方对你有用......

第一次尝试很有用,因为当我重新阅读 PDF 文件中的规范时,我了解到 A7 到 G0 符号实际上是位...

数据被“打包”,因为 Midi 非控制字必须是 7 位干净的(高位始终未设置)。 他们获取 7 字节的原始数据,将其视为 56 位的大字,然后将该字每 7 位拆分一次,使高位始终为 0:

原始数据(使用不同的符号):

0 - b07 b06 b05 b04 b03 b02 b01 b00
1 - b17 b16 b15 b14 b13 b12 b11 b10
2 - b27 b26 b25 b24 b23 b22 b21 b20
3 - b37 b36 b35 b34 b33 b32 b31 b30
4 - b47 b46 b45 b44 b43 b42 b41 b40
5 - b57 b56 b55 b54 b53 b52 b51 b50
6 - b67 b66 b65 b64 b63 b62 b61 b60

传输/编码数据:

0 -  0  b06 b05 b04 b03 b02 b01 b00
1 -  0  b15 b14 b13 b12 b11 b10 b07
2 -  0  b24 b23 b22 b21 b20 b17 b16
3 -  0  b33 b32 b31 b30 b27 b26 b25
4 -  0  b42 b41 b40 b37 b36 b35 b34
5 -  0  b51 b50 b47 b46 b45 b44 b43
6 -  0  b60 b57 b56 b55 b54 b53 b52
7 -  0  b67 b66 b65 b64 b63 b62 b61

所以我们有:

0 - 00000000 0x00
1 - 01100000 0x60
2 - 00100100 0x24
3 - 00001011 0x0B
4 - 00100111 0x27
5 - 00100111 0x27
6 - 00000001 0x01
7 - 01100100 0x64

0 - 00011110 0x1E
1 - 00011001 0x19
2 - 00011001 0x19
3 - 00000101 0x05
4 - 00100011 0x23
5 - 00011001 0x19
6 - 00011110 0x1E
7 - 00101010 0x2A

一旦解码,我们应该有:

0 - 00000000 0x00
1 - 00110000 0x30
2 - 01101001 0x69
3 - 01110001 0x71
4 - 00111010 0x3A
5 - 00000101 0x05
6 - 11001000 0xC8

0 - 10011110 0x9E
1 - 01001100 0x4C
2 - 10100110 0xA6
3 - 00110000 0x30
4 - 11001010 0xCA
5 - 01111000 0x78
6 - 01010100 0x54

我相信我正确解码了数据,但仍然有垃圾(即不可读的字符串)... 也许你会在我的代码中看到一个逻辑错误,这可能是一个起点。

我看到 MIDI-OX 可以用 WSH 编写脚本,所以我编写了一个用 WSH 运行的 JS 脚本,并在控制台上输出:

var midiData =
[
  0xF0, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x73,
  0x00, 0x60, 0x24, 0x0B, 0x27, 0x27, 0x01, 0x64, 0x1E, 0x19, 0x19, 0x05, 0x23, 0x19, 0x1E, 0x2A,
  0x41, 0x0D, 0x23, 0x46, 0x19, 0x1E, 0x06, 0x00, 0x47, 0x0D, 0x23, 0x30, 0x6C, 0x18, 0x63, 0x30,
  0x6C, 0x18, 0x40, 0x3F, 0x0A, 0x67, 0x1B, 0x16, 0x20, 0x40, 0x00, 0x60, 0x18, 0x00, 0x18, 0x06,
  0x05, 0x0C, 0x2B, 0x41, 0x13, 0x70, 0x05, 0x30, 0x40, 0x31, 0x63, 0x70, 0x05, 0x00, 0x40, 0x31,
  0x63, 0x70, 0x05, 0x00, 0x40, 0x31, 0x63, 0x00, 0x4C, 0x2A, 0x51, 0x00, 0x46, 0x7F, 0x78, 0x18,
  0x40, 0x0F, 0x40, 0x31, 0x40, 0x31, 0x04, 0x30, 0x0C, 0x00, 0x30, 0x6C, 0x03, 0x30, 0x3C, 0x0F,
  0x00, 0x00, 0x05, 0x0A, 0x0F, 0x14, 0x19, 0x1E, 0x23, 0x28, 0x2D, 0x72, 0x00, 0x76, 0x34, 0x3C,
  0x54, 0x42, 0x19, 0x46, 0x0C, 0x33, 0x3C, 0x0C, 0x00, 0x0E, 0x1B, 0x46, 0x60, 0x58, 0x31, 0x46,
  0x61, 0x58, 0x31, 0x00, 0x7F, 0x14, 0x4E, 0x37, 0x6C, 0x74, 0x13, 0x00, 0x40, 0x31, 0x00, 0x30,
  0x0C, 0x0A, 0x18, 0x56, 0x02, 0x27, 0x60, 0x0B, 0x60, 0x00, 0x63, 0x46, 0x61, 0x0B, 0x00, 0x00,
  0x63, 0x46, 0x61, 0x0B, 0x00, 0x00, 0x63, 0x46, 0x01, 0x18, 0x55, 0x22, 0x01, 0x0C, 0x7F, 0x71,
  0x31, 0x00, 0x1F, 0x00, 0x63, 0x00, 0x63, 0x08, 0x60, 0x18, 0x00, 0x60, 0x58, 0x07, 0x60, 0x18,
  0x1E, 0x00, 0x00, 0x0A, 0x14, 0x1E, 0x28, 0x32, 0x3C, 0x46, 0x50, 0x5A, 0x64, 0x01, 0x0C, 0x2D,
  0x15, 0x29, 0x05, 0x36, 0x0C, 0x19, 0x66, 0x78, 0x18, 0x00, 0x1C, 0x36, 0x0C, 0x41, 0x31, 0x63,
  0x0C, 0x43, 0x31, 0x63, 0x00, 0x7E, 0x29, 0x1C, 0x6F, 0x58, 0x00, 0x01, 0x02, 0x00, 0x63, 0x00,
  0x60, 0x18, 0x14, 0x30, 0x2C, 0x05, 0x4E, 0x40, 0x17, 0x40, 0x01, 0x46, 0x0D, 0x43, 0x17, 0x00,
  0x00, 0x46, 0x0D, 0x43, 0x17, 0x00, 0x00, 0x46, 0x0D, 0x03, 0x30, 0x2A, 0x45, 0x02, 0x18, 0x7E,
  0x63, 0x63, 0x00, 0x3E, 0x00, 0x46, 0x01, 0x46, 0x11, 0x40, 0x31, 0x00, 0x40, 0x31, 0x0F, 0x40,
  0x71, 0x3D, 0x00, 0x00, 0x14, 0x28, 0x3C, 0x50, 0x64, 0x78, 0x0C, 0x21, 0x35, 0x49, 0x03, 0x58,
  0x4C, 0x71, 0x31, 0x1C, 0x6C, 0x18, 0x32, 0x4C, 0x71, 0x31, 0x00, 0x38, 0x6C, 0x18, 0x02, 0x63,
  0x46, 0x19, 0x06, 0x63, 0x46, 0x01, 0x7C, 0x53, 0x00, 0x60, 0x18, 0x53, 0x37, 0x6C, 0x70, 0x0D,
  0x03, 0x40, 0x31, 0x28, 0x60, 0x58, 0x0A, 0x1C, 0x01, 0x2F, 0x00, 0x03, 0x0C, 0x1B, 0x06, 0x2F,
  0x00, 0x00, 0x0C, 0x1B, 0x06, 0x2F, 0x00, 0x00, 0x0C, 0x1B, 0x06, 0x60, 0x54, 0x0A, 0x05, 0x30,
  0x7C, 0x47, 0x47, 0x01, 0x7C, 0x00, 0x0C, 0x03, 0x0C, 0x23, 0x00, 0x63, 0x00, 0x00, 0x63, 0x1E,
  0x3C, 0x63, 0x18, 0x00, 0x00, 0x28, 0x50, 0x78, 0x20, 0x49, 0x71, 0x19, 0x42, 0x6A, 0x12, 0x07,
  0xF7
];

// Show original data
DumpData(midiData, 16);

var headerLength = 7; // Bytes to skip
var resultData = new Array();
var decodedByteCount = 0;  // Number of expanded bytes in result

var cumulator = 0;
var bitCount = 0;
for (var i = headerLength; // Skip header
    i < midiData.length - 1; // Omit EOF
    i++)

  var rank = (i - headerLength) % 8; // We split the data in runs of 8 bytes
  // We cumulate the bits of these runs (less the high bit) to make a big word of 56 bits
/*
  cumulator |= midiData[i] << (7 * rank);
  if (rank == 7)  // End of the run
  
    // Split the cumulator in 7 bytes
    for (var j = 0; j < 7; j++)
    
      var shift = 8 * j;
      var byte = (cumulator & (0xFF << shift)) >> shift;
      WScript.StdOut.Write(ByteToHex(byte) + ' ');
      resultData[decodedByteCount++] = byte;
    
    cumulator = 0;  // Reset the buffer
  
*/
  // Actually, we cannot do that, because JS' bit arithmetic seems to be limited to signed 32 bits!
  // So I get the bytes out as soon as they are complete.
  // Somehow, it is more elegant anyway (but reflects less the original algorithm).
  cumulator |= midiData[i] << bitCount;
  bitCount += 7;
//~   WScript.StdOut.Write((i - 7) + ':' + ByteToHex(midiData[i]) + ' (' + bitCount + ') ' + DecimalToHex(cumulator) + '\n');
  if (bitCount >= 8)
  
    var byte = cumulator & 0xFF;
    bitCount -= 8;
    cumulator >>= 8;
    resultData[decodedByteCount++] = byte;
//~     WScript.StdOut.Write((i - 7) + ':' + ByteToHex(midiData[i]) + ' (' + bitCount + ') ' + DecimalToHex(cumulator) + ' > '  + ByteToHex(byte) + '\n');
  

DumpData(resultData, 14);

实用程序:

function DumpData(data, lineLength)

  WScript.StdOut.Write("Found " + data.length + " bytes\n");
  var txt = '';
  for (var i = 0; i < data.length; i++)
  
    var rd = data[i];
    if (rd > 31)
    
      txt += String.fromCharCode(rd);
    
    else
    
      txt += '.';
    
    WScript.StdOut.Write(ByteToHex(rd) + ' ');
    if ((i+1) % lineLength == 0)
    
      WScript.StdOut.Write(' ' + txt + '\n');
      txt = '';
    
  
  WScript.StdOut.Write(' ' + txt + '\n');


function NibbleToHex(halfByte)

  return String.fromCharCode(halfByte < 10 ?
      halfByte + 48 : // 0 to 9
      halfByte + 55); // A to F


function ByteToHex(dec)

  var h = (dec & 0xF0) >> 4;
  var l = dec & 0x0F;
  return NibbleToHex(h) + NibbleToHex(l);


function DecimalToHex(dec)

  var result = '';
  do
  
    result = ByteToHex(dec & 0xFF) + result;
    dec >>= 8;
   while (dec > 0);
  return result;

输出:

Found 350 bytes
00 30 69 71 3A 05 C8 9E 4C A6 30 CA 78 54  .0iq:.ÈL¦0ÊxT
C1 C6 C8 98 F1 18 00 C7 C6 08 C6 C6 8C 61  ÁÆÈñ..ÇÆ.ÆÆa
6C 0C F0 A7 38 6F 2C 20 20 00 8C 01 60 0C  l.ð§8o,  ..`.
05 C6 2A 38 81 17 60 C0 D8 18 5E 00 00 63  .Æ*8.`ÀØ.^..c
63 78 01 00 8C 8D 01 4C 55 14 60 FC E3 31  cx...LU.`üã1
C0 07 30 06 8C 11 60 0C 00 8C 3D 80 F1 1E  À.0..`..=ñ.
00 40 41 F1 A0 64 3C 23 54 4B 0E B0 D3 78  .@Añ d<#TK.°Óx
54 61 C6 C8 98 F1 18 00 C7 C6 08 C6 C6 8C  TaÆÈñ..ÇÆ.ÆÆ
61 6C 0C F0 A7 38 6F 6C FA 04 00 8C 01 60  al.ð§8olú...`
0C 05 C6 2A 38 81 17 60 C0 D8 18 5E 00 00  ..Æ*8.`ÀØ.^..
63 63 78 01 00 8C 8D 01 4C 55 14 60 FC E3  ccx...LU.`üã
31 C0 07 30 06 8C 11 60 0C 00 8C 3D 80 31  1À.0..`..=1
1E 00 40 41 F1 A0 64 3C 23 54 4B 0E 30 5A  ..@Añ d<#TK.0Z
95 54 C1 C6 C8 98 F1 18 00 C7 C6 08 C6 C6  TÁÆÈñ..ÇÆ.ÆÆ
8C 61 6C 0C F0 A7 38 6F 2C 20 20 00 8C 01  al.ð§8o,  ..
60 0C 05 C6 2A 38 81 17 60 C0 D8 18 5E 00  `..Æ*8.`ÀØ.^.
00 63 63 78 01 00 8C 8D 01 4C 55 14 60 FC  .ccx...LU.`ü
E3 31 C0 07 30 06 8C 11 60 0C 00 8C 3D 80  ã1À.0..`..=
F1 1E 00 40 41 F1 A0 64 3C 23 54 4B 0E B0  ñ..@Añ d<#TK.°
CC 78 8C C3 C6 C8 98 F1 18 00 C7 C6 08 C6  ÌxÃÆÈñ..ÇÆ.Æ
C6 8C 61 6C 0C F0 A7 00 30 66 7A 63 C3 1B  Æal.ð§.0fzcÃ.
03 60 0C 05 C6 2A 38 81 17 60 C0 D8 18 5E  .`..Æ*8.`ÀØ.^
00 00 63 63 78 01 00 8C 8D 01 4C 55 14 60  ..ccx...LU.`
FC E3 31 C0 07 30 06 8C 11 60 0C 00 8C 3D  üã1À.0..`..=
BC 31 06 00 40 41 F1 A0 64 3C 23 54 4B 0E  ¼1..@Añ d<#TK.

以防万一,另一种解包算法:

// Here the 8 bits of 7 bytes of raw data are coded as 7 bytes of data stripped off of the high bit,
// while the stripped bits are grouped in the first byte of the data run.
// In other words, when we have a run of 8 bytes, the first one groups the high bits of the 7 next bytes.
// Information found at http://crystal.apana.org.au/ghansper/midi_introduction/file_dump.html

var headerLength = 7;
var resultData = new Array();
var decodedByteCount = 0;  // Number of expanded bytes in result
var runCount = -1; // Number of runs in the encoded data
for (var i = headerLength; // Skip header
    i < midiData.length - 1; // Omit EOF
    i++)

  var rank = (i - headerLength) % 8; // We split the data in runs of 8 bytes
  if (rank == 0)  // Start of the run
  
    // Get the high bits
    var highBits = midiData[i];
    runCount++;
//~     WScript.StdOut.Write(runCount + ' > ' + (i - 7) + ' >> ' + ByteToHex(highBits) + '\n');
  
  else
  
    resultData[decodedByteCount++] = midiData[i] |
        ((highBits & (1 << (7 - rank))) << rank);
//~     WScript.StdOut.Write((i - 7) + ' >> ' +  ByteToHex(midiData[i]) + ' > ' +
//~         ByteToHex(midiData[i] | ((highBits & (1 << (7 - rank))) << rank)) + '\n');
  

【讨论】:

扩展算法效果很好,但我无法弄清楚“打包”算法。你是怎么做到的? 7年后,我不记得了...但是从我自己的cmets来看,显然我没有成功正确实现它...我给出了错误的代码让人们修补它并修复它... :-)【参考方案2】:

感谢您的出色工作,我提出了这个打包算法。 似乎 Alesis 使用与 Moog Voyager 相同的架构。

packSysex : function(midiData) 
    var header = [0xF0, 0x04, 0x01, 0x00, 0x03, 0x00]; //Voyager Single Preset Dump.

    var resultData = new Array();
    var packedByteCount = 0;
    var bitCount = 0;

    var thisByte;
    var packedByte;
    var nextByte = 0x0;


    for (var i = 0; i <= midiData.length; i++)
    
        thisByte = midiData[i];
        packedByte = ((thisByte << bitCount) | nextByte) & 0x7F;
        nextByte = midiData[i] >> (7-bitCount);

        resultData[packedByteCount++] = packedByte;

        bitCount++;
        if(bitCount >= 7) 
            bitCount = 0;

            //Fill last byte
            packedByte = nextByte & 0x7F;
            resultData[packedByteCount++] = packedByte;
            nextByte = 0x0;
        
    

    resultData[packedByteCount++] = 0xF7;
    resultData = header.concat(resultData);

    return resultData;
,

【讨论】:

以上是关于Alesis QS MIDI Sysex 数据转换的主要内容,如果未能解决你的问题,请参考以下文章

如何从音量级别创建 MIDI Sysex Master Volume 消息?

MIDI 蓝牙 LE、SYSEX 消息不完整

发送和接收 Java Midi Sysex 消息

使用 Arduino 发送 MIDI SysEx 消息?

Audiokit 似乎只接收前三个 sysex MIDI 消息

如何检测 MidiInProc 中丢失的 SysEx 数据?