Graphs

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.

A two-node effects chainPlayground
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 node lines 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 param default, 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#

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