TouchDesigner Expert Article 7

Live Performance Architecture in TouchDesigner

Build a robust, cue-driven live performance rig with MIDI control, scene management, crash recovery, and Perform Mode optimisations for consistent show operation.

⏱ 55 min live-performance midi cue-system python architecture

A live performance rig in TouchDesigner is not just a creative system — it is a piece of infrastructure. It must be deterministic, recoverable, and operable under pressure by humans who may be running a show in a dark room with a minute between cues. Every design decision should be evaluated against that operational reality.

The Single .toe File Architecture

Everything lives in one .toe file. Never split a live show across multiple files with Touchin/out OPs bridging them — each inter-file connection is a potential desync point and a slower save operation. Consolidate all assets relative to the .toe file and use File > Consolidate Assets before each show to gather all external media.

Project structure inside the .toe:

/project1
  /master_controller    ← Python class managing all show state
  /scenes
    /scene_01           ← Base COMP
    /scene_02           ← Base COMP
    ...
    /scene_06           ← Base COMP
  /audio
    /analysis           ← CHOP network
    /playback
  /output
    /composite          ← Final compositing chain
    /display            ← Window COMP
  /midi
    /input              ← MIDI In Map COMP
    /mapping            ← Table DAT of CC → parameter mappings
  /cue_system
    /cue_table          ← Table DAT
    /cue_runner         ← Script DAT
  /watchdog
    /heartbeat          ← Timer CHOP pulsing every 500ms
    /monitor            ← Script DAT checking heartbeat

Scene Management with Base COMPs

Each scene is a Base COMP. Scenes are isolated — they have their own feedback loops, GLSL TOPs, and animation networks. Scene switching is handled by a Switch TOP at the compositing level and by Cook flag management.

# master_controller.py — module extension on the master_controller Base COMP

class MasterController:
    def __init__(self, owner_comp):
        self.owner = owner_comp
        self.current_scene = 0
        self.scene_comps = [op(f'/project1/scenes/scene_{i+1:02d}') for i in range(6)]
        self.transition_duration = 2.0  # seconds
        self._active_transition = False

    def go_to_scene(self, index, duration=None):
        """Switch to scene by index (0-based) with optional cross-fade duration."""
        if index == self.current_scene:
            return
        if duration is None:
            duration = self.transition_duration

        prev = self.current_scene
        self.current_scene = index

        # Enable new scene's Cook flag
        self.scene_comps[index].allowCooking = True

        # Cross-fade via LFO CHOP controlling Switch TOP blend
        lfo = op('/project1/output/crossfade_lfo')
        lfo.par.period = duration
        lfo.par.Start.pulse()

        # Disable previous scene after transition completes
        run("args[0].allowCooking = False", self.scene_comps[prev],
            delayFrames=int(duration * project.cookRate) + 5)

        print(f'[MASTER] Scene {prev+1} → Scene {index+1} ({duration:.1f}s fade)')

    def update_midi(self, cc, value):
        """Route incoming MIDI CC to parameter — called from MIDI In CHOP callback."""
        mapping = op('/project1/midi/mapping')
        for row in range(mapping.numRows):
            if int(mapping[row, 'cc']) == cc:
                target_op  = op(mapping[row, 'op'])
                target_par = mapping[row, 'par']
                min_val    = float(mapping[row, 'min'])
                max_val    = float(mapping[row, 'max'])
                mapped     = min_val + (value / 127.0) * (max_val - min_val)
                setattr(target_op.par, target_par, mapped)
                break

MIDI Mapping with MIDI In Map COMP

The MIDI In Map COMP (available in TD 2022+) is the cleanest way to handle hardware controller integration. It provides a visual matrix of CC → parameter mappings that can be learned via MIDI learn mode.

For complex shows, augment with a Table DAT that stores the mapping and a Python script that applies it — this makes the mapping inspectable, version-controllable, and reconfigurable without opening the MIDI In Map COMP UI.

# /project1/midi/mapping Table DAT structure:
cc    op                               par              min    max
1     /project1/scenes/scene_01        Brightness       0.0    2.0
2     /project1/scenes/scene_01        Speed            0.1    5.0
7     /project1/output/composite       Opacity          0.0    1.0
10    /project1/audio/analysis         Gain             0.0    4.0

The Cue System

A cue system drives scene transitions and parameter automation without manual intervention. The cue table stores each cue’s target scene, transition duration, and any parameter snapshots to apply:

# /project1/cue_system/cue_table Table DAT:
cue    scene    duration    trigger        notes
1      1        0.0         manual         Opening state
2      2        3.0         manual         First transition
3      3        2.0         manual         Mid-show build
4      4        4.0         auto:30        30s auto-advance
5      5        1.5         manual         Climax
6      1        5.0         manual         Outro / reset
# /project1/cue_system/cue_runner Script DAT

current_cue = 0
auto_timer = None

