The language

Functions

Give an idea a name, pass its inputs explicitly, and reuse it — with signatures, arguments and worked calls.

Anyone who wants to turn a repeated expression into a readable DSP helper.

Suppose your patch blends two values in several places: a dry and wet sample, two table entries, perhaps two modulation sources. You could repeat a + (b - a) * t each time. A function lets you name that operation once and make the rest of the patch read in terms of what it does.

In Pole a helper belongs inside a processor, beside main(). The helper receives values, computes a result and returns it. It does not run on its own: you choose when to call it.

Read a function signature#

Here is the reference form of a linear interpolation helper:

float lerp(float a, float b, float t)

Read it from left to right: “return a float; the function is named lerp; it takes three float arguments called a, b and t.” The types belong in the definition. At the call site, supply only the values:

lerp(0.2f, 0.8f, 0.5f) → 0.5f
ParameterMeaning
aStart value. Returned when t is 0.
bEnd value. Returned when t is 1.
tBlend position. 0.5 is halfway; values outside 0…1 extrapolate beyond the endpoints.

Returns: a + (b - a) * t, as a float. This version does not clamp t. There is no built-in lerp; the definition below creates it for this processor.

A function parameter is the name in its definition. An argument is the value you pass when calling it. These are different from a processor's param, which is a control the host can move.

Define it, then call it#

A dry/wet blend with a reusable helperPlayground
processor Blend
{
    input stream float in;
    output stream float out;
    param float mix = 0.5f [0.0f, 1.0f] smooth 20.0f;

    float lerp(float a, float b, float t)
    {
        return a + (b - a) * t;
    }

    void main()
    {
        loop
        {
            let dry = in;
            let wet = tanh(in * 3.0f) * 0.5f;
            out <- lerp(dry, wet, mix);
            advance();
        }
    }
}

The call lerp(dry, wet, mix) binds dry to a, wet to b and mix to t. Pole evaluates the helper's expression and uses the returned sample as the output. You can pass literals, local variables, endpoints or other expressions; the helper sees their values.

At mix = 0, you hear the original input. At mix = 1, you hear the soft-clipped signal. Between them you hear a blend. This is a linear crossfade, so it does not promise constant perceived loudness for unrelated signals.

Try it: change the wet expression to in * -1.0f. Halfway through the blend, the original and inverted signals cancel. The helper has not changed; you changed what its arguments mean.

Calls are positional and explicitly typed#

Supply every argument in the order shown in the signature. Pole does not have named arguments or default argument values for helpers.

CallWhat happens
lerp(0.0f, 1.0f, 0.25f)Returns 0.25f.
lerp(1.0f, 0.0f, 0.25f)Returns 0.75f; swapping the endpoints changes the direction.
lerp(0.0f, 1.0f)Rejected: an argument is missing (POLE0326).
lerp(0, 1, 0.25f)Rejected: the first two arguments are ints (POLE0327).

Use float(value) when a value is an int and the helper expects a float. The compiler checks user-defined helper arguments against their exact declared types. In particular, the scalar broadcasting supported by maths builtins does not make a scalar argument satisfy a helper's float<2> parameter.

Pure helpers and persistent state#

A Pole helper is pure: it reads its arguments and returns a value. It cannot read or write a processor's state or endpoints directly. It also cannot advance the audio frame. Pass the values it needs, and assign its result in main().

This distinction matters for a filter. The arithmetic for one update can be a helper, while the previous sample remains state owned by the processor:

Filter arithmetic in a helper, memory in the processorPlayground
processor GentleLowpass
{
    input stream float in;
    output stream float out;
    param float amount = 0.1f [0.001f, 1.0f] smooth 20.0f;
    float previous = 0.0f;

    float follow(float sample, float history, float coefficient)
    {
        return history + coefficient * (sample - history);
    }

    void main()
    {
        loop
        {
            previous = follow(in, previous, amount);
            out <- previous;
            advance();
        }
    }
}

history is a copy of the previous output. The assignment to previous happens after the call returns, preserving the new result for the next frame. A small coefficient makes the output follow the input slowly, reducing high frequencies; at 1 it follows immediately. The coefficient is dimensionless, not a cutoff in Hz.

For a reusable effect that owns several state variables, use a processor in a graph. A helper is a good home for arithmetic; a processor is a good home for an oscillator, envelope or filter.

Return on every path#

A typed helper must return a result whichever branch it takes. A final return is often the clearest way to express the ordinary case:

Playground
float clip(float x)
{
    if (x > 1.0f) { return 1.0f; }
    if (x < -1.0f) { return -1.0f; }
    return x;
}

The two early returns handle out-of-range values. The final return handles all values inside the range. A missing return is POLE0351.

return is for helpers. In main(), end the frame with advance(); returning from main() is POLE0352.

Vector arguments and results#

A helper can take and return a stereo value:

Playground
processor SwapChannels
{
    input stream float<2> in;
    output stream float<2> out;

    float<2> swap(float<2> sample)
    {
        return float<2>(sample[1], sample[0]);
    }

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

sample[0] is the left lane and sample[1] the right. The constructor creates a new pair in the opposite order. No array or separate channel state is needed.

How calls compile#

Pole inlines helpers: the compiler places their arithmetic where each call appears. There is no runtime function-call overhead, but calling an expensive helper many times still repeats its work.

A helper can call another helper. clip(clip(in)) is also valid: the inner call returns before the outer one uses its value. Recursion is different: a helper that calls itself, directly or indirectly, is rejected (POLE0603). You also cannot redefine a builtin such as sin or clamp (POLE0350).

Next: maths in a patch explains vector calls and common DSP conversions. For exact argument lists, open the built-in function reference.