DEV Community

Cover image for You Don't Need MIDI-OX Anymore: Building a SysEx Librarian with the Web MIDI API
knobmonster
knobmonster

Posted on

You Don't Need MIDI-OX Anymore: Building a SysEx Librarian with the Web MIDI API

You Don't Need MIDI-OX Anymore: Building a SysEx Librarian with the Web MIDI API

If you own a Yamaha DX7 or a Roland Juno-106, you already know the ritual.

Install a utility from 2003. Fight USB-MIDI drivers on Windows 11. Open MIDI-OX. Configure ports. Pray the SysEx buffer doesn't choke mid-dump. Save a .syx file with a name like backup_final_v3_REAL.syx. Lose it anyway.

I built knob.monster to kill that ritual. It's a browser-native cloud librarian: plug in your synth, capture a bank, see patch names, restore with one click. No Electron. No installers. No registry edits.

This post is the technical story: Web MIDI in, SysEx bytes through the wire, names out of the payload, bytes back to the synth.


The problem is not MIDI. It's desktop software.

Vintage synths speak System Exclusive (SysEx). Bulk dumps are just long binary messages wrapped in F0 ... F7. The hardware side hasn't changed since the 80s.

What has changed is everything around it:

  • Modern OSes don't ship MIDI utilities
  • Abandonware editors break on new machines
  • Driver stacks for cheap USB-MIDI cables are flaky
  • Cloud backup means "a folder I hope still exists"

The Web MIDI API has been in Chromium browsers for years. Almost nobody used it for real librarian workflows until recently. That's the gap.


Architecture in one sentence

Browser ↔ Web MIDI ↔ USB interface ↔ synth. Server stores raw hex + parsed names. Restore streams hex back out the same port.

┌─────────────┐    requestMIDIAccess({ sysex: true })    ┌──────────────┐
│   Chrome    │ ◄──────────────────────────────────────► │  USB-MIDI    │
│  knob.monster│    Uint8Array SysEx streams             │  interface   │
└──────┬──────┘                                          └──────┬───────┘
       │ POST sysex_hex + synth_model                           │
       ▼                                                        ▼
┌─────────────┐                                          ┌──────────────┐
│   Server    │  parse_dx7_sysex() / parse_juno106()     │  DX7 / Juno │
│  + Postgres │  → ["BASS 1", "BRASS 2", ...]            │  / M1 / CZ   │
└─────────────┘                                          └──────────────┘
Enter fullscreen mode Exit fullscreen mode

Step 1: Ask for SysEx permission (not optional)

Default Web MIDI access is useless for librarians. You need sysex: true or the browser strips exclusive messages.

const midiAccess = await navigator.requestMIDIAccess({ sysex: true });

for (const input of midiAccess.inputs.values()) {
  input.onmidimessage = (event) => {
    ingestSysExBytes(new Uint8Array(event.data));
  };
}
Enter fullscreen mode Exit fullscreen mode

Reality check: Works in Chrome, Edge, and Opera. Safari and Firefox users are out of luck for now. We say that upfront on the landing page. Hiding it creates angry forum posts.


Step 2: Request a dump (every synth is a special snowflake)

There is no universal "please send all patches" command. Each manufacturer has its own handshake.

For a Yamaha DX7, we send a bulk dump request:

const dumpRequest = new Uint8Array([0xF0, 0x43, 0x20, 0x09, 0xF7]);
outputPort.send(dumpRequest);
Enter fullscreen mode Exit fullscreen mode

Korg M1, Roland Jupiter-6, and Casio CZ-101 each get different byte sequences. The Juno-106 doesn't respond to an outbound request at all: the user must press WRITE on the panel while we're listening. The UI has to say that out loud or support mail explodes.

We concatenate incoming messages into one buffer, watch for idle timeout, then POST the hex string to the server.


Step 3: Reassemble the stream

SysEx often arrives as multiple MIDIMessage events. A single bulk dump can be thousands of bytes.

let ingestedBytes = [];

function ingestSysExBytes(data) {
  for (const byte of data) {
    ingestedBytes.push(byte);
  }
}

function finalizeDump() {
  const hexString = ingestedBytes
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");

  return fetch("/banks", {
    method: "POST",
    body: formDataWith(hexString),
  });
}
Enter fullscreen mode Exit fullscreen mode

We log the stream in a monospace "MIDI monitor" panel so power users can sanity-check the handshake. Transparency builds trust with synth people who've been burned by black-box tools before.


Step 4: Index extraction (where the magic is)

