The language

Parameters

An input that carries a name, a default, a range — and smoothing, which is not optional in practice.

Anyone whose patch will be driven by a host, a knob or a slider.

A param is an input endpoint that also carries a name, a default and a range, so a host can enumerate a patch's controls instead of being told the layout out of band.

Read a declaration#

Think of a parameter as a labelled socket the host can control. Its name is how you address it; its initial value is where a new instance starts; its range tells the host what values the control is intended to cover.

param float gain = 0.5f [0.0f, 1.0f] smooth 20.0f;
            name  default  min   max   time constant in ms
PartMeaningIn this example
gainName used in the patch and by the hostRead it in out <- in * gain;.
0.5fInitial valueStart at half amplitude.
[0.0f, 1.0f]Declared control rangeA control from silence to unity gain.
smooth 20.0fOptional smoothing time constant, in millisecondsApproach a new target gradually.

The units of a parameter come from how you use it. A value named time is not automatically seconds, and a value named cutoff is not automatically hertz. Give controls descriptive names and document the unit next to their range. Validate a computed value where the algorithm needs a hard bound, particularly before division, table indexing or feedback.

Playground
param float cutoff = 800.0f [40.0f, 12000.0f];
param float depth  = 0.0f   [-1.0f, 1.0f];
param float amount;                            // default 0, no range
param float gain   = 0.5f   [0.0f, 1.0f] smooth 20.0f;

polec --params prints them, --param=cutoff=400 sets one, and unset ones take their declared default.

stream is implied, and there is no separate "value" kind. A parameter that only updated between blocks would not be sample-accurate, and everything else here is.

Smooth continuous controls#

Without it, a stepped parameter steps — and a step in a cutoff or a gain is a click.

Drag the cutoff: smoothed, so it does not clickPlayground
processor Smoothed
{
    input  stream float in;
    output stream float out;

    param  float cutoff = 800.0f [80.0f, 8000.0f] smooth 20.0f;

    float z = 0.0f;

    void main()
    {
        loop
        {
            // A one-pole lowpass. The coefficient follows the smoothed cutoff.
            let a = clamp (cutoff / 24000.0f, 0.001f, 0.9f);
            z = z + a * (in - z);
            out <- z;
            advance();
        }
    }
}

smooth 20.0f is a 20 ms time constant. The compiler inserts a one-pole into a hidden slot, seeds it to the parameter's default so the patch starts settled rather than sliding up from zero, and computes the coefficient from processor.frequency at run time — so the smoothing is 20 ms at 44.1 kHz and 20 ms at 96 kHz, rather than a different number of milliseconds at each.

Measured: 63.2% of the way after one time constant, and the largest single-sample change on a 0 → 0.8 step falls from 0.8 to 0.00085.

Time constant is not a finish time#

A 20 ms time constant does not mean the target is reached exactly after 20 ms. The remaining distance shrinks exponentially: about 63% of the change happens in one time constant and about 95% in three. This is a useful compromise for many knobs, but a deliberately sharp gate or trigger has a different job.

Try it: compare a gain with and without smoothing while it multiplies a steady tone. The difference is easiest to hear on a sudden large change. For a moving delay, smoothing the control is only half the job: the read itself also needs interpolation, as the chorus shows.

Modulation is arithmetic#

There is no modulation system, because there is no block rate to be coarser than. main() runs once per sample, so anything computed in it is per-sample.

Playground
param float base  = 400.0f [40.0f, 8000.0f];
param float depth = 0.0f   [-1.0f, 1.0f];
input stream float lfo;

// Exponential, so it tracks pitch: a semitone is a ratio, and a linear
// offset detunes low notes far more than high ones.
let cutoff = base * pow (2.0f, lfo * depth);
out <- in * 0.0f + cutoff * 0.0f;   // (shown for the expression, not the sound)

That is checkable rather than a claim. Phase-modulate a 400 Hz carrier with a 100 Hz source at index 2 and the sidebands land on the Bessel amplitudes J_n(2) to four decimal places — a spectrum block-rate modulation cannot produce. The practical consequence: an LFO taken to audio rate becomes real FM with nothing switched over, which modmatrix.pole shows by changing one rate parameter from 3 Hz to 110 Hz.

A modulation matrix is a loop over two arrays, not a language feature. Adding a source means growing an array rather than editing any routing.

Two rules worth keeping#

  • Modulate pitch and cutoff exponentially, not linearly.
  • Smooth continuous controls where steps are audible. A gain or cutoff usually benefits; a gate, trigger or discrete mode needs its intended timing and should not automatically be treated as a continuous value.