fast_math.h
Cheap approximations of the expensive functions.
#include <sonore/fast_math.h>- 7other declarations
- Guides: Getting started, MIDI and MPE
On this page
Design notes
What they are for
std::tanh is a saturating waveshaper's whole inner loop, and it costs on the order of forty cycles. At 96 kHz with eight times oversampling that is thirty million calls a second per voice, and the difference between a polyphonic synth that runs and one that does not.
The honest way to ship an approximation
Say what the error IS, over what range, and check it. An approximation documented as "fast" and nothing else is one nobody can decide whether to use: the question is never "is it accurate" but "is it accurate enough for what I am doing", and that cannot be answered without a number.
Every function here states its maximum absolute error over its stated range, and the tests measure that number across the range rather than at a handful of points -- an approximation checked at three values is one whose worst case is wherever nobody looked.
They are approximations, not replacements
Nothing in this SDK uses these by default. A filter coefficient computed with fastExp would be subtly wrong forever; a saturator using fastTanh is indistinguishable by ear. The choice belongs to whoever writes the DSP, and making it for them silently is how a plugin ends up detuned.
Functions 7
- fn
inline float fastTanh(float x)tanh, to within 2.5e-4 over [-5, 5].fast_math.h:51More
A Padé-style rational approximation: the ratio of two cubics, which is two multiplies and a divide against std::tanh's exponential. Beyond the range it saturates to ±1, which is what tanh does anyway to five decimal places past ±5, so the clamp is not a compromise -- it is the correct answer arrived at cheaply.
The shape matters as much as the error for a waveshaper: this is monotonic and odd-symmetric like the real thing, so it cannot fold back on itself and cannot introduce even harmonics that were not asked for.
- fn
inline float fastExp(float x)exp, to within 2e-5 relative over [-8, 8].fast_math.h:68More
Splits the exponent into an integer power of two -- assembled directly into the float's bits, which is free -- and a remainder in [-0.5, 0.5] handled by a short polynomial. The bit assembly is what makes it fast; a polynomial over the whole range would need far more terms for the same error.
- fn
inline float fastSin(float x)sin, to within 3e-7 over [-pi, pi] (measured 1.8e-7).fast_math.h:105More
A five-term odd polynomial -- odd because sin is, so the approximation cannot drift asymmetric and put a DC offset into an oscillator that should have none -- fitted to minimise the worst error over [0, pi/2], and only ever evaluated there: sin(pi - x) == sin(x) folds the rest of the circle in. Unfolded, the same series run out to pi was 6.9e-3 off, about -43 dB of harmonic distortion on a held sine.
Outside the range it WRAPS rather than clamping: an oscillator's phase is unbounded and a sine that flattened past pi would be a square wave with extra steps. The wrap costs what the float's resolution of x costs -- at x = 1000 that is 6e-5 -- so keep a phase small and it stays exact.
- fn
inline float fastCos(float x)cos, to the same accuracy as fastSin, by the identity that defines it.fast_math.h:121 - fn
inline float fastLog(float x)A natural logarithm, to within 1e-5 absolute over every positive normal float, and 2e-6 between 1e-6 and 1e6 (measured 7.9e-6 and 1.0e-6). What is left is the float's own resolution of the answer: ln(1e38) is 87.5, where one step of a float is 7.6e-6.fast_math.h:145More
The exponent comes straight out of the float's bits -- exact, and free -- and the mantissa is brought into [sqrt(1/2), sqrt(2)), where ln(m) = 2 atanh((m - 1) / (m + 1)) and the atanh series converges after five terms. A Taylor series of ln(1 + m) taken to m = 1, which is what this was, converges there hardly at all: 0.076 off, 0.66 dB in fastGainToDb.
Zero, negatives, NaN and subnormals return the log of the smallest normal float rather than -infinity or a NaN: a NaN in an audio buffer propagates through every subsequent sample and every mix bus it reaches, and a very negative decibel value is what a caller asking for the log of silence meant.
- fn
inline float fastGainToDb(float gain)Decibels from a gain, through fastLog: within 2e-5 dB from -100 to +12 dB. The shape a meter needs sixty times a second.fast_math.h:168 - fn
inline float fastDbToGain(float db)And back. 1/20 * ln(10) folded into the constant.fast_math.h:171