enharmonic
/ rig prototyping
the hardware build →

reference · control surfaces, midi & djay pro

build the mapping
before you buy the hardware

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.

132
controls mapped
4
midi channels
33
per deck
~0
clicks per control
01

the idea

You can't tell how a controller will feel from a product page. Building the layout on a touchscreen first lets you find out before you spend anything, and the mapping you make carries straight over to the hardware.

The mapping takes the time

Assigning and learning a few hundred controls takes days. The hardware shows up in one. Do the slow part first, on something you already have.

Layout is testable

Does a vertical channel strip actually beat a grid of knobs? Three nights of mixing on a mock-up will tell you.

It carries over

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.

You might not buy it

That's a real outcome. If the mock-up shows you only ever touch nine controls, it's already paid for itself.

The transfer trick

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.

02

a .tosc file is just xml

TouchOSC's layout format is zlib-compressed XML. That means you can edit a hundred-control surface with a script, instead of clicking through every control by hand.

# 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>

The gotcha that eats an evening

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.

editing without breaking it

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.

03

the midi budget

Control Change gives you 128 numbers per channel across 16 channels. Big surfaces can run out, though usually later than you'd expect.

one channel per deck

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.

Buttons don't need CC at all

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.

04

a worked map

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.

controltypemididjay keypath

keypaths shown for the selected deck ·

05

generate, don't click

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.

validate before you write

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'

Before you write anything

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.

06

where it's pointed

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.

concerndecisionwhy
Master clockno masterAbleton Link is peer consensus — any participant can move the tempo. The discipline is deciding that only one of them does.
TempolockedLoops never time-stretch, phase stays solid, and hardware beat-FX latch instantly.
Start/stop syncoffOtherwise pausing a deck stops the DAW's loop bed underneath you.
OutputssplitSeparate mixer channels per app. If the DAW dies you lose the layers, not the set.
Phase driftbeatgridsLink aligns to whatever the software believes the downbeat is. Most "Link is drifting" is a bad grid.
Latencymeasure onceKeep the DAW master free of lookahead processing, measure the offset against a transient, set track delay, never touch it again.

Next: the hardware

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 while building the thing. The prototype cost an evening and an iPad that was already sitting on the desk, and it answered the hardware question before any money moved.