Skip to content

MIDI Protocol

Mark Green edited this page Jul 21, 2019 · 28 revisions

App Communication

App is a Unity engine program using standard OS MIDI interface. No data can be obtained from iOS App binary due to FairPlay DRM. However, Teenage Engineering's GitHub repo reflects a fork of the library keijiro/MidiJack which is likely used by the app.

The Mac app binary's decompiled C# code does not contain MIDI communication, but the inner file libSnakeLib.dylib can be decompiled with a disassembler, or C or Objective-C decompiler. (Big thanks to @Lrk for finding this.) The app appears to use the open-source-parsers/jsoncpp library at version 1.7.5.

App communicates with OP-Z via SysEx messages with common format:

F0 00 20 76 01 00 ....... F7

F0 is standard sysEx start; 00 20 76 is Teenage Engineering manufacturer code; 01 is presumably device ID. Following 00 is command/message number. F7 marks end of message; values between 00 and F7 vary based on message number.

While on "Device" screen, App repeatedly sends SysEx ID request messages F0 7E 7F 06 01 F7. If OP-Z replies, begins to send custom messages requesting updates. ID request/response handshake is not mandatory.

OP-Z's ID reply is extended: F0 7E 7F 06 02 00 20 76 01 58 33 43 4A 44 31 59 38 31 2E 31 2E 39 28 30 00 00 00 00 00 00 00 05 F7. Bytes 16-18 are ASCII of "serial number" displayed on app screen. Bytes 19-24 are ASCII of "version number".

7-bit encoding

To avoid clashes with the MIDI SysEx format, all values sent in SysEx messages are 7-bit. When messages requiring all 8 bits are sent, they are packed into chunks of 8 bytes or less. The first byte of each chunk holds the 8th bits of the following 7 bytes, with the LSB of the first byte being the MSB of the first data byte, the 2nd bit being the MSB of the second data byte, and so on. If there are less than 8 bytes to encode, the chunk may be shorter than 8 bytes but the first byte will still contain the MSBs of all following bytes in the same format.

The following Python code will undo the 7-bit packing:

def decode7bit(barray):
    chunks = [barray[x:x+8] for x in range(0, len(barray), 8)]
    out = bytearray()
    for chunk in chunks:
        extraBits = chunk[0]
        mask = 1
        for chunkIndex in range(1,len(chunk)):
            value = chunk[chunkIndex]
            if ((extraBits & mask) > 0):
                value += 128
            out.append(value)
            mask = mask << 1
    return out

ZLib compression

Certain messages sent by the OP-Z are compressed using zlib. These can be identified by the Zlib signature 78 9c at the beginning. However because of the 7-bit encoding above, the OP-Z transforms this to 78 1c with an appropriate MSB bit in the chunk.

Special messages 1 ($00-$02)

$00: Master Heartbeat

F0 00 20 76 01 00 03 2D 0E 05 F7 (prior to version 1.2.5)

F0 00 20 76 01 00 01 4E 2E 06 F7 (version 1.2.5)

This message must be sent regularly for any of the other SysEx messages to appear.

Sent roughly every ~1 second by app. Triggers a status response. SysEx status updates continue to be sent for ~1 second whenever data in that update changes, then stop until request refreshed. Parameters are unknown.

2D 0E can be different. Same bytes are sent back in the response 01 message (see below), although they are capped at 7F (7 bit max?). 03 can also be changed to 00-02. Any change to 05 makes message invalid.

On first 00 send in a session, a number of messages are sent in rapid succession, including current settings for all voices in 0E messages (see below).

$01: Universal response

F0 00 20 76 01 01 0C 15 55 2D 0E F7

Sent by OP-Z every time a 00 message is received. Significance currently unknown. Bytes 10/11 match bytes 8/9 of 00 message, but with 8th bit cleared if it was set.

$02: Track settings?

F0 00 20 76 01 02 00 0C 00 30 00 00 04 00 01 01 00 00 00 F7

8th byte appears to be a sequence number. 10th byte indicates which value was changed. 19th byte indicates what it was changed to.

State Sync messages ($03-$14)

$03: Keyboard setting

F0 00 20 76 01 03 00 02 05 F7

