A TouchDesigner ↔ SuperCollider bridge. TouchDesigner launches sclang as a
subprocess, SuperCollider boots the audio server and loads your SynthDefs, and
the two sides talk OSC: TD sends play/update/kill requests, SC reports back
what actually happened. The synthTable DAT in TD is a live mirror of the
voices playing on the server.
- SuperCollider 3.13+ (macOS or Windows)
- TouchDesigner
- git on the PATH (first launch installs the ChordSymbol2 quark)
- Drop
TDSuperCollider/toxes/TDSuperCollider.toxinto your project. - Keep the
TDSuperCollider/supercollider/folder next to your.toe(the launcher resolves<project folder>/TDSuperCollider/supercollider/). - If sclang lives somewhere non-standard, set the Sclangpath parameter
on the component. Defaults:
- macOS:
/Applications/SuperCollider.app/Contents/MacOS/sclang - Windows:
C:\Program Files\SuperCollider-3.13.0\sclang.exe
- macOS:
- Pulse Start. When the textport prints
TDSuperCollider is ready to use!and thesynthDefstable fills in, you're up.
First-ever launch installs the ChordSymbol2 quark; sclang can only compile quark classes at startup, so restart once after that install before using chord-symbol strings.
TouchDesigner SuperCollider
│ launch sclang TDSuperCollider.scd <tdPort> │
│──────────────────────────────────────────────▶│ boots scsynth, loads
│ │ synthdefs/ + oscdefs/
│◀───────── /synthDef name, params ─────────────│ (introspection)
│◀───────── /sc/port <langPort> ────────────────│ (handshake: TD points
│ │ oscout1 at langPort)
│────────── /synth <type> play … ──────────────▶│ spawns voices
│◀───────── /synth <type> <id> created … ───────│
│────────── /synth <type> update id … ─────────▶│ /n_set on the node
│◀───────── /synth <type> <id> updated … ───────│
│────────── /synth <type> kill id … ───────────▶│ gate release or /n_free
│◀───────── /synth <type> <id> killed ──────────│ (sent from onFree)
│────────── /tdsc/quit ────────────────────────▶│ s.quit, then exit
The server is the single source of truth for voice state. TD's Python methods
only send requests; synthTable rows are created, patched, and deleted
exclusively by the created / updated / killed events coming back from
SC. A killed event is emitted from each node's onFree handler, so it fires
exactly once whether the voice was killed from TD or freed itself
(doneAction: 2). Killed-but-unconfirmed voices show status = killing and
can be retried.
StartSuperCollider always tears down any previous session first (including
an orphaned scsynth if sclang crashed) and clears both mirror tables — node
IDs restart with the server, so stale rows would otherwise address freshly
spawned, unrelated voices.
# one voice, or one per freq in a list; extra kwargs become synth params
op.TDSuperCollider.PlaySynth('pad1', 'simpleSine', [220, 277, 330], lpFreq=3000)
# update every voice under a logical name
op.TDSuperCollider.UpdateSynth('pad1', lpFreq=800)
# free every voice under a logical name
op.TDSuperCollider.KillSynth('pad1')freqs (and any string param) may also be SuperCollider code — see below.
Any string parameter that compiles as SuperCollider code is evaluated on the language side, so this plays a chord:
op.TDSuperCollider.PlaySynth('chord1', 'simpleSine', "ChordSymbol.asNotes('Cmaj7').midicps")A string that evaluates to an array fans out into one voice per element.
Strings that don't compile are passed through unchanged (they won't crash the
handler). Because this is arbitrary code execution, the /synth and
/tdsc/quit handlers ignore any message that doesn't originate from
127.0.0.1.
Drop a .scd file in TDSuperCollider/supercollider/synthdefs/. Conventions:
- File name must match the SynthDef name (
simpleSine.scddefines\simpleSine) — introspection uses the file name. - One SynthDef per file.
- Give sustaining synths a
gatearg with an envelopeddoneAction: 2(seesimpleSine.scd). Kills then release with a declick fade instead of a hard/n_free, and the free still reports back to TD. A kill hard-frees any gated voice still alive 5 s after its release was requested, so keep gated release tails under ~5 s. - Percussive one-shots (see
FMRhodes1.scd) need no gate; theirdoneAction: 2envelope frees the voice and TD's row disappears on its own.
TD → SC, address /synth, args [synthType, action, key, value, ...]:
| action | required keys | effect |
|---|---|---|
play |
name, optional freq + params |
one Synth per freq (array freq fans out) |
update |
id + params |
/n_set each param on the node |
kill |
id |
gate release if the def has a gate, else /n_free |
SC → TD, address /synth, args [synthType, nodeID, event, ...]:
| event | when |
|---|---|
created |
a voice was spawned (echoes name/freq/params) |
updated |
an update was applied (echoes the set values) |
killed |
the node was actually freed (from onFree) |
error |
e.g. play request for an unknown SynthDef |
Plus /synthDef name, params (introspection, at boot), /sc/port langPort
(handshake), /sc/error msg (boot failure), and TD → SC /tdsc/quit (clean
shutdown: quits scsynth, then sclang).
tests/tdsc_e2e_test.py simulates the TD side over OSC (pure Python, no
dependencies) and exercises every protocol path — play/update/kill, chord
fan-out, self-freeing synths, missing freq, uninterpretable strings, unknown
SynthDefs, and clean shutdown. All voices play at zero volume.
python3 tests/tdsc_e2e_test.pyTDSuperCollider/
├── toxes/TDSuperCollider.tox # the component
├── extensions/ # Python extensions (synced into the tox)
│ ├── extTDSuperCollider.py # process lifecycle (start/stop sclang+scsynth)
│ └── extSynthManager.py # voice requests + synthTable mirroring
└── supercollider/
├── TDSuperCollider.scd # master startup script (sclang entry point)
├── scripts/ # pre/post-boot setup, quark install, introspection
├── synthdefs/ # one SynthDef per file — add yours here
└── oscdefs/tdSynth.scd # the /synth request handler
Note: the .tox embeds its own copies of the extension DATs. After editing
the .py files on disk, re-sync them inside TouchDesigner and re-save the
tox. The SuperCollider side is loaded from disk on every launch.