Skip to content

About

.tox that bridges SuperCollider and TouchDesigner, allowing for complex sound design with simple python code

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

40 Commits

Folders and files

Repository files navigation

TDSuperCollider

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.

Requirements

Setup

  1. Drop TDSuperCollider/toxes/TDSuperCollider.tox into your project.
  2. Keep the TDSuperCollider/supercollider/ folder next to your .toe (the launcher resolves <project folder>/TDSuperCollider/supercollider/).
  3. 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
  4. Pulse Start. When the textport prints TDSuperCollider is ready to use! and the synthDefs table 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.

How it works

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.

Python API (extSynthManager)

# 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.

SC code as parameter strings

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.

Adding SynthDefs

Drop a .scd file in TDSuperCollider/supercollider/synthdefs/. Conventions:

  • File name must match the SynthDef name (simpleSine.scd defines \simpleSine) — introspection uses the file name.
  • One SynthDef per file.
  • Give sustaining synths a gate arg with an enveloped doneAction: 2 (see simpleSine.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; their doneAction: 2 envelope frees the voice and TD's row disappears on its own.

OSC protocol reference

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).

Testing without TouchDesigner

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.py

Repo layout

TDSuperCollider/
├── 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.

About

.tox that bridges SuperCollider and TouchDesigner, allowing for complex sound design with simple python code

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages