From 1c01effbd929c709116c92d7e1a2ba6c3c2068e2 Mon Sep 17 00:00:00 2001 From: Francois Best Date: Sat, 10 Mar 2018 16:58:17 +0100 Subject: [PATCH] feat: Handle Korg-style of flipping SysEx data header bits Closes #92 --- doc/sysex-codec.md | 87 ++++++ src/MIDI.cpp | 20 +- src/MIDI.h | 10 +- .../tests/unit-tests_SysExCodec.cpp | 258 +++++++++++------- 4 files changed, 267 insertions(+), 108 deletions(-) create mode 100644 doc/sysex-codec.md diff --git a/doc/sysex-codec.md b/doc/sysex-codec.md new file mode 100644 index 0000000..7d2dddd --- /dev/null +++ b/doc/sysex-codec.md @@ -0,0 +1,87 @@ +# SysEx Encoding & Decoding + +There are various ways of encoding & decoding arbitrary 8-bit wide data into +SysEx, which is 7-bit wide. + +The [official documentation](http://www.somascape.org/midi/tech/spec.html#nusx_fd) +for FileDump data exchanges states the following: + + > The 8-bit file data needs to be converted to 7-bit form, + > with the result that every 7 bytes of file data translates + > to 8 bytes in the MIDI stream. + > + > For each group of 7 bytes (of file data) the top bit from each + > is used to construct an eigth byte, which is sent first. + > So: + > ``` + > AAAAaaaa BBBBbbbb CCCCcccc DDDDdddd EEEEeeee FFFFffff GGGGgggg + > ``` + > becomes: + > ``` + > 0ABCDEFG 0AAAaaaa 0BBBbbbb 0CCCcccc 0DDDdddd 0EEEeeee 0FFFffff 0GGGgggg + > ``` + > + > The final group may have less than 7 bytes, and is coded as follows + > (e.g. with 3 bytes in the final group): + > ``` + > 0ABC0000 0AAAaaaa 0BBBbbbb 0CCCcccc + > ``` + +## SysEx encoding / decoding functions + +The MIDI library supplies two functions to do this, `encodeSysEx` and `decodeSysEx`. + +Example usage: +```c++ +#include + +static const byte myData[12] = { + // Hex dump: CAFEBABE BAADF00D FACADE42 + 0xca, 0xfe, 0xba, 0xbe, 0xba, 0xad, 0xf0, 0x0d, + 0xfa, 0xca, 0xde, 0x42 +}; + +byte encoded[16]; +const unsigned encodedSize = midi::encodeSysEx(myData, encoded, 12); +// Encoded hex dump: 07 4a 7e 3a 3e 3a 2d 70 07 0d 7a 4a 5e 42 + +byte decoded[12]; +const unsigned decoded = midi::decodeSysEx(encoded, decoded, encodedSize); +``` + +## Special case for Korg devices + +Korg apparently uses another convention for their SysEx encoding / decoding, +where: +``` +AAAAaaaa BBBBbbbb CCCCcccc DDDDdddd EEEEeeee FFFFffff GGGGgggg +``` +becomes: +``` +0GFEDCBA 0AAAaaaa 0BBBbbbb 0CCCcccc 0DDDdddd 0EEEeeee 0FFFffff 0GGGgggg +``` + +The order of the bits in the "header" byte is reversed. +To follow this beheaviour, set the inFlipHeaderBits argument to true. + +Example: +```c++ +void handleSysEx(byte* inData, unsigned inSize) +{ + // SysEx body data starts at 3rd byte: F0 42 aa bb cc dd F7 + // 42 being the hex value of the Korg SysEx ID. + const unsigned dataStartOffset = 2; + const unsigned encodedDataLength = inSize - 3; // Remove F0 42 & F7 + + // Create a large enough buffer where to decode the message + byte decodedData[64]; + + const unsigned decodedSize = decodeSysEx(inData + dataStartOffset, + decodedData, + encodedDataLength, + true); // flip header bits + // Do stuff with your message +} +``` + +See original discussion in issue #92. diff --git a/src/MIDI.cpp b/src/MIDI.cpp index 7761821..c5b8824 100644 --- a/src/MIDI.cpp +++ b/src/MIDI.cpp @@ -38,11 +38,15 @@ BEGIN_MIDI_NAMESPACE \param inData The data to encode. \param outSysEx The output buffer where to store the encoded message. \param inLength The lenght of the input buffer. + \param inFlipHeaderBits True for Korg and other who store MSB in reverse order \return The lenght of the encoded output buffer. @see decodeSysEx Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com */ -unsigned encodeSysEx(const byte* inData, byte* outSysEx, unsigned inLength) +unsigned encodeSysEx(const byte* inData, + byte* outSysEx, + unsigned inLength, + bool inFlipHeaderBits) { unsigned outLength = 0; // Num bytes in output array. byte count = 0; // Num 7bytes in a block. @@ -54,7 +58,7 @@ unsigned encodeSysEx(const byte* inData, byte* outSysEx, unsigned inLength) const byte msb = data >> 7; const byte body = data & 0x7f; - outSysEx[0] |= (msb << (6 - count)); + outSysEx[0] |= (msb << (inFlipHeaderBits ? count : (6 - count))); outSysEx[1 + count] = body; if (count++ == 6) @@ -75,11 +79,15 @@ unsigned encodeSysEx(const byte* inData, byte* outSysEx, unsigned inLength) \param inSysEx The SysEx data received from MIDI in. \param outData The output buffer where to store the decrypted message. \param inLength The lenght of the input buffer. + \param inFlipHeaderBits True for Korg and other who store MSB in reverse order \return The lenght of the output buffer. @see encodeSysEx @see getSysExArrayLength Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com */ -unsigned decodeSysEx(const byte* inSysEx, byte* outData, unsigned inLength) +unsigned decodeSysEx(const byte* inSysEx, + byte* outData, + unsigned inLength, + bool inFlipHeaderBits) { unsigned count = 0; byte msbStorage = 0; @@ -94,8 +102,10 @@ unsigned decodeSysEx(const byte* inSysEx, byte* outData, unsigned inLength) } else { - const byte body = inSysEx[i]; - const byte msb = ((msbStorage >> byteIndex--) & 1) << 7; + const byte body = inSysEx[i]; + const byte shift = inFlipHeaderBits ? 6 - byteIndex : byteIndex; + const byte msb = ((msbStorage >> shift) & 1) << 7; + byteIndex--; outData[count++] = msb | body; } } diff --git a/src/MIDI.h b/src/MIDI.h index 5fe356c..e00bf98 100644 --- a/src/MIDI.h +++ b/src/MIDI.h @@ -250,8 +250,14 @@ private: // ----------------------------------------------------------------------------- -unsigned encodeSysEx(const byte* inData, byte* outSysEx, unsigned inLenght); -unsigned decodeSysEx(const byte* inSysEx, byte* outData, unsigned inLenght); +unsigned encodeSysEx(const byte* inData, + byte* outSysEx, + unsigned inLenght, + bool inFlipHeaderBits = false); +unsigned decodeSysEx(const byte* inSysEx, + byte* outData, + unsigned inLenght, + bool inFlipHeaderBits = false); END_MIDI_NAMESPACE diff --git a/test/unit-tests/tests/unit-tests_SysExCodec.cpp b/test/unit-tests/tests/unit-tests_SysExCodec.cpp index e1b9428..ab544dc 100644 --- a/test/unit-tests/tests/unit-tests_SysExCodec.cpp +++ b/test/unit-tests/tests/unit-tests_SysExCodec.cpp @@ -11,115 +11,171 @@ BEGIN_UNNAMED_NAMESPACE using namespace testing; -TEST(SysExCodec, Encoder) +TEST(SysExCodec, EncoderAscii) { - // ASCII content - { - const byte input[] = "Hello, World!"; - byte buffer[16]; - memset(buffer, 0, 16 * sizeof(byte)); - const unsigned encodedSize = midi::encodeSysEx(input, buffer, 13); - EXPECT_EQ(encodedSize, unsigned(15)); - const byte expected[16] = { - 0, 'H', 'e', 'l', 'l', 'o', ',', ' ', - 0, 'W', 'o', 'r', 'l', 'd', '!', 0, - }; - EXPECT_THAT(buffer, Each(Le(0x7f))); // All elements are <= 127 - EXPECT_THAT(buffer, ContainerEq(expected)); - } - // Non-ASCII content - { - const byte input[] = { - 182, 236, 167, 177, 61, 91, 120, // 01111000 -> 120 - 107, 94, 209, 87, 94 // 000100xx -> 16 - }; - byte buffer[16]; - memset(buffer, 0, 16 * sizeof(byte)); - const unsigned encodedSize = midi::encodeSysEx(input, buffer, 12); - EXPECT_EQ(encodedSize, unsigned(14)); - const byte expected[16] = { - // MSB Data - 120, 54, 108, 39, 49, 61, 91, 120, - 16, 107, 94, 81, 87, 94, 0, 0, - }; - EXPECT_THAT(buffer, Each(Le(0x7f))); // All elements are <= 127 - EXPECT_THAT(buffer, ContainerEq(expected)); - } + const byte input[] = "Hello, World!"; + byte buffer[16]; + memset(buffer, 0, 16 * sizeof(byte)); + const unsigned encodedSize = midi::encodeSysEx(input, buffer, 13); + EXPECT_EQ(encodedSize, unsigned(15)); + const byte expected[16] = { + 0, 'H', 'e', 'l', 'l', 'o', ',', ' ', + 0, 'W', 'o', 'r', 'l', 'd', '!', 0, + }; + EXPECT_THAT(buffer, Each(Le(0x7f))); // All elements are <= 127 + EXPECT_THAT(buffer, ContainerEq(expected)); } -TEST(SysExCodec, Decoder) +TEST(SysExCodec, EncoderNonAscii) { - // ASCII content - { - const byte input[] = { - 0, 'H', 'e', 'l', 'l', 'o', ',', ' ', - 0, 'W', 'o', 'r', 'l', 'd', '!', - }; - byte buffer[16]; - memset(buffer, 0, 16 * sizeof(byte)); - const unsigned decodedSize = midi::decodeSysEx(input, buffer, 15); - EXPECT_EQ(decodedSize, unsigned(13)); - const byte expected[16] = { - 'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', - 'o', 'r', 'l', 'd', '!', 0, 0, 0, - }; - EXPECT_THAT(buffer, Each(Le(0x7f))); // All elements are <= 127 - EXPECT_THAT(buffer, ContainerEq(expected)); - } - // Non-ASCII content - { - const byte input[] = { - // MSB Data - 120, 54, 108, 39, 49, 61, 91, 120, - 16, 107, 94, 81, 87, 94, - }; - byte buffer[16]; - memset(buffer, 0, 16 * sizeof(byte)); - const unsigned encodedSize = midi::decodeSysEx(input, buffer, 14); - EXPECT_EQ(encodedSize, unsigned(12)); - const byte expected[16] = { - 182, 236, 167, 177, 61, 91, 120, - 107, 94, 209, 87, 94, 0, 0, - 0, 0, - }; - EXPECT_THAT(input, Each(Le(0x7f))); // All elements are <= 127 - EXPECT_THAT(buffer, ContainerEq(expected)); - } + const byte input[] = { + 182, 236, 167, 177, 61, 91, 120, // 01111000 -> 120 + 107, 94, 209, 87, 94 // 000100xx -> 16 + }; + byte buffer[16]; + memset(buffer, 0, 16 * sizeof(byte)); + const unsigned encodedSize = midi::encodeSysEx(input, buffer, 12); + EXPECT_EQ(encodedSize, unsigned(14)); + const byte expected[16] = { + // MSB Data + 120, 54, 108, 39, 49, 61, 91, 120, + 16, 107, 94, 81, 87, 94, 0, 0, + }; + EXPECT_THAT(buffer, Each(Le(0x7f))); // All elements are <= 127 + EXPECT_THAT(buffer, ContainerEq(expected)); } -TEST(SysExCodec, Codec) +TEST(SysExCodec, EncoderNonAsciiFlipHeader) { - // ASCII content - { - const byte input[] = "Hello, World!"; - byte buffer1[16]; - byte buffer2[16]; - memset(buffer1, 0, 16 * sizeof(byte)); - memset(buffer2, 0, 16 * sizeof(byte)); - const unsigned encodedSize = midi::encodeSysEx(input, buffer1, 13); - EXPECT_EQ(encodedSize, unsigned(15)); - const unsigned decodedSize = midi::decodeSysEx(buffer1, buffer2, encodedSize); - EXPECT_EQ(decodedSize, unsigned(13)); - EXPECT_STREQ(reinterpret_cast(buffer2), - reinterpret_cast(input)); - } + const byte input[] = { + 182, 236, 167, 177, 61, 91, 120, // 00011111 -> 15 + 107, 94, 209, 87, 94 // 0xx00100 -> 4 + }; + byte buffer[16]; + memset(buffer, 0, 16 * sizeof(byte)); + const unsigned encodedSize = midi::encodeSysEx(input, buffer, 12, true); + EXPECT_EQ(encodedSize, unsigned(14)); + const byte expected[16] = { + // MSB Data + 15, 54, 108, 39, 49, 61, 91, 120, + 4, 107, 94, 81, 87, 94, 0, 0, + }; + EXPECT_THAT(buffer, Each(Le(0x7f))); // All elements are <= 127 + EXPECT_THAT(buffer, ContainerEq(expected)); +} + +// ----------------------------------------------------------------------------- + +TEST(SysExCodec, DecoderAscii) +{ + const byte input[] = { + 0, 'H', 'e', 'l', 'l', 'o', ',', ' ', + 0, 'W', 'o', 'r', 'l', 'd', '!', + }; + byte buffer[16]; + memset(buffer, 0, 16 * sizeof(byte)); + const unsigned decodedSize = midi::decodeSysEx(input, buffer, 15); + EXPECT_EQ(decodedSize, unsigned(13)); + const byte expected[16] = { + 'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', + 'o', 'r', 'l', 'd', '!', 0, 0, 0, + }; + EXPECT_THAT(buffer, Each(Le(0x7f))); // All elements are <= 127 + EXPECT_THAT(buffer, ContainerEq(expected)); +} + // Non-ASCII content - { - const byte input[] = { - // MSB Data - 182, 236, 167, 177, 61, 91, 120, - 107, 94, 209, 87, 94 - }; - byte buffer1[14]; - byte buffer2[12]; - memset(buffer1, 0, 14 * sizeof(byte)); - memset(buffer2, 0, 12 * sizeof(byte)); - const unsigned encodedSize = midi::encodeSysEx(input, buffer1, 12); - EXPECT_EQ(encodedSize, unsigned(14)); - const unsigned decodedSize = midi::decodeSysEx(buffer1, buffer2, encodedSize); - EXPECT_EQ(decodedSize, unsigned(12)); - EXPECT_THAT(buffer2, ContainerEq(input)); - } +TEST(SysExCodec, DecoderNonAscii) +{ + const byte input[] = { + // MSB Data + 120, 54, 108, 39, 49, 61, 91, 120, + 16, 107, 94, 81, 87, 94, + }; + byte buffer[16]; + memset(buffer, 0, 16 * sizeof(byte)); + const unsigned encodedSize = midi::decodeSysEx(input, buffer, 14); + EXPECT_EQ(encodedSize, unsigned(12)); + const byte expected[16] = { + 182, 236, 167, 177, 61, 91, 120, + 107, 94, 209, 87, 94, 0, 0, + 0, 0, + }; + EXPECT_THAT(input, Each(Le(0x7f))); // All elements are <= 127 + EXPECT_THAT(buffer, ContainerEq(expected)); +} + +TEST(SysExCodec, DecoderNonAsciiFlipHeader) +{ + const byte input[] = { + // MSB Data + 15, 54, 108, 39, 49, 61, 91, 120, + 4, 107, 94, 81, 87, 94, + }; + byte buffer[16]; + memset(buffer, 0, 16 * sizeof(byte)); + const unsigned encodedSize = midi::decodeSysEx(input, buffer, 14, true); + EXPECT_EQ(encodedSize, unsigned(12)); + const byte expected[16] = { + 182, 236, 167, 177, 61, 91, 120, + 107, 94, 209, 87, 94, 0, 0, + 0, 0, + }; + EXPECT_THAT(input, Each(Le(0x7f))); // All elements are <= 127 + EXPECT_THAT(buffer, ContainerEq(expected)); +} + +// ----------------------------------------------------------------------------- + +TEST(SysExCodec, CodecAscii) +{ + const byte input[] = "Hello, World!"; + byte buffer1[16]; + byte buffer2[16]; + memset(buffer1, 0, 16 * sizeof(byte)); + memset(buffer2, 0, 16 * sizeof(byte)); + const unsigned encodedSize = midi::encodeSysEx(input, buffer1, 13); + EXPECT_EQ(encodedSize, unsigned(15)); + const unsigned decodedSize = midi::decodeSysEx(buffer1, buffer2, encodedSize); + EXPECT_EQ(decodedSize, unsigned(13)); + EXPECT_STREQ(reinterpret_cast(buffer2), + reinterpret_cast(input)); +} + +TEST(SysExCodec, CodecNonAscii) +{ + const byte input[] = { + // MSB Data + 182, 236, 167, 177, 61, 91, 120, + 107, 94, 209, 87, 94 + }; + byte buffer1[14]; + byte buffer2[12]; + memset(buffer1, 0, 14 * sizeof(byte)); + memset(buffer2, 0, 12 * sizeof(byte)); + const unsigned encodedSize = midi::encodeSysEx(input, buffer1, 12); + EXPECT_EQ(encodedSize, unsigned(14)); + const unsigned decodedSize = midi::decodeSysEx(buffer1, buffer2, encodedSize); + EXPECT_EQ(decodedSize, unsigned(12)); + EXPECT_THAT(buffer2, ContainerEq(input)); +} + +TEST(SysExCodec, CodecNonAsciiFlipHeader) +{ + const byte input[] = { + // MSB Data + 182, 236, 167, 177, 61, 91, 120, + 107, 94, 209, 87, 94 + }; + byte buffer1[14]; + byte buffer2[12]; + memset(buffer1, 0, 14 * sizeof(byte)); + memset(buffer2, 0, 12 * sizeof(byte)); + const unsigned encodedSize = midi::encodeSysEx(input, buffer1, 12, true); + EXPECT_EQ(encodedSize, unsigned(14)); + const unsigned decodedSize = midi::decodeSysEx(buffer1, buffer2, encodedSize, true); + EXPECT_EQ(decodedSize, unsigned(12)); + EXPECT_THAT(buffer2, ContainerEq(input)); } END_UNNAMED_NAMESPACE