reference · control surfaces, midi & djay pro
A DJ controller is two things: a slab of knobs, and a mapping. The mapping is the part that takes days — and you can finish it on a touchscreen weeks before the knobs arrive. This is how: a real 132-control surface prototyped in TouchOSC, mapped to djay Pro across four decks, and edited by script instead of by hand.
Buying a controller is a bet on ergonomics you haven't tested. Prototyping it on glass first turns that bet into an experiment — and the mapping you build transfers to the metal unchanged.
Assigning and learning a few hundred controls takes days. Hardware arrives in one. Do the slow part first, on something you already own.
Does a vertical channel strip actually beat a grid of knobs? You can't answer that from a product page. You can answer it after three nights of mixing on a mock-up.
Your DJ software doesn't know what sent a CC. Emit the same messages the target hardware sends, and the day the box arrives, everything already works.
The honest outcome. A prototype that reveals you only ever touch nine controls has saved you more than it cost.
Most hardware controllers send a fixed factory MIDI map you can't change. Find its MIDI chart, build the prototype to emit those exact channels and CC numbers, and the mapping you make on glass is byte-identical to what the metal will send. Zero rework.
TouchOSC's layout format is zlib-compressed XML. That single fact turns "click every control" into "run a script" — which is the difference between a hundred-control surface being practical and being a weekend you'll never get back.
# decompress → edit → recompress. that's the whole format.
import zlib
xml = zlib.decompress(open('layout.tosc','rb').read()).decode()
# ... transform xml ...
open('out.tosc','wb').write(zlib.compress(xml.encode(), 9))
Inside, every control is a <node> with a unique ID, a frame (x, y, w, h), and — if it sends MIDI — a <messages><midi> block:
<message>
<type>CONTROLCHANGE</type>
<channel>1</channel>
<data1>10</data1> <!-- the CC number -->
<data2>0</data2>
</message>
<values>
<value><type>CONSTANT</type>…</value> <!-- channel ← literal -->
<value><type>INDEX</type>…</value> <!-- data1 ← NOT the field! -->
<value><type>VALUE</type>…</value> <!-- data2 ← control position -->
</values>
Those three <values> entries declare where channel, data1 and data2 get their numbers. On the default template, data1's source is INDEX — so the CC number you carefully typed into the field is ignored, and every control emits the same message. Symptom: your DJ software learns all of them to one control, and each new assignment silently overwrites the last. Fix: set that source to CONSTANT.
The XML leans on <![CDATA[…]]> throughout. Python's built-in ElementTree throws CDATA away on re-serialisation, which is legal XML but a gamble on someone else's parser. Two safe routes: use lxml, or do string surgery on the decompressed text, anchoring each edit to the node's unique ID='…'. Parse with ElementTree to find things; edit the raw string to change them.
Control Change gives you 128 numbers per channel, and 16 channels. Big surfaces run out — but usually far later than people think, and almost never in the way they assume.
The move that makes big surfaces trivial: give each deck its own MIDI channel and reuse identical CC numbers across them. Deck 3's mid EQ is CC 11 on channel 3, exactly as deck 1's mid EQ is CC 11 on channel 1.
You get four times the space, but the real win is cognitive — the surface becomes self-documenting, you learn one deck's map instead of four, and mapping decks 2–4 is mechanical repetition a script can do for you.
If you genuinely run out, switch buttons to Note messages — they stop competing for CC numbers entirely. Worth knowing, rarely needed. With channel-per-deck, a 33-control deck leaves roughly 95 CCs free.
The surface below is a real four-deck djay Pro rig: 33 controls per deck across four vertical channel strips — EQ, filter, volume, stem isolation, FX, transport. Same CC numbers on every deck; only the channel changes.
| control | type | midi | djay keypath |
|---|
keypaths shown for the selected deck ·
Once one deck is mapped by hand, the other three are a transformation. djay Pro stores its mappings as an Apple property list — which Python reads and writes natively.
import plistlib, copy
d = plistlib.load(open(path,'rb'))
tmpl = [e for e in d['controls'] if e['keyPath'].startswith('turntable1.')]
for chan, deck in [(2,2), (3,3), (4,4)]:
for e in tmpl:
n = copy.deepcopy(e)
n['keyPath'] = f'turntable{deck}.' + e['keyPath'].split('.',1)[1]
n['midiChannel'] = chan
d['controls'].append(n)
Thirty-one hand assignments become ninety-three generated ones. buttonMode (hold vs toggle) and controlType ride along in the deep copy, so behaviour stays identical across decks — which hand-mapping reliably fails to achieve.
The failure mode that costs you a soundcheck is two controls landing on the same (channel, CC). Assert it away:
pairs = [(e['midiChannel'], e['midiData']) for e in d['controls']]
assert len(pairs) == len(set(pairs)), 'collision'
Back up the original before writing, and quit the host app first — DJ software will happily overwrite your edit with its in-memory copy on quit. Then verify: plutil -lint on the plist, and a re-parse of the .tosc.
The surface above drives a hybrid rig: DJ software running four decks alongside a DAW running a loop bed, tempo-locked over Ableton Link, each feeding its own mixer channel.
| concern | decision | why |
|---|---|---|
| Master clock | no master | Ableton Link is peer consensus — any participant can move the tempo. The discipline is deciding that only one of them does. |
| Tempo | locked | Loops never time-stretch, phase stays solid, and hardware beat-FX latch instantly. |
| Start/stop sync | off | Otherwise pausing a deck stops the DAW's loop bed underneath you. |
| Outputs | split | Separate mixer channels per app. If the DAW dies you lose the layers, not the set. |
| Phase drift | beatgrids | Link aligns to whatever the software believes the downbeat is. Most "Link is drifting" is a bad grid. |
| Latency | measure once | Keep the DAW master free of lookahead processing, measure the offset against a transient, set track delay, never touch it again. |
Once the prototype proves the layout, the same map drives a custom build — a four-deck controller on a CDJ footprint, one MIDI channel per deck, same CC numbers. See the build reference →
enharmonic · lab
Written from a working rig, not a product page. The prototype cost nothing but an evening and an iPad that was already on the desk — and it answered the hardware question before any money moved.