This is a hands-on walkthrough: by the end you'll have a working, linted,
tested osc-bridge driver for a CC-based synthesizer, and you'll know how to
extend it.
We'll build the driver step by step. The running example is a small CC-only mono synth with a filter, an envelope, and an LFO — the kind of device whose MIDI implementation fits on one page. The CC numbers below are illustrative. When you follow along with a real device, swap in the values from its MIDI implementation chart.
If you just want the reference (every field, every type), read
DEVICE_JSON_SCHEMA.md. This tutorial is the worked
example; that's the dictionary.
git clone https://github.com/roomi-fields/osc-bridge
cd osc-bridge
cargo build --release
./target/release/osc-bridge --helpIf cargo build succeeds, you're set. You do not need the physical synth
to write and lint a driver — only to promote it to ✅ later.
Every driver must cite where its mapping comes from. No source = the PR stays open until one is provided. Acceptable sources:
- the manufacturer's MIDI implementation chart / programmer's reference,
- a canonical community CSV (e.g. pencilresearch/midi),
- your own reverse-engineering log from the hardware.
For this tutorial, assume we have the synth's one-page MIDI chart in hand.
Drivers live at devices/<vendor>/<slug>.json. Vendor folder is the
manufacturer in lowercase-kebab; slug is the model.
mkdir -p devices/acme
$EDITOR devices/acme/bassline.jsonEvery driver starts with a device block. For a CC-only synth, that's almost
all the boilerplate there is:
_sources[]— your provenance.typeis one ofvendor-doc,hardware-verified,electra-preset,pencilresearch, … (see the schema doc).osc_prefix— every OSC route this device exposes will be prefixed with it. Keep it short and unambiguous.
A SysEx synth would also declare manufacturer_id / device_id and a sysex
header/footer. A pure-CC synth needs none of that.
This is the heart of the driver. Each entry maps one named OSC address to one MIDI Control Change message.
{
"cc_params": {
"channel": 0,
"entries": [
{ "osc": "/filter/cutoff", "cc": 74, "range": [0, 127] },
{ "osc": "/filter/resonance", "cc": 71, "range": [0, 127] },
{ "osc": "/amp/attack", "cc": 73, "range": [0, 127] },
{ "osc": "/amp/release", "cc": 72, "range": [0, 127] },
{ "osc": "/lfo/rate", "cc": 76, "range": [0, 127] },
{ "osc": "/lfo/depth", "cc": 77, "range": [0, 127] }
]
}
}channel is the default MIDI channel (0–15, i.e. "channel 1"). Sending
/bassline/filter/cutoff 64 now emits B0 4A 40 — a CC message on channel 1.
A few things you'll reach for as your device needs them:
- 14-bit CC — declare an
cc_lsbalongsideccand widen the range:{ "osc": "/osc/pitch", "cc": 3, "cc_lsb": 35, "range": [0, 16383] } - NRPN — declare
nrpn_msb/nrpn_lsbinstead ofcc; the bridge emits the full NRPN sequence for you. - Per-entry channel — add
"channel": 9to one entry to override the default for that parameter only. - Grouping — add a
"section": "Filter"tag; it's free-form and only used for organisation in tooling.
Add one entry per parameter on your synth's chart. This is the bulk of the work and the part worth getting right.
If your device should also respond to note-on/off, pitch-bend, and friends,
add a midi_out block. Its mere presence switches on a standard set of
performance routes:
{
"midi_out": {
"default_channel": 0,
"note_offset": 0
}
}With that, the bridge accepts (all prefixed by osc_prefix):
| OSC | sends |
|---|---|
/note/on {note} {vel} |
Note-On |
/note/off {note} |
Note-Off |
/pitchbend {value_u14} |
Pitch-Bend |
/cc/{num} {value} |
raw CC |
/program_change {prog} |
Program Change |
Omit the whole block for a device that should not accept performance routes (a pure effects unit, a controller).
./target/release/osc-bridge lint devices/acme/bassline.jsonExpected:
devices/acme/bassline.json: parsed OK — 0 commands, 0 replies, 0 params, 6 cc_params
The linter parses the file, counts what it found, and warns on any scripted
fallback (transform / script) — you have none, so it's silent. If the JSON
is malformed or a required field is missing, this is where you find out.
Inspect the driver without connecting anything:
./target/release/osc-bridge inspect devices/acme/bassline.jsonIf you have the synth on a MIDI port, run the bridge and send it something:
# find your MIDI out port index
./target/release/osc-bridge list
# run the bridge (port 4 is an example)
./target/release/osc-bridge run --device devices/acme/bassline.json --out-port 4 &
# send an OSC message
./target/release/osc-bridge osc-send /bassline/filter/cutoff 100The filter should move. If it doesn't: double-check the CC number and channel against the chart, and watch the bridge's stderr — it logs every message it dispatches.
The device list is generated. Regenerate it so your driver shows up:
python3 scripts/regen_supported_devices.py
python3 scripts/build_device_index.pyBoth docs/SUPPORTED_DEVICES.md and docs/devices.json now include the Acme
Bassline. CI fails if you forget this step, so always run it before committing
a device change.
cargo test --release # the schema round-trip tests must pass
git add devices/acme/bassline.json docs/SUPPORTED_DEVICES.md docs/devices.json
git commit -m "devices: add Acme Bassline (vendor-doc)"Push and open the PR. The new_device.md template walks you through the rest.
You've covered the 90% case. The remaining 10%, in rough order of how often you'll need it:
- Fixed commands — preset recall, display text, pad colours: the
commands[]block builds a SysEx/OSC frame from a placeholder template. - SysEx parameters — hundreds of params behind one opcode family: the
paramsblock declares the framing once and a flat address table. - Replies — decode device-initiated messages back into OSC: the
replies[]block. - Software targets — driving a DAW or live-coding environment instead of
hardware:
device.transport.kind = "osc". See the schema doc's "Software drivers" section. - The Lua escape hatch — checksums, exotic encodings, conditional
transforms:
transform/script. Use sparingly;lintwarns on every use. Seescripting.md.
All of it is in DEVICE_JSON_SCHEMA.md. And the
contributor checklist — folder conventions, the no-silent-fusion rule, how to
promote a driver to ✅ — is in ../CONTRIBUTING.md.
{ "_sources": [ { "type": "vendor-doc", "name": "Acme Bassline MIDI Implementation Chart v1.2", "url": "https://acme.example/bassline/midi.pdf" } ], "device": { "name": "Acme Bassline", "vendor": "Acme", "revision": "firmware 1.2", "osc_prefix": "/bassline" } }