Start here

Your first patch

A gain, an oscillator, and an envelope — each one runnable, each one a few lines longer than the last.

Anyone who has never written a line of Pole.

Three patches. Each is complete, each is playable here, and each adds exactly one idea to the one before it.

Before pressing Run#

An effect needs an input signal; a generator makes its own. The first patch below is an effect; the runner supplies noise automatically when you press Run. The oscillator makes sound without any input. The last patch responds to the keyboard.

For each one, read the endpoints first, find any state, and follow one frame from the start of the loop to advance(). Make one small change and predict what you will hear before running it again.

The smallest program#

A gainPlayground
processor Gain
{
    input  stream float in;
    param  float gain = 0.5f [0.0f, 1.0f] smooth 10.0f;
    output stream float out;

    void main()
    {
        loop
        {
            out <- in * gain;
            advance();
        }
    }
}

main() runs once per sample. out <- … writes the output. advance() ends the frame; everything after it in that pass is unreachable and reported (POLE0601), and a loop that never advances is refused (POLE0405).

Every output starts each frame at 0. An output the frame does not write reads zero that frame, not the previous frame's value — so a value that should hold between writes belongs in a state variable, which is the next idea.

What the gain actually does

If the input sample is 0.8, multiplying by 0.5 produces 0.4. If the input is -0.8, the output is -0.4. The waveform keeps its shape and sign while its amplitude is halved. Nothing needs to be remembered for the next sample.

Try it: press Run and drag gain to 0 for silence, 0.25 for a quieter result, and 1 for the original input level. The slider runs from 0 to 1: moving it right always increases the level, and moving it left reduces it.

in, out and Gain are names chosen for this example. processor, input, stream, loop and advance are the language forms that give those names their roles.

Something that remembers#

An oscillator cannot be a function of the current sample: it has to know where its phase was. That is what processor state is for.

An oscillator, with a parameterPlayground
processor Osc
{
    output stream float out;
    param  float freq = 220.0f [55.0f, 880.0f] smooth 20.0f;
    param  float level = 0.2f [0.0f, 0.5f] smooth 10.0f;

    float phase = 0.0f;

    void main()
    {
        loop
        {
            out <- sin (phase) * level;
            phase = phase + 6.2831853f * freq / processor.frequency;
            if (phase > 6.2831853f) { phase = phase - 6.2831853f; }
            advance();
        }
    }
}

Two new things.

float phase = 0.0f; at processor scope persists across frames. Its initialiser has to be a single literal, because the block is stamped into memory before any code runs — there is nothing to evaluate an expression with.

param float freq = 220.0f [55.0f, 880.0f] smooth 20.0f; is an input that also carries a name, a default and a range, so a host can enumerate it. Drag the slider while it plays. smooth 20.0f is why dragging it does not click: without smoothing a parameter steps, and a step in a frequency or a cutoff is audible as a click.

Follow the oscillator for one second

phase is an angle in radians. Each frame adds 2π * freq / sampleRate. After one second, an oscillator at 220 Hz has travelled through 220 cycles. Subtracting one turn when phase crosses 2π keeps the stored angle small.

sin(phase) maps that angle to a -1…1 waveform, and the default level of 0.2 leaves output headroom. Changing freq changes how quickly the angle moves; changing the output multiplier changes loudness without changing pitch.

Try it: set the frequency to 440 Hz, one octave above 220. Then change only the level slider. You have now separated two independent controls: where the waveform is in time, and how large it is.

Something you can play#

Add a note and an envelope and it becomes an instrument rather than a tone.

A held note with a release tail — press the keysPlayground
processor Pluck
{
    input  stream float pitch;
    input  stream float velocity;
    input  stream float modWheel;
    input  stream float gate;
    output stream float out;

    param  float release = 0.35f [0.02f, 2.0f] smooth 5.0f;
    param  float level = 0.3f [0.0f, 0.5f] smooth 10.0f;

    float phase = 0.0f;
    float env   = 0.0f;

    void main()
    {
        loop
        {
            // Hold full level while pressed; fade after release.
            if (gate > 0.5f) { env = 1.0f; }
            else { env = env * exp(-6.907755f / (release * processor.frequency)); }

            out <- sin (phase) * env * level;
            phase = phase + 6.2831853f * pitch / processor.frequency;
            if (phase > 6.2831853f) { phase = phase - 6.2831853f; }
            advance();
        }
    }
}

pitch and gate are ordinary input streams. The host feeds them: pitch in Hz, gate 1 while a key is held and 0 once it is released — and pitch holds through the release rather than dropping to zero, so a release tail fades instead of becoming DC. Silencing a voice is the envelope's job, not the oscillator's.

The release is exponential, like a capacitor discharging through a resistor. The level falls quickly at first, then more slowly. Linear releases are useful too; they simply have a different shape.

Why this is a release envelope

While the gate is high, this patch holds env at 1. On release, it repeatedly multiplies the level by a number just below 1, so the tail approaches zero. The release slider is in seconds: after that interval, the level is 60 dB lower (one thousandth of its starting amplitude). It approaches zero rather than reaching exact silence.

Hear the difference: set release to 0.05, hold the A key, then let go. Repeat with release at 2.0. The second note keeps ringing after you release the key. Changing release while holding a note does not change its held loudness; changing level does. Stop ends playback immediately, so release the note rather than pressing Stop when comparing tails.

This simple example has an abrupt attack and can click on note-on. It is a step toward the ADSR chapter, where attack, decay, sustain and release are controlled separately.

The two extra input declarations preserve the command-line note layout: pitch, velocity, mod wheel, then gate. This example only reads pitch and gate; the browser runner locates those streams by name. A host's input mapping is a contract worth checking before debugging the oscillator itself.

What you now know#

Endpoints, main, advance, state, and parameters — which is most of a single-processor program. From here: