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 value | Lifetime | Example |
|---|---|---|
| Processor state | Across frames, until reset | Oscillator phase or filter history. |
let or var in the frame loop | This pass through the loop | A phase increment or a blended sample. |
external buffer | Owned and supplied by the host | A loaded sample shared by several voices. |
Values written by init | Stored in state for later frames | A waveform table built before playback. |
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
floatorint, 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#
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.
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 frameloop, noevery. 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
initbuilt, because a reset returns the patch to the state it starts in. processor.frequencyis 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.