Built-in maths
What the builtins are, how they behave on vectors and ints, and the flag that trades a few ulp for speed.
Anyone reaching for sin, exp or clamp.
The full list lives on the built-in maths reference, which is generated by asking the compiler rather than by transcribing a list. This page is about how they behave.
Start with the question your expression answers#
“How far through this cycle am I?” and “how fast does it repeat?” are related questions, but their answers have different units. Many DSP mistakes are ordinary arithmetic applied to the wrong kind of value. Keep the units in your variable names while learning.
let frequencyHz = 220.0f;
let cyclesPerSample = frequencyHz / processor.frequency;
let radiansPerSample = cyclesPerSample * 6.2831853f;
out <- radiansPerSample;This fragment computes an increment; it does not generate a tone by itself.
An oscillator stores a phase, adds this increment every sample and calls
sin(phase). The oscillator chapter
puts those pieces together.
Three useful conversions#
| Question | Expression | Example |
|---|---|---|
| How many samples is this duration? | seconds * processor.frequency | 0.25 seconds at 48 kHz is 12,000 samples. |
| What gain corresponds to this amplitude change in dB? | pow(10.0f, decibels / 20.0f) | -6 dB is approximately half amplitude. |
| What frequency is this many semitones above a note? | baseHz * pow(2.0f, semitones / 12.0f) | 12 semitones above 220 Hz is 440 Hz. |
These are expressions built from primitives, not extra named builtins. If you use one in several places, turn it into a helper.
float decibelsToGain(float decibels)
{
return pow(10.0f, decibels / 20.0f);
}Call decibelsToGain(-6.0f) to get approximately 0.5012f. Multiplying audio
by that value changes its amplitude; it does not subtract six from the sample.
Keep the domain in view#
Some functions accept any ordinary finite sample; others describe only a
part of the number line. log and log10 need positive inputs. sqrt needs
a non-negative input. pow with a fractional exponent generally needs a
positive base for a real result.
For a decibel meter, use 20.0f * log10(max(abs(in), 0.000001f)) so silence
lands on a finite floor. That expression produces a level value, not audio to
send to a speaker. Input-domain protection is part of the algorithm; compiling
successfully does not prove that every runtime value will be sensible.
Elementwise, with broadcasting#
Every maths builtin is elementwise on vectors, and a scalar broadcasts into it:
input stream float<2> in;
output stream float<2> out;
out <- clamp (in, -1.0f, 1.0f); // one call, not one per lanemin, max and clamp take ints#
They are the only builtins that do. Ordering and bounding are not float ideas,
and clamping an integer index is exactly what an array read needs — writing
int(min(float(i), float(n))) to say that would be the sort of workaround this
language exists not to have.
Their arguments must agree, all int or all float (POLE0327), for the same
reason 1 + 1.0f is rejected. The rest stay float-only: sqrt of an int
returning an int implies a rounding rule nobody asked for.
--approx-math#
sin, exp and tanh can be compiled as inline polynomials instead of calls
into the platform's libm:
./build/polec examples/synth.pole --approx-math --emit=wav --out=/tmp/out.wavSeveral times faster where a patch leans on them, a few ulp less accurate, and — the part that matters more than the speed — the same arithmetic on every target, which libm never was. It also produces markedly smaller modules, which is why the browser build uses it.
Use it when a patch is transcendental-heavy and you care that macOS, Linux and WebAssembly agree to the bit. Leave it off when you want the platform's own accuracy.