Raw SysEx is opaque. Users want names: EP BASS, UNI VERS, A11 ACID BASS.

DX7: names live at a fixed offset

A DX7 32-voice bulk dump is ~4104 bytes. Each voice is 128 bytes. The patch name is the last 10 bytes of each voice structure.

Python on the server (same logic you'd use in JS):

def clean_ascii(name_bytes: bytes) -> str:
    chars = []
    for b in name_bytes:
        if 32 <= b <= 126:
            chars.append(chr(b))
        else:
            chars.append(" ")
    return "".join(chars).strip()


def parse_dx7_sysex(data: bytes) -> list[str]:
    voices = []
    # Find F0 43 .. 09 header or accept raw 4096-byte voice block
    start_offset = 6 if data[:4] == b"\xF0\x43\x00\x09" else 0

    for i in range(32):
        voice_offset = start_offset + (i * 128)
        name_offset = voice_offset + 118
        name_bytes = data[name_offset : name_offset + 10]
        patch_name = clean_ascii(name_bytes) or f"DX7 Voice {i + 1:02d}"
        voices.append(patch_name)

    return voices
Enter fullscreen mode Exit fullscreen mode

That + 118 line is the entire product demo. Hex editor on the left, readable names on the right.

Juno-106: no stored names, infer from sliders

Juno patches don't ship ASCII labels in the dump. We parse 18 parameter bytes per message and generate descriptors like A11 ACID BASS from cutoff, resonance, and envelope shape. Different problem, same goal: human-readable rows in a table.

Generic fallback: scan for printable ASCII runs

Unknown synth? Walk the byte stream, collect 6–16 character printable sequences, filter junk. Surprisingly effective on 80s ROMs that embed names inline.


Step 5: Single-click recall (flash the payload back)

Backup is half the job. Restore means sending the same bytes back through the output port, often with pacing so 1980s UART buffers don't overflow.

async function flashBank(sysexHex) {
  const bytes = hexToUint8Array(sysexHex);
  outputPort.send(bytes); // chunk + delay in production
}
Enter fullscreen mode Exit fullscreen mode

We chunk large transfers and show progress. Users named this "Sync to Synth" in usability tests. Engineers call it "write SysEx." Same thing.


What we deliberately did NOT build

  • A patch editor. No virtual sliders. We're a librarian, not a programmer.
  • A DAW plugin. Browser tab, not VST.
  • Universal synth support on day one. DX7, Juno-106, M1, Jupiter-6, CZ-101, then generic scan. Depth beats breadth.

Saying "no" on the landing page filtered out the wrong customers and reduced support load.


Security and privacy (brief, because people ask)

  • Libraries are private by default
  • We store raw .syx equivalent hex for restore fidelity
  • Parsing extracts names for the UI; we don't need to expose your dumps publicly
  • requestMIDIAccess is a powerful permission. We only ask when you start a capture

Business model (for the curious)

Lifetime pricing, not subscription. Personal tier for bedroom studios, Studio tier for commercial use. Regional pricing (CAD, GBP, EUR) because $39 on a Canadian screen reads American, not "fair."

Organic discovery beat ads at this LTV. HN, Synthtopia, Gearspace, SEO guides for "DX7 backup" and "MIDI-OX alternative Mac." The Web MIDI angle is what developers share. The Juno-106 angle is what musicians search.


Lessons if you're building hardware-facing web apps

  1. Lead with the demo moment. HN taught me: one live counter beats three paragraphs of copy. For us, it's patch names appearing from hex.
  2. Document manufacturer quirks in UI, not a PDF manual from 1987.
  3. Sysex: true or go home.
  4. Ship parsers incrementally. One great DX7 path > ten broken generic paths.
  5. Export guarantee. Let people download .syx anytime. Trust compounds.

If this is you

You're the person with a DX7 in the bedroom and a drawer full of cables that almost work.

You're the Juno owner who finally got SysEx working once, then forgot the switch combo for six months.

You're the repair tech who just needs the bank out before the customer leaves, and back in when the battery's swapped.

You're the developer who looked at MIDI-OX and thought: this should be a URL, not an installer.

You don't need another rabbit hole. You need the dump saved, the names readable, the restore one click away.

knob.monster is free to start. No card. Chrome, a USB-MIDI cable, memory protect off, and the synth you already love.

If you're building something else entirely, steal the Web MIDI patterns anyway. Pedals, controllers, lab gear. The browser is the installer your users were never asking for.


Links

Top comments (0)