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.
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:
- Add an OSC In DAT. Set Port to
8000(or any open port). - Leave Address blank to accept from any sender.
In TouchOSC:
- Go to Connections > OSC. Enable OSC.
- Set Host to your computer’s local IP address (find it with
ipconfig/ifconfig). - Set Send Port to
8000. - 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) — serveshtml_control_pagewebsocket1(WebSocket DAT, Port 9981) — receives slider updateshtml_control_page(Text DAT) — contains the HTML abovenoise1(Noise TOP) — controlled target
Workflow:
- Visitor on the same network opens
http://[computer-ip]:9980in any browser. - The page loads instantly (served by
webserver1). - Moving a slider sends a WebSocket JSON message to port 9981.
- The WebSocket DAT callback parses the JSON and sets
noise1.par.amplitude. - 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.