TouchDesigner Intermediate Article 9

TouchOSC & Web Control in TouchDesigner

Control TouchDesigner from mobile devices via TouchOSC, serve custom HTML control pages from inside TD, and build low-latency bidirectional WebSocket connections.

⏱ 22 min touchosc web websocket remote-control

Remote control over a network opens up workflows that are impossible with a mouse and keyboard: performers can carry a phone on stage, audience members can influence visuals from a QR code on the wall, and operators at the front-of-house mixing desk can adjust parameters without touching the production machine. TouchDesigner has built-in tools for all of these patterns.

TouchOSC: Designing a Mobile Layout

TouchOSC (by Hexler, available for iOS and Android) lets you design a custom multi-touch control surface using sliders, buttons, XY pads, faders, and multi-toggle grids. Install it and open the editor (TouchOSC Editor on desktop, or the in-app editor on newer versions).

A basic layout for a visual performance might include:

  • 4 faders: Master Brightness, Colour Hue, Noise Speed, Noise Scale
  • 6 toggle buttons for scene selection
  • 1 XY pad for 2D position control
  • 1 push button for Flash/Reset

Each control has an OSC address you define (e.g. /1/fader1, /scene/button3). Use descriptive paths — they make the TD script readable.

Connecting TouchOSC to TouchDesigner

Ensure your computer and mobile device are on the same WiFi network (or connected via USB tether/hotspot).

In TouchDesigner:

  1. Add an OSC In DAT. Set Port to 8000 (or any open port).
  2. Leave Address blank to accept from any sender.

In TouchOSC:

  1. Go to Connections > OSC. Enable OSC.
  2. Set Host to your computer’s local IP address (find it with ipconfig / ifconfig).
  3. Set Send Port to 8000.
  4. Hit Done and play your layout.

Fader movements now appear as rows in the OSC In DAT immediately.

Mapping TouchOSC Controls via Python

Rather than using a DAT Execute that processes every incoming message, use a dedicated dispatch dictionary to keep the mapping clean:

# DAT Execute DAT — Table Change, attached to oscin1
# Maps OSC address to (operator path, parameter name)
OSC_MAP = {
    '/1/fader1': ('noise1',      'amplitude'),
    '/1/fader2': ('noise1',      'period'),
    '/1/fader3': ('level1',      'invert'),
    '/1/fader4': ('render_cam',  'tz'),
    '/1/rotary1': ('lfo1',       'rate'),
}

def onTableChange(dat):
    if dat.numRows == 0:
        return
    
    row   = dat.numRows - 1
    addr  = dat[row, 'address'].val
    args  = dat[row, 'args'].val
    
    if addr not in OSC_MAP:
        return
    
    op_name, param = OSC_MAP[addr]
    target = op(op_name)
    
    if target is None:
        return
    
    try:
        val = float(args.split()[0])
        setattr(target.par, param, val)
    except (ValueError, AttributeError, IndexError) as e:
        debug(f'OSC map error {addr}: {e}')

For the XY pad (which sends two values in one message), parse args differently:

if addr == '/1/xy1':
    parts = args.split()
    if len(parts) >= 2:
        x, y = float(parts[0]), float(parts[1])
        op('camera1').par.tx = (x - 0.5) * 400
        op('camera1').par.ty = (y - 0.5) * 400

Web Server DAT

The Web Server DAT runs a full HTTP server inside TouchDesigner. Add one to your network and set its Port to 9980 (default). It serves requests via Python callbacks:

# Web Server DAT callbacks (in its Callbacks DAT)
def onHTTPRequest(webServerDAT, request, response):
    uri = request['uri']
    
    if uri == '/' or uri == '/index.html':
        html = op('html_control_page').text
        response['statusCode'] = 200
        response['statusReason'] = 'OK'
        response['data'] = html
        response['headers'] = {'Content-Type': 'text/html; charset=utf-8'}
    
    elif uri.startswith('/set/'):
        # e.g. /set/noise_amplitude/0.75
        parts = uri.split('/')
        if len(parts) == 4:
            param, val_str = parts[2], parts[3]
            try:
                val = float(val_str)
                handle_set(param, val)
                response['statusCode'] = 200
                response['data'] = '{"status":"ok"}'
            except ValueError:
                response['statusCode'] = 400
                response['data'] = '{"error":"bad value"}'
        
        response['headers'] = {'Content-Type': 'application/json'}
    
    return response