8th byte is selected octave, with octaves 2-7 represented by signed 7-byte values 7D to 02. 9th byte is selected track number.

Sending this message updates appropriate values.

$04: ?

F0 00 20 76 01 04 02 64 7F 05 00 F7

7th byte: 3rd bit set = microphone mode.

$06: Button states

F0 00 20 76 01 06 06 01 40 1F 1F 00 F7

8th byte 1st bit (hi) and 9th byte 7th bit (lo) indicate selected encoder mode: 00 = white, 01 = green, 10 = purple/blue, 11 = orange. 8th byte 2nd bit set = mixer button down. 8th byte 3rd bit set = record button held

10th byte: 5th bit unset = track/step button held, bits 1-4 give number of held track/step. 6th bit set = shift button held. 7th bit set = tempo button held

11th byte: 2nd bit set = screen button held. 3rd bit set = sequencer playing

12th byte: 1st bit set = track select held. 2nd bit set = program button held.

Sending these messages can update encoder mode and system modes. Sending messages indicating a button is held can "lock" OPZ into having this button held until another message is sent (pressing and releasing button does not release)

$07: Sequencer settings?

F0 00 20 76 01 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 F7

27th bit is number of selected program. Not sendable. If sent with different settings, OP-Z resends 07 message with current settings.

$08: Unknown Query

Never seems to be sent. Sending F0 00 20 76 01 08 F7 provokes an 09 response and also seems to retrigger held notes.

$09: Pattern

F0 00 20 76 01 09 00 0A 00 36 00 00 00 78 57 1C 6D 5D 6D 0C 54 57 6E 7D 47 71 7F 3E 41 30 1E 04 42 2E 40 29 61 31 40 6B 58 2F 06 1C 0C 54 59 6B 05 32 5D 5D 09 46 1E 36 06 60 5A 48 4E 62 4A 3B 72 26 45 31 2F 12 79 62 35 09 42 6A 16 3B 12 60 5C 64 5D 12 0A 6C 05 2B 45 16 25 02 4A 79 1E 53 29 35 09 04 10 2F 3F 4C 0B 36 72 0B 64 4D 1C 55 59 15 7C 22 72 54 75 54 46 15 25 10 52 0A 3D 26 35 27 77 5C 27 39 73 73 30 3B 77 1E 76 1E 37 3B 73 3D 12 7A 1D 3D 5D 77 7E 66 1E 39 67 6F 2B 5C 19 1D 5D 3F 27 45 38 64 59 57 1B 48 3D 59 75 79 49 58 1E 7B 0A 1C 53 0A 5A 59 79 43 68 2F 1A 74 5B 5F 68 6A 5C 2B 3B 5F 58 20 5F 09 7A 73 76 5E 3F 5B 56 5A 2E 09 03 7A 07 F7

Code indicates that this may be the data for a full pattern, compressed using zlib and sent in packets.

$0B: Unknown message

F0 00 20 76 01 0B 00 09 00 00 00 36 00 00 00 00 F7

Sent in response to an 09 message sent by the app.

$0C: Global Data

F0 00 20 76 01 0C 02 78 1C 63 61 60 60 64 00 64 62 62 06 02 16 20 24 60 65 7D 3F 0A 06 13 31 60 22 20 3F 71 54 6F 21 3F 74 06 28 58 40 40 10 18 34 30 3B 7D 07 52 1E 00 05 7A 71 4B F7

Sent in response to an 09 message sent by the app. Code indicates this may be global data compressed using zlib.

$0E: Sound preset

F0 00 20 76 01 0E 20 05 02 00 75 53 61 0D 10 2A 2B 31 39 25 73 00 13 6B 20 1B 68 00 7F

Byte Function
8 Selected track number
9 Engine parameter 1
10 Engine parameter 2
11 Attack
12 Decay
13 Sustain
14 Release
15 FX Send level 1
16 FX Send level 2
18 Filter
19 Resonance
20 Pan
21 Level
22 Portamendo
24 LFO Depth / Arp Speed
25 LFO Speed / Arp Pattern
26 LFO Value / Arp Style
27 LFO Shape / Arp Range

$0F: Unknown configurator command?

