Adds four new reference docs covering common TD use cases not previously documented in the skill: - animation.md: LFOs, timers, keyframes, easing, time references - midi-osc.md: MIDI controllers, OSC routing, TouchOSC, multi-machine sync - particles.md: POPs and particleSOP — emission, forces, collisions, render - projection-mapping.md: windowCOMP, corner pin, mesh warp, edge blending Also clarifies the SKILL.md tool quick reference: adds td_screen_point_to_global and notes that 4 admin/dev-mode tools (td_project_quit, td_test_session, td_dev_log, td_clear_dev_log) live only in mcp-tools.md to keep the main reference focused on creative workflows. No SKILL.md workflow or critical-rules changes. References load on demand so no token-budget impact at session start.
7.2 KiB
MIDI / OSC Reference
External controller input and output — MIDI hardware, TouchOSC mobile UIs, OSC routing across the network.
For audio-driven MIDI patterns (track triggers from spectrum analysis), see also audio-reactive.md.
MIDI Input — Hardware Controllers
Discovery
List connected MIDI devices first. Use a midiinDAT to enumerate:
mdat = root.create(midiinDAT, 'mid_devices')
# Read available device names from the DAT after one cook
Or via Python directly:
# In td_execute_python
import td
devices = [d for d in op.MIDI.devices] # verify with td_get_docs('midi')
Verify the API with td_get_docs(topic='midi') since this varies between TD versions.
MIDI In CHOP
Standard pattern:
midi_in = root.create(midiinCHOP, 'midi_in')
midi_in.par.device = 0 # device index from discovery
midi_in.par.activechan = True
Output channels follow the convention chCcN and chCnN:
ch1c74— channel 1, CC 74ch1n60— channel 1, note 60 (middle C) — value is velocity 0-127
Map a CC to a parameter:
op('/project1/bloom1').par.threshold.mode = ParMode.EXPRESSION
op('/project1/bloom1').par.threshold.expr = "op('midi_in')['ch1c74'][0] / 127.0"
Map a note as a trigger:
Notes in midiinCHOP output velocity while held, 0 when released. Use a triggerCHOP to convert a held note into pulses:
trig = root.create(triggerCHOP, 'note_trig')
trig.par.threshold = 1
trig.par.triggeron = 'increase'
trig.inputConnectors[0].connect(op('midi_in'))
# Filter to a single channel via a selectCHOP if desired
MIDI Learn Pattern
Build a reusable learn pattern when you don't know the controller's CC layout in advance:
- Drop a
midiinCHOPandselectCHOPafter it. - User wiggles the controller knob.
- Use
td_read_chopon the midiinCHOP to identify which channel is non-zero — that's the active CC. - Set the
selectCHOP.par.channamesto that channel name. - Save the mapping to a
tableDATso it persists across sessions.
MIDI Output
midi_out = root.create(midioutCHOP, 'midi_out')
midi_out.par.device = 0
midi_out.par.outputformat = 'continuous' # 'continuous' | 'event'
# Drive an output: send out a CC mapped from any 0-1 source
src = root.create(constantCHOP, 'cc_src')
src.par.name0 = 'ch1c20'
src.par.value0 = 0.5
midi_out.inputConnectors[0].connect(src)
For note events specifically, use event mode and pulse the value with a pulseCHOP or triggerCHOP.
OSC Input — Network Control
OSC is the more flexible cousin of MIDI. Used heavily for:
- TouchOSC / Lemur mobile control surfaces
- Show control systems (QLab, Watchout)
- Inter-application sync (Ableton via Max for Live, Resolume, etc.)
OSC In CHOP
osc_in = root.create(oscinCHOP, 'osc_in')
osc_in.par.port = 7000 # listen on UDP 7000
osc_in.par.localaddress = '' # empty = all interfaces
osc_in.par.queued = False # immediate vs. queued processing
Each incoming OSC address becomes a channel. /scene/1/intensity becomes a channel named scene_1_intensity (TD sanitizes slashes to underscores).
Common gotcha: TD only creates the channel after the FIRST message arrives at that address. Send a "hello" message from the controller during setup, or pre-declare channel names manually.
OSC In DAT (for raw events)
Use a oscinDAT when you need full message access (multiple typed args, addresses with brackets/regex).
osc_dat = root.create(oscinDAT, 'osc_events')
osc_dat.par.port = 7001
# Each row: timestamp, address, type tags, args...
Drive logic via a datExecuteDAT watching the oscinDAT:
def onTableChange(dat):
last = dat[dat.numRows - 1, 'message']
parsed = last.val.split()
addr = parsed[0]
args = parsed[1:]
if addr == '/scene/trigger':
op('/project1/scene_switcher').par.index = int(args[0])
return
OSC Output — Sending to External Apps
osc_out = root.create(oscoutCHOP, 'osc_out')
osc_out.par.netaddress = '127.0.0.1' # destination IP
osc_out.par.port = 9000
# Channel names become OSC addresses
src = root.create(constantCHOP, 'send')
src.par.name0 = 'scene/intensity' # → /scene/intensity
src.par.value0 = 0.7
osc_out.inputConnectors[0].connect(src)
Channel-to-address mapping: TD prepends / automatically. Use / in channel names to nest.
For one-shot string/typed messages, use oscoutDAT and call .sendOSC(address, args):
op('osc_out_dat').sendOSC('/scene/trigger', [1, 'fade'])
TouchOSC / Mobile UI Pattern
Common setup for live VJ control from a phone/tablet:
- Configure TouchOSC layout — assign each control an OSC address like
/vj/master,/vj/scene/1, etc. - Find your machine's LAN IP — TouchOSC needs to point at it.
- TD listens on
oscinCHOP.par.port = 8000(or whichever). - Map channels to params via expressions:
op('/project1/master_level').par.opacity.mode = ParMode.EXPRESSION
op('/project1/master_level').par.opacity.expr = "op('osc_in')['vj_master']"
- Send feedback to the controller via
oscoutCHOP— useful for syncing state across multiple devices.
Network / Multi-Machine
OSC over LAN works out-of-the-box. For multi-TD-instance sync (e.g., projection cluster):
- One TD acts as master, broadcasts
/sync/...over OSC - Worker TDs run
oscinCHOPlistening on the same port - Use UDP broadcast address (e.g.,
192.168.1.255) on the master'soscoutCHOP.par.netaddressto hit all peers
For reliability over WAN, use webserverDAT or websocketDAT with an external relay instead — UDP loss is invisible.
Pitfalls
- MIDI device indexing — device
0is whichever device TD enumerated first. Reorder may shift it. Pin by name when possible. - OSC channel names — TD doesn't create a channel until the first message lands. New channels invalidate cooked dependents on first arrival, causing a one-frame stutter.
- OSC queued mode —
par.queued = Truedefers processing to a single per-frame batch. Lower latency but messages arriving same frame collapse to the last value. Off for triggers, on for continuous knobs. - MIDI clock vs. transport —
midiinCHOPreports clock if available. UsemidisyncCHOP(if your TD version exposes it) or compute BPM from clock pulses (24 per quarter note). - Latency — wired MIDI is ~1-3ms. WiFi OSC is 10-30ms with jitter. Use wired for tight beat-locked work.
- Port conflicts — only one process can bind a UDP port on most OS. If
oscinCHOPshows no traffic, check that another app (Max, Ableton, etc.) isn't already listening on that port.
Quick Recipes
| Goal | Op chain |
|---|---|
| Knob → bloom intensity | midiinCHOP → expression on bloom.par.threshold |
| Note → scene change | midiinCHOP → triggerCHOP → selectCHOP → drive switchTOP.par.index |
| Phone slider → master fader | TouchOSC /master → oscinCHOP → expression on output level.par.opacity |
| TD → Resolume scene trigger | oscoutCHOP channel composition/layers/1/clips/1/connect → Resolume listening on 7000 |
| Multi-projector sync | Master TD oscoutCHOP broadcast → workers oscinCHOP |