def handle_set(param, val):
    if param == 'brightness':
        op('level1').par.brightness = val
    elif param == 'speed':
        op('lfo1').par.rate = val * 10

Serving an HTML Control Page

Store your HTML in a Text DAT named html_control_page. The Web Server DAT serves it when / is requested. A minimal control page with a WebSocket:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>TD Control</title>
  <style>
    body { font-family: monospace; background: #111; color: #eee; padding: 20px; }
    input[type=range] { width: 300px; }
    label { display: block; margin: 10px 0 4px; }
  </style>
</head>
<body>
  <h2>Noise Control</h2>
  <label>Amplitude <span id="amp_val">0.5</span></label>
  <input type="range" id="amplitude" min="0" max="1" step="0.01" value="0.5">
  
  <label>Speed <span id="spd_val">1.0</span></label>
  <input type="range" id="speed" min="0.1" max="20" step="0.1" value="1.0">

  <script>
    const ws = new WebSocket('ws://' + location.hostname + ':9981');
    
    ws.onopen = () => console.log('Connected');
    ws.onclose = () => console.log('Disconnected');
    
    function sendControl(name, val) {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ param: name, value: val }));
      }
    }
    
    document.getElementById('amplitude').addEventListener('input', function() {
      document.getElementById('amp_val').textContent = this.value;
      sendControl('noise_amplitude', parseFloat(this.value));
    });
    
    document.getElementById('speed').addEventListener('input', function() {
      document.getElementById('spd_val').textContent = this.value;
      sendControl('noise_speed', parseFloat(this.value));
    });
  </script>
</body>
</html>

WebSocket DAT for Bidirectional Communication

The WebSocket DAT in TouchDesigner creates a WebSocket server. Set its Port to 9981 (must differ from the Web Server DAT port). Its Callbacks DAT handles incoming messages:

# WebSocket DAT callbacks
def onReceiveText(dat, rowIndex, message, bytes, peer):
    import json
    
    try:
        data = json.loads(message)
        param = data.get('param', '')
        val   = float(data.get('value', 0))
        
        param_map = {
            'noise_amplitude': ('noise1', 'amplitude'),
            'noise_speed':     ('lfo1',   'rate'),
        }
        
        if param in param_map:
            op_name, par_name = param_map[param]
            target = op(op_name)
            if target:
                setattr(target.par, par_name, val)
    
    except (json.JSONDecodeError, ValueError, TypeError) as e:
        debug(f'WebSocket parse error: {e}')
    
    return

def onConnect(dat, rowIndex, peer):
    debug(f'WebSocket client connected: {peer}')
    # Optionally send current state to the new client
    import json
    state = json.dumps({
        'noise_amplitude': op('noise1').par.amplitude.val,
        'noise_speed': op('lfo1').par.rate.val,
    })
    dat.sendText(state, peer)
    return

Web Client DAT

The Web Client DAT makes outbound HTTP requests from TouchDesigner. Use it to POST data to an external API, pull weather data, or notify a webhook:

# Script DAT — send current frame data to an API
import json

payload = json.dumps({
    'frame': me.time.frame,
    'level': op('audio_level')['chan1'][0],
})

wc = op('webclient1')
wc.request(
    url='https://your-logging-api.example.com/frames',
    method='POST',
    data=payload,
    headers={'Content-Type': 'application/json'},
)

Full Example: HTML Slider Controls Noise TOP in Real Time

TD network:

  • webserver1 (Web Server DAT, Port 9980) — serves html_control_page
  • websocket1 (WebSocket DAT, Port 9981) — receives slider updates
  • html_control_page (Text DAT) — contains the HTML above
  • noise1 (Noise TOP) — controlled target

Workflow:

  1. Visitor on the same network opens http://[computer-ip]:9980 in any browser.
  2. The page loads instantly (served by webserver1).
  3. Moving a slider sends a WebSocket JSON message to port 9981.
  4. The WebSocket DAT callback parses the JSON and sets noise1.par.amplitude.
  5. The Noise TOP updates on the next cook — latency is typically under 20ms on a local network.

For a QR code entry point at a gallery installation, generate a QR code for http://[ip]:9980 using any QR code library and display it on a secondary monitor.