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| Part | Meaning | In this example |
|---|---|---|
gain | Name used in the patch and by the host | Read it in out <- in * gain;. |
0.5f | Initial value | Start at half amplitude. |
[0.0f, 1.0f] | Declared control range | A control from silence to unity gain. |
smooth 20.0f | Optional smoothing time constant, in milliseconds | Approach 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.
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.
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.
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.