Tzopilotl
Docs
GitHub

Writing SynthDefs

1. How SynthDefs Work

A synthdef is written as a Tzopilotl function of no arguments returning S (short for SignalExpr). The function does not process audio when it runs. Instead, it runs once at definition time to build a graph: every operation on an S value — +, sinosc, lag, outlet — appends a node to the graph under construction and returns a new S referring to it. The finished graph is handed to the synthdef compiler, which analyzes it (rates, types, channel counts, common subexpressions, SIMD) and generates a native .dylib plugin that the engine loads and can instantiate as nodes.

your synth fn (.x) —runs once→ SignalGraph —defSynthX→ compiled .dylib plugin
                                                        ↓
                                       engine loads it; newNode() makes instances

Two consequences of this staging model are worth internalizing:

A minimal synthdef, defined and played:

import synthdef.*;
import synthc.compile.*;   -- provides defSynthX, the production compile path
import common_ugens.*;

fn beep() S =
    [330.0, 331.0] sinosc          -- 2-channel sine (slightly detuned)
    * (1.0 lfimp decay(0.25))      -- 1 Hz impulse -> exponential decay envelope
    * 0.2
    |> outlet;                     -- the graph's output sink

beep defSynthX("beep") await;      -- compile + load into the engine
"beep" play;                       -- make a node and connect it to audio out

The compile itself runs in the background, so any music already playing keeps running while clang works. The await waits until the def is loaded into the engine — needed here because the next line plays it. When you re-evaluate a defSynthX call for a def that is already playing you can drop the await: players keep using the old version and switch to the new one the moment it finishes loading.

Synthdef code leans on two pieces of Tzopilotl syntax used heavily below: the space-separated pipeline (x sinosc sq is sq(sinosc(x))) and the |> pipe operator, which forwards the whole expression to its left into the function on its right. Many ugens are written with untyped parameters; they are generic over anything convertible to a signal (see §3).

↑ Back to top