def go(dat):
    """Advance to next cue — called by MIDI button or keyboard shortcut."""
    global current_cue, auto_timer
    cue_table = op('/project1/cue_system/cue_table')
    if current_cue >= cue_table.numRows - 1:
        return

    current_cue += 1
    row = cue_table[current_cue]
    scene_idx = int(row['scene']) - 1
    duration  = float(row['duration'])
    trigger   = row['trigger'].val

    mc = op('/project1/master_controller').ext.MasterController
    mc.go_to_scene(scene_idx, duration)

    # Schedule auto-advance if specified
    if trigger.startswith('auto:'):
        seconds = float(trigger.split(':')[1])
        auto_timer = run("op('/project1/cue_system/cue_runner').go(None)",
                         delaySeconds=seconds)

    print(f'[CUE] {current_cue}: Scene {scene_idx+1}, fade={duration}s')

Perform Mode Optimisations

Perform Mode (F1) hides the UI and locks the network, but the bigger gains come from deliberately managing what cooks:

Cook flag management: each inactive scene has allowCooking = False. A 1920×1080 GLSL simulation that isn’t needed right now should not consume GPU cycles. Apply this consistently — a scene with 10 feedback-looping GLSL TOPs costs a significant fraction of your frame budget even when invisible.

Viewer Active flags: all TOP viewers in inactive COMPs should be disabled. TD’s viewer rendering (the small thumbnails in network view) persists in Perform Mode if viewers are set to Active. Add a startup script that disables all viewers:

# Startup Execute DAT — runs once on first frame
def frameStart(frame):
    if frame > 1: return
    for top in root.findChildren(type=topType, depth=10):
        top.viewer = False

Python optimization: avoid per-frame Python for anything that can be a CHOP expression. A Script CHOP that runs Python on every cook costs ~0.1–0.5 ms even for trivial operations; a Math CHOP expression costs microseconds.

Watchdog and Crash Recovery

TD can crash — a GPU driver reset, a Python exception in a callback, a malformed media file. Your watchdog strategy must survive TD itself being absent.

Internal watchdog (catches freezes, not crashes): a Timer CHOP pulses every 500 ms. A Script DAT monitors the pulse. If the pulse hasn’t updated in 1 second, the script logs the error and can attempt a soft recovery (re-cooking stuck OPs):

last_pulse = 0.0

def onPulse(channel, sampleIndex, val, prev):
    global last_pulse
    last_pulse = absTime.seconds

def frameEnd(frame):
    if absTime.seconds - last_pulse > 1.0:
        print('WARNING: Watchdog pulse missed — checking state')
        # Attempt recovery: re-enable cooking on stuck OPs
        for comp in op('/project1/scenes').children:
            comp.allowCooking = comp == op(f'/project1/scenes/scene_{master_controller.current_scene+1:02d}')

External watchdog (catches crashes): a separate Windows batch script or Python process monitors TD’s process and relaunches it:

@echo off
:loop
start /wait "" "C:\Program Files\Derivative\TouchDesigner\TouchDesigner.exe" "C:\Show\show.toe" --perform
echo [%date% %time%] TouchDesigner exitedrelaunching in 3 seconds
timeout /t 3 /nobreak >nul
goto loop

Save this as td_watchdog.bat in the show directory and add it to Windows Task Scheduler to run at system startup with highest privileges.

Pre-Show Checklist

Run this before every show — ideally scripted as a Python diagnostic that prints a pass/fail report:

def pre_show_check():
    checks = []
    import psutil, subprocess

    # GPU temperature
    result = subprocess.run(['nvidia-smi', '--query-gpu=temperature.gpu',
                             '--format=csv,noheader'], capture_output=True, text=True)
    gpu_temp = int(result.stdout.strip())
    checks.append(('GPU temp < 75°C', gpu_temp < 75, f'{gpu_temp}°C'))

    # Available VRAM
    result = subprocess.run(['nvidia-smi', '--query-gpu=memory.free',
                             '--format=csv,noheader,nounits'], capture_output=True, text=True)
    vram_free = int(result.stdout.strip())
    checks.append(('VRAM > 4000 MB free', vram_free > 4000, f'{vram_free} MB'))

    # Cook rate
    actual_fps = 1.0 / max(me.cookTime, 0.001)
    checks.append(('Cook rate ≥ 58 fps', actual_fps >= 58, f'{actual_fps:.1f} fps'))

    # All scenes present
    for i in range(6):
        sc = op(f'/project1/scenes/scene_{i+1:02d}')
        checks.append((f'Scene {i+1} exists', sc is not None, ''))

    for label, passed, detail in checks:
        status = 'PASS' if passed else 'FAIL'
        print(f'  [{status}] {label} {detail}')

Also ensure Windows Update is disabled (or paused for 30 days), power plan is set to High Performance, and all non-essential background processes are killed. A clean Windows install dedicated to TD shows, with no antivirus scanning the TD project directory, is the production standard.