Reverse engineering · Web · Audio
JamJum JP-1 Studio: reverse engineering a MIDI controller
I decoded the undocumented configuration protocol of a MIDI controller and built my own web app to edit it: pads, knobs, buttons, RGB color and effects.
- Year
- 2026
- Status
- Validated on hardware
- Role
- Reverse engineering & development · personal project
- Repository
- Repository coming soon
01 · Problem
The JamJum JP-1 (16 pads, 8 knobs, 6 transport buttons, USB and Bluetooth) can only be configured with a limited official app. The protocol is undocumented and the Holtek microcontroller is read-protected: its firmware cannot be dumped.
02 · Approach
I captured the USB-MIDI traffic with USBPcap and Wireshark, diffed dumps bit by bit and tested checksum hypotheses with brute force and RANSAC until I could reproduce every message of the official app byte for byte. Then I wrote JamJum Studio, a Web MIDI app that runs in Chrome.
03 · Result
Protocol decoded: the app reads the full dump of each preset on connect and writes pads, knobs, transport buttons and per-pad RGB color. All four checksums are solved and verified byte for byte against the official app (for color, 384 out of 384 blocks). It adds dynamic RGB effects, including a reactive “ripple” when you hit the pads.
04 · Stack
- Web MIDI API
- JavaScript
- USBPcap
- Wireshark / tshark
- Python
- Node.js (tests)
The protocol in one line
Every configuration message is a proprietary SysEx: F0 78 32 <3-byte command> … F7. The payload is not made of regular bytes: it is a 7-bit, LSB-first bitstream (the low 7 bits of each byte). Fields do not align to byte boundaries, so they had to be mapped bit by bit.
| Command | Direction | What it does |
|---|---|---|
45 00 00 00 40 7F |
PC → JP-1 | Opens the edit session (without it, writes are ignored) |
1D 0B … |
PC → JP-1 | Selects a preset and triggers its dump |
4D 0A 04 … |
PC → JP-1 | Writes the active pad bank |
51 4A 02 … |
PC → JP-1 | Writes the active knob bank |
49 02 03 … |
PC → JP-1 | Writes the transport buttons |
45 3A 03 … |
PC → JP-1 | RGB color of the 16 pads |
21 03 00 00 40 7F |
PC → JP-1 | Saves to flash |
The checksum that wasn’t a CRC
Every write block carries a checksum. The first hypothesis —a linear CRC— turned out to be wrong: it came from a solvability test with too few equations. All four checksums are modular sums from the same family:
def color_checksum(channels: list[int], pad_index: int) -> tuple[int, int]: """channels: the 48 8-bit values (16 pads × G, R, B).""" s = sum(channels) + pad_index # the missing piece: add the index chk = (2720 - 32 * s) % 8192 return chk & 0x7F, (chk >> 7) & 0x7F # bytes 64 and 65
def pads_checksum(pads: list[dict]) -> tuple[int, int]: s = sum(p["channel"] + 16 * p["type"] + p["note"] + p["min"] + p["max"] for p in pads) c = (510 - 2 * s) % 512 return c & 0x7F, (c >> 7) & 0x7F # bytes 76 and 77The buttons checksum is (8175 − 32·Σ) mod 8192 and the knobs one is (15456 − 32·Σ) mod 16384. For color, a “uniform” model fit the first captures by pure aliasing; a new capture with varied indices and brightness, plus a (K + w·Σ) mod M search over several definitions of Σ, finally cracked it.
What the app does
- Auto-load on connect: it infers the active preset and bank from the handshake and decodes the dump (including the color of all 3 banks, starting at bit 2517).
- Per-pad editor: type (note, CC, program change), channel, velocity range and color, with live preview.
- Writes like the official app: one block per change and an explicit save to flash.
- RGB effects: rainbow, chase, pulse, gradient, hit-reactive and “ripple” (rings expanding from the pad you hit), each with its own parameters.
Takeaways
- A hypothesis is worth nothing until it is verified on hardware. Models that fit on the computer failed on the device until validation was byte for byte.
- Test the layer people actually use. At first I validated the engine from the console, not the UI; the UI had a different bug.
- Well-designed captures save experiments. One capture with varied data solved what dozens of random trials could not.