TouchDesigner Intermediate Article 4

OSC & MIDI in TouchDesigner

Receive and send OSC and MIDI messages in TouchDesigner to connect mobile controllers, DAWs, and hardware instruments to your visual network.

⏱ 20 min osc midi networking control

Open Sound Control (OSC) and MIDI are the two dominant protocols for connecting performance controllers, DAWs, and other software to TouchDesigner. OSC travels over UDP or TCP on your network; MIDI travels over USB or virtual ports. Both have dedicated operators in TD, and knowing their quirks saves you hours of debugging on stage.

OSC In DAT

The OSC In DAT listens on a UDP port and appends incoming messages as rows in its table. Configure the port in the parameters (default 9000) and leave Local Address blank to listen on all interfaces.

Each incoming message produces a row with columns: address, args, timeStamp, and peer. For real-time response, script against it in a DAT Execute:

# DAT Execute DAT — set "DAT" to oscin1, Table Change callback active
def onTableChange(dat):
    # dat is the OSC In DAT; last row is the newest message
    if dat.numRows == 0:
        return
    
    row = dat.numRows - 1
    address = dat[row, 'address'].val
    args    = dat[row, 'args'].val  # space-separated string
    
    if address == '/synth/filter':
        try:
            freq = float(args.split()[0])
            op('filter_chop').par.cutoff = freq
        except (IndexError, ValueError):
            pass
    
    return

For a dedicated callback-per-message approach, use the OSC In DAT’s Python callback directly. Open the DAT’s Callbacks tab:

def onReceiveOSC(dat, rowIndex, message, bytes, timeStamp, address, args, peer):
    # address is the OSC path string, e.g. '/1/fader1'
    # args is a list of typed Python values (float, int, string)
    
    if address == '/1/fader1':
        op('noise1').par.amplitude = args[0]
    
    elif address == '/1/toggle1':
        op('feedback1').par.resetpulse.pulse() if args[0] == 1 else None
    
    return

OSC Out DAT

The OSC Out DAT sends messages. Set its network address and port. To send a message, execute a Python script that calls sendOSC() on the operator:

# Send a single OSC message
osc_out = op('oscout1')
osc_out.sendOSC('/visual/speed', [float(op('lfo1')['chan1'][0])])

# Send an OSC bundle (multiple messages with the same timestamp)
bundle = [
    ('/colour/r', [0.8]),
    ('/colour/g', [0.2]),
    ('/colour/b', [0.5]),
]
for address, args in bundle:
    osc_out.sendOSC(address, args)

MIDI In CHOP

The MIDI In CHOP receives MIDI data and exposes it as continuously updating channels. In its parameters, select your MIDI device from the Device dropdown. Common channel outputs:

  • Note on/off: one channel per note, named note_60, note_61, etc. (middle C = 60). Value is velocity (0–127, normalized to 0–1).
  • Control Change: channels named cc_1, cc_7, cc_74, etc.
  • Pitch Bend: channel named pitchbend, range –1 to 1.
  • Channel Pressure / Aftertouch: channel named aftertouch.

Wire the MIDI In CHOP into a Select CHOP to isolate specific channels, then into a Math CHOP to rescale the 0–1 range to whatever your parameter needs.

MIDI In Map COMP

For a visual, no-code approach to MIDI mapping, use the MIDI In Map COMP. It presents a patchbay interface where you drag MIDI channels to operator parameters. Under the hood it generates the same Select CHOP + Export chains you would build manually, but it’s faster for live setups. Access it via Op Palette > COMP > MIDI In Map.

MIDI Out CHOP

The MIDI Out CHOP sends MIDI. Connect a CHOP driving note numbers and velocities to its inputs. Set the Device and channel. To trigger a specific note from Python:

midi_out = op('midiout1')
# Arguments: note number, velocity, channel, duration in samples
midi_out.sendNote(60, 100, 1, 100)

# Send CC
midi_out.sendCC(74, int(op('lfo1')['chan1'][0] * 127), 1)

Network Setup: TouchOSC over WiFi

TouchOSC (Hexler) is a mobile app for designing custom OSC control surfaces. Setup:

  1. Put your computer and phone on the same WiFi network.
  2. In TouchDesigner, add an OSC In DAT, set Port to 8000.
  3. Find your computer’s local IP address (run ipconfig on Windows or ifconfig on macOS/Linux in a terminal).
  4. In TouchOSC (the editor or the app), set Host to that IP address and Port to 8000.
  5. Hit play in TouchOSC — messages arrive in the OSC In DAT immediately.

Latency over WiFi is typically 5–20ms, acceptable for visual control but not for musical tempo-sync. For lower latency, use a USB tether (Personal Hotspot on iOS) or a direct Ethernet adapter.

Connecting Ableton Live via Virtual MIDI

On macOS, create a virtual MIDI port in Audio MIDI Setup (IAC Driver). On Windows, install loopMIDI (free). Then:

  1. In Ableton, assign an instrument or clip to output to the virtual port.
  2. In TD, set the MIDI In CHOP’s Device to that virtual port.
  3. MIDI notes from Ableton’s clips now appear as MIDI In CHOP channels in real time.

For timecode sync (Ableton → TD), use MIDI Clock or MTC — both arrive through the same MIDI In CHOP. The Timecode CHOP can decode MTC.

Full Example: MIDI Keyboard → Visual Bursts

The goal: each keypress on a MIDI keyboard triggers a particle burst, with the note pitch controlling the burst direction and velocity controlling size.

Network layout:

midi_keyboard_in (MIDI In CHOP)
  └─→ select_notes (Select CHOP) ── channels: note_36 to note_96
        └─→ trigger_chop (Trigger CHOP) ── Threshold: 0.1
              └─→ chop_execute (CHOP Execute DAT)
# CHOP Execute DAT attached to trigger_chop
def onOffToOn(channel, sampleIndex, val, prev):
    # channel.name is e.g. 'note_60'
    note_num = int(channel.name.split('_')[1])
    
    # Map note (36–96) to angle in degrees
    angle = (note_num - 36) / 60.0 * 360.0
    
    # Map velocity (val 0–1) to particle count
    count = int(val * 500)
    
    particles = op('particlesop1')
    
    # Set birth direction
    angle_rad = angle * 0.01745329  # degrees to radians
    particles.par.birthvx = math.cos(angle_rad) * 50
    particles.par.birthvy = math.sin(angle_rad) * 50
    particles.par.birthvz = 0
    
    # Pulse the birth rate up then let it decay
    op('birth_const').par.value0 = count
    
    return

def onValueChange(channel, sampleIndex, val, prev):
    return

Add a Lag CHOP after birth_const with a decay of 0.5 seconds so each burst fades quickly. The Particle SOP uses birth_const’s output as its birth rate.

Latency Considerations

UDP OSC has no acknowledgement — packets can arrive out of order or not at all on congested networks. For critical triggers (e.g. syncing to a music downbeat), use TCP OSC (OSC In DAT > Protocol: TCP) or MIDI, which has tighter timing guarantees over USB. The MIDI In CHOP also has a small internal buffer; increase it in the parameters if you are sending bursts of many simultaneous notes.