The language

Processor state and init

What persists across frames, why an initialiser must be a literal, and the block that builds a table once.

Anyone writing an oscillator, filter, envelope or delay — which is everyone.

State is declared at processor scope and persists across frames. This is what makes an oscillator, filter, envelope or delay possible at all — without it every program is a pure function of the current sample.

Decide what must survive#

Imagine stopping after one output sample and listing everything the next sample needs. An oscillator needs its phase. A filter needs its history. A delay needs the earlier audio in its buffer. Those values are state.

Intermediate arithmetic does not need to survive. Keep a coefficient or a mixed sample in a local let when you can compute it for the current frame. This makes the distinction visible in the source:

Where you put a valueLifetimeExample
Processor stateAcross frames, until resetOscillator phase or filter history.
let or var in the frame loopThis pass through the loopA phase increment or a blended sample.
external bufferOwned and supplied by the hostA loaded sample shared by several voices.
Values written by initStored in state for later framesA waveform table built before playback.
Playground
processor Osc
{
    output stream float out;

    float phase = 0.0f;     // scalar
    int   count = 0;
    float line[2048];       // array

    void main() { loop { advance(); } }
}

The rules:

  • Types are float or int, or a vector of either. A slot is 32 bits.
  • An initialiser must be a single literal (POLE0215). The block is stamped into memory before any code runs, so there is nothing to evaluate an expression with.
  • Array sizes must be a power of two (POLE0320), which is what makes index wrapping one instruction.

external — a buffer the host owns#

Playground
external float wave[2048];

The compiler never sees the contents. That is how a wavetable, impulse response or sample is loaded without the language knowing what a WAV file is. Load one with --data=wave=table.wav.

An external is not state. It is a pointer the host publishes, reached through a table passed to every frame call, and that has two consequences:

  • It is shared. Binding one buffer to eight voices costs eight pointers, not eight copies of a wavetable.
  • It survives a reset. A panic empties a delay line without unloading the wavetable the patch is playing.

In a graph an external belongs to one node, so the name says which: --data=osc.wave=table.wav, or --data=voices[3].osc.wave=… for one element of a node array.

init — state built before the first frame#

A table, a coefficient set, anything computed once: put it in init, which runs once before the first frame and writes the same state block main() reads.

A sine table built once, then readPlayground
processor Wavetable
{
    output stream float out;
    param  float freq = 220.0f [55.0f, 880.0f] smooth 20.0f;

    float table[1024];
    float phase = 0.0f;

    init
    {
        for (wrap<1024> i)
        {
            table[i] = sin (6.2831853f * float (i) * (1.0f / 1024.0f));
        }
    }

    void main()
    {
        loop
        {
            out <- table[int (phase)] * 0.2f;
            phase = phase + 1024.0f * freq / processor.frequency;
            if (phase >= 1024.0f) { phase = phase - 1024.0f; }
            advance();
        }
    }
}

Before init existed, the way to write that was a flag — if (ready == 0) { …fill…; ready = 1; } inside the frame loop — and that costs far more than the branch it looks like. The fill's store sits in the loop, the optimiser cannot prove it does not alias the rest of the state block, and so phase is reloaded and stored every single frame instead of living in a register.

Measured in a browser on exactly this oscillator (tools/webbench, case 06): 1298× realtime with the flag, 5786× with init — same audio, to the bit.

The rules follow from when it runs:

  • No endpoints (POLE0370). There is no frame yet, so there is no input to read and no output to write. State is the whole point.
  • No advance(), no frame loop, no every. It is not a frame.
  • Bounded, like everything else. It may be expensive — filling a table is — but it cannot fail to finish.
  • Once per reset. A panic that empties delay lines rebuilds what init built, because a reset returns the patch to the state it starts in.
  • processor.frequency is available: a rate-dependent table is exactly what this is for.

Cmajor spells it void init(), and Pole accepts that spelling too. That is not politeness — a Cmajor patch carrying an init() would otherwise compile here and silently never run it, and a table of zeros is a quiet patch with nothing to report.

A processor without an init emits no symbol and costs nothing.