Graphs and nodes
Wiring processors together — and the rules that decide what a half-wired graph does.
Anyone whose patch is more than one processor.
A graph names processor instances and wires them together. Every node is an
ordinary processor, and nothing about a processor knows it is in a graph.
Think in boxes and wires#
A processor is a recipe; a node is an instance of that recipe with its own
memory. Naming two nodes of type Filter creates two independent filters,
just as using two delay pedals gives each pedal its own recorded history.
A graph supplies the wiring and the public controls. In the example below,
follow the three audio connections from in to dist, then flt, then
out. The separate cutoff connection carries a control value to the filter.
Nothing is being called as Filter(...); graph instances are declared with
node name = Type, and their arguments arrive through endpoints.
processor Drive
{
input stream float in;
output stream float out;
param float amount = 2.0f [1.0f, 12.0f];
void main() { loop { out <- tanh (in * amount) * 0.7f; advance(); } }
}
processor Filter
{
input stream float in;
output stream float out;
param float cutoff = 1200.0f [60.0f, 12000.0f];
float z = 0.0f;
void main()
{
loop
{
let a = clamp (cutoff / 24000.0f, 0.001f, 0.9f);
z = z + a * (in - z);
out <- z;
advance();
}
}
}
graph FxChain
{
input stream float in;
output stream float out;
param float cutoff = 2400.0f [60.0f, 12000.0f];
node dist = Drive;
node flt = Filter;
connection
{
in -> dist.in;
dist.out -> flt.in;
cutoff -> flt.cutoff;
flt.out -> out;
}
}A bare name in a connection means one of the graph's own endpoints; a dotted name means a node's. Data flows out of the graph's inputs and into its outputs, which is the one place the sense of "source" and "destination" inverts.
The rules, all of them#
- Fan-in sums. Several wires into one input add. Fan-out copies.
- Execution order comes from the wires, not from the order the
nodelines are written. Moving a declaration changes nothing. - An ordinary node runs once per graph frame. A divided-clock node deliberately runs less often; see slower clocks.
- Each node has its own state. Two instances of one processor are two independent filters.
- An unconnected input reads its
paramdefault, or 0 if it has none. A half-wired graph compiles, because that is the normal state of one being edited. - An unconnected output is a warning, not an error.
- A cycle needs a delay on one of its edges — see feedback — and the diagnostic names the loop and the fix.
- A node's own array endpoint cannot be wired. Eight slots on one instance share one state block; eight instances do not. Use a node array.
A node may be a graph#
graph Voice
{
input stream float pitch;
output stream float out;
node osc = Osc; node flt = Filter;
connection { pitch -> osc.pitch; osc.out -> flt.in; flt.out -> out; }
}
graph Synth { node voices = Voice[8]; /* ... */ }A voice in any real instrument is several blocks, so a Voice that had to be
one processor was not a voice — it was the graph thrown away and rewritten as
hand-indexed parallel arrays.