F0 00 20 76 01 0F F7

Sent during configurator process. Provokes a 10 response.

$10: ZLib Compressed MIDI config

F0 00 20 76 10 02 78 1C 63 64 44 05 0C 55 0C 4C 4C 2C 2C 6C 6C 2A 1C 1C 5C 5C 3C 3C 7C 79 7C 68 5C 01 11 46 1F 27 4E 40 40 00 00 63 0C 02 09 20 F7

Sent in response to 0F message. This appears to be a zlib compressed and 7-bit packed message. Decoded, the message sent here is 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 97 00 00 00. The repeating 16 sequences likely correspond to 16 tracks but the value of the sequential numbers is not clear.

$11: ?

F0 00 20 76 01 11 00 58 33 43 4A 44 31 59 00 38 00 31 2E 31 2E 31 00 37 2B 00 00 00 00 00 00 00 00 F7

Appears to contain ASCII serial number and firmware version number.

$12: Sound state?

F0 00 20 76 01 12 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 03 7F 7F 10 20 F7

Seems to sometimes be send when notes are played, and then to be sent again with 00/00 in the later positions when notes go silent (not necessarily when keys are released). But is not always sent?

File server messages $14-$60

$33: Unknown configurator probe

F0 00 20 76 01 33 35 53 21 7F 68 69 0A 4F F7

Sent repeatedly by OP-Z after 52 command, once a second, six times before giving up. Function unknown.

$34: Configurator data?

F0 00 20 76 01 34 1A 7F 68 69 0A 4F f7

OP-Z's response to 51 message below. Unknown.

$35: File request

F0 00 20 76 01 35 03 1E 69 00 00 00 00 00 00 00 F7

Seems to request a file to be sent in $53 packets. The bytes after the 35 seem to indicate the file. File is only sent if a file client heartbeat has also recently been sent, and the heartbeat must be maintained during file transfer or file sending will stop. 03 1E 69 may need to match bytes in the heartbeat. The large number seems to indicate the file to send.

  • 00 00 00 00 00 00 00 sends settings/slotConfiguration.json
  • 01 00 00 00 00 00 00 sends settings/plugs.json
  • 02 00 00 00 00 00 00 sends nothing, but code suggests it looks for a file called syncjob.json.
  • 03 00 00 00 00 00 00 sends project01.opz and similar up to $0c.

It is not clear if any other files can be sent by including different codes here. An invalid code will give no response.

$51: Configurator startup

F0 00 20 76 01 51 03 1E 69 F7

Sent by app when configurator is starting up, at regular intervals.

$52: File client heartbeat

F0 00 20 76 01 52 03 1E 69 00 F7 Sent by app during configurator operation.

$53: File data

F0 00 20 76 01 53 ....

Sends a packet of data. Everything after 53 and before the final F7 is file data in 7-bit encoded format. Once 7-bit decoding is applied, this will contain a 9 byte header, followed by zlib compressed file data. The 9 byte header is:

af 70 01 00 00 00 23 00 00 The seventh and eighth bytes are the sequence number of the packet (little endian). The nineth byte is 01 if this is the last packet of the file.

Special messages 2 ($60-$62)

$61: Add Gate Sequencer Step??

F0 00 20 76 01 61 .... Unknown functionality, but snakelib indicates a method called "addGateSequenceStep" is called.

$62: Text command

F0 00 20 76 01 62 ....

Is followed by a command in UTF-8 text, ending with F7 (with no carriage return, zero, or any other characters at the end). Known commands are:

  • enable-debug and disable-debug: enable and disable debug mode functionality based on pressing and holding SCREEN during usage.
  • enable-verbose-log and disable-verbose-log: may modify logging.
  • enable-controller-mode and disable-controller-mode: unknown.
  • enable-battery-log and disable-battery-log: unknown.
  • enable-uart-log and disable-uart-log: unknown.
  • debug-crash: sends the OP-Z directly to update mode with a "crash report" dropped in the root of the filesystem.
  • debug-save: unknown function.

$66: ??

Handler added in 1.2.5, but functionality not clear.

VideoLab

F0 00 20 76 03 xx yy F7

Documented in videolab MidiJack source code as indicating Videolab messages.