Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
fast_math.hpp
Go to the documentation of this file.
1#pragma once
2
5
7
8#include <algorithm> // std::max / std::min, used by clamp()
9#include <bit>
10#include <cstddef>
11#include <cstdint>
12#include <cstring>
13#include <type_traits>
14#include <cmath>
15
16#if defined(__APPLE__) && defined(__clang__)
17#include <simd/simd.h>
18#endif
19
20// Clang's IEEE 754-2019 maximum/minimum builtins propagate NaN, as the
21// AArch64 FMAX/FMIN instructions do. Nested so that a compiler without
22// __has_builtin never parses the call.
23#if defined(__clang__) && defined(__has_builtin)
24#if __has_builtin(__builtin_elementwise_maximum) && __has_builtin(__builtin_elementwise_minimum)
25#define PULP_FAST_MATH_HAS_IEEE_MINMAX 1
26#endif
27#endif
28#ifndef PULP_FAST_MATH_HAS_IEEE_MINMAX
29#define PULP_FAST_MATH_HAS_IEEE_MINMAX 0
30#endif
31
32namespace pulp::signal {
33
48
88struct FastMath {
89
101 template <FastTrigProfile Profile>
102 static float sin_cycles(float phase_cycles) noexcept {
103 if constexpr (Profile == FastTrigProfile::reference) {
104 constexpr float two_pi = 6.28318530717958647692f;
105 return std::sin(two_pi * phase_cycles);
106 } else {
107 float x = phase_cycles;
108 if (x > 0.5f)
109 x -= 1.0f;
110 if (x > 0.25f)
111 x = 0.5f - x;
112 else if (x < -0.25f)
113 x = -0.5f - x;
114 const float x2 = x * x;
115
116 if constexpr (Profile == FastTrigProfile::realtime_efficient) {
117 return x * (6.28250560008354834003487064338056964f +
118 x2 * (-41.1664423308903732524414077000881397f +
119 74.4524187069072428211419543641796188f * x2));
120 } else {
121 static_assert(Profile == FastTrigProfile::realtime_precise);
122 return x *
123 (precise_c0 +
124 x2 *
125 (precise_c1 +
126 x2 *
127 (precise_c2 +
128 x2 *
129 (precise_c3 + precise_c4 * x2))));
130 }
131 }
132 }
133
135 static float sin_cycles(float phase_cycles, FastTrigProfile profile) noexcept {
136 switch (profile) {
142 default: return sin_cycles<FastTrigProfile::reference>(phase_cycles);
143 }
144 }
145
146#if defined(__APPLE__) && defined(__clang__)
151 static simd_float4 sin_cycles_precise(simd_float4 phase_cycles) noexcept {
152 simd_float4 x = phase_cycles - ::simd::floor(phase_cycles + 0.5f);
153 const simd_float4 magnitude = ::simd::abs(x);
154 x = ::simd::copysign(::simd::min(magnitude, 0.5f - magnitude), x);
155 const simd_float4 x2 = x * x;
156 return x *
157 (precise_c0 +
158 x2 *
159 (precise_c1 +
160 x2 *
161 (precise_c2 +
162 x2 *
163 (precise_c3 + precise_c4 * x2))));
164 }
165#endif
166
194 static float tanh(float x) {
195 const float c = tanh_clamp(x);
196 constexpr float kScale = 1.0f - 0x1p-21f;
197 constexpr float a0 = 0.9997622141279201f * kScale;
198 constexpr float a1 = 0.10092608875679396f * kScale;
199 constexpr float a2 = 0.0006321601824242231f * kScale;
200 constexpr float b1 = 0.4335808440239807f;
201 constexpr float b2 = 0.01240458813388444f;
202 const float t = c * c;
203 return c * (a0 + t * (a1 + t * a2)) / (1.0f + t * (b1 + t * b2));
204 }
205
208 static constexpr float kTanhClamp = 5.24f;
209
211 static float tanh_clamp(float x) noexcept {
212#if PULP_FAST_MATH_HAS_IEEE_MINMAX
213 return __builtin_elementwise_minimum(__builtin_elementwise_maximum(x, -kTanhClamp),
214 kTanhClamp);
215#else
216 const float upper = x > kTanhClamp ? kTanhClamp : x;
217 return upper < -kTanhClamp ? -kTanhClamp : upper;
218#endif
219 }
220
242 static float tanh_accurate(float x) {
243 // Clamping x^2 rather than x keeps the sign path short; past |x| = 9
244 // the ratio exceeds 1 and the output clamp saturates it.
245 const float u = std::fmin(x * x, 81.0f);
246 // Ordered compares rather than fmin/fmax: a NaN fails both and
247 // propagates, as std::tanh's does, instead of becoming a full-scale
248 // constant that hides the upstream fault.
249 const float r = tanh_accurate_rational(x, u);
250 const float upper = r > 1.0f ? 1.0f : r;
251 return upper < -1.0f ? -1.0f : upper;
252 }
253
257 static float tanh_accurate_rational(float x, float u) {
258 const float u2 = u * u;
259 // Both polynomials in Estrin form: shorter dependency chains than
260 // Horner, which matters inside a serial per-sample recursion.
261 const float p = (1.0f + u * 0.133803148f) +
262 u2 * ((0.00349475497f + u * 2.05960245e-05f) + u2 * 1.33379685e-08f);
263 const float q = (1.0f + u * 0.467136322f) +
264 u2 * ((0.0258737793f + u * 0.000328429886f) + u2 * 7.7694232e-07f);
265 return x * p / q;
266 }
267
296 static float tanh_minimax(float x) {
297 if (x < -kTanhMinimaxClamp)
298 return -1.0f;
299 if (x > kTanhMinimaxClamp)
300 return 1.0f;
301 return tanh_minimax_rational(x);
302 }
303
317 static void tanh_minimax(const float* input, float* output, std::size_t count) noexcept {
318 std::size_t i = 0;
319#if defined(__clang__) || defined(__GNUC__)
320 using Lanes = float __attribute__((vector_size(16)));
321 constexpr std::size_t kVectors = 4;
322 constexpr std::size_t kStep = kVectors * 4;
323 for (; i + kStep <= count; i += kStep) {
324 Lanes v[kVectors];
325 std::memcpy(v, input + i, sizeof v);
326 for (auto& lane : v) {
327#if defined(__clang__)
328 const Lanes c = __builtin_elementwise_min(
329 __builtin_elementwise_max(lane, Lanes{} - kTanhMinimaxClamp),
330 Lanes{} + kTanhMinimaxClamp);
331#else
332 const Lanes lo = Lanes{} - kTanhMinimaxClamp;
333 const Lanes hi = Lanes{} + kTanhMinimaxClamp;
334 const Lanes floored = lane < lo ? lo : lane;
335 const Lanes c = floored > hi ? hi : floored;
336#endif
337 lane = tanh_minimax_rational(c);
338 }
339 std::memcpy(output + i, v, sizeof v);
340 }
341#endif
342 for (; i < count; ++i)
343 output[i] = tanh_minimax_rational(
344 std::fmin(std::fmax(input[i], -kTanhMinimaxClamp), kTanhMinimaxClamp));
345 }
346
350 template <typename T> static T tanh_minimax_rational(T x) noexcept {
351 constexpr float kScale = 1.0f - 0x1p-21f;
352 const T t = x * x;
353 const T num = x * (kScale + t * (1.2183626499918065e-01f * kScale +
354 t * (2.1714800492144003e-03f * kScale +
355 t * (3.4510574788349538e-06f * kScale))));
356 const T den = 1.0f + t * (4.5515217250939749e-01f +
357 t * (2.0577605545641264e-02f + t * 1.3105688057792639e-04f));
358 return num / den;
359 }
360
379 static float tanh_minimax_recursive(float x) noexcept {
380 if (x < -kTanhMinimaxClamp)
381 return -1.0f;
382 if (x > kTanhMinimaxClamp)
383 return 1.0f;
384 constexpr float kScale = 1.0f - 0x1p-21f;
385 constexpr float n1 = 1.2183626499918065e-01f * kScale;
386 constexpr float n2 = 2.1714800492144003e-03f * kScale;
387 constexpr float n3 = 3.4510574788349538e-06f * kScale;
388 constexpr float d1 = 4.5515217250939749e-01f;
389 constexpr float d2 = 2.0577605545641264e-02f;
390 constexpr float d3 = 1.3105688057792639e-04f;
391 const float t = x * x;
392 const float t2 = t * t;
393 const float num = x * ((kScale + t * n1) + t2 * (n2 + t * n3));
394 const float den = (1.0f + t * d1) + t2 * (d2 + t * d3);
395 return num / den;
396 }
397
398 static constexpr float kTanhMinimaxClamp = 7.0f;
399
402 static float sin(float x) {
403 constexpr float pi = 3.14159265f;
404 constexpr float two_pi = 6.28318530f;
405 // Wrap to [0, 2*pi)
406 x = std::fmod(x, two_pi);
407 if (x < 0) x += two_pi;
408 // Map to [-pi, pi]
409 if (x > pi) x -= two_pi;
410 // Parabolic approximation with correction
411 constexpr float B = 4.0f / pi;
412 constexpr float C = -4.0f / (pi * pi);
413 float y = B * x + C * x * std::abs(x);
414 // Extra precision via Corrected Parabola
415 y = 0.225f * (y * std::abs(y) - y) + y;
416 return y;
417 }
418
420 static float cos(float x) {
421 return sin(x + 1.5707963f); // x + pi/2
422 }
423
435 static float exp2(float x) {
436 return std::exp2(x);
437 }
438
440 static float log2(float x) {
441 // Extract exponent and mantissa via IEEE 754
442 uint32_t bits;
443 std::memcpy(&bits, &x, sizeof(bits));
444 int32_t exponent = static_cast<int32_t>((bits >> 23) & 0xFF) - 127;
445 bits = (bits & 0x007FFFFF) | 0x3F800000; // set exponent to 0 (mantissa in [1,2))
446 float m;
447 std::memcpy(&m, &bits, sizeof(m));
448 // Polynomial for log2(m) where m in [1, 2)
449 float result = static_cast<float>(exponent);
450 m -= 1.0f;
451 result += m * (1.4423904f + m * (-0.7210953f + m * 0.4778948f));
452 return result;
453 }
454
456 static float pow(float base, float exp) {
457 if (base <= 0) return 0;
458 return exp2(exp * log2(base));
459 }
460
462 static float db_to_gain(float db) {
463 return exp2(db * 0.16609640f); // log2(10)/20 ≈ 0.16609640
464 }
465
467 static float gain_to_db(float gain) {
468 if (gain <= 0) return -200.0f;
469 return log2(gain) * 6.0205999f; // 20 / log2(10) ≈ 6.0205999
470 }
471
472 // Branch-free decibel conversions for per-sample dynamics paths: plain
473 // arithmetic and bit manipulation with no calls and no branches, so a loop
474 // over them auto-vectorizes, unlike the libm calls they replace. Accurate
475 // enough to keep a compressor's gain within a few parts in 10^7 of the
476 // libm path (a residual below -120 dBFS), unlike the coarse log2-based
477 // db_to_gain() / gain_to_db() above.
478
488 static float log10_accurate(float x) noexcept {
489 constexpr std::uint32_t kSqrtHalfBits = 0x3f3504f3u; // bits of sqrt(1/2)
490 const std::uint32_t offset = std::bit_cast<std::uint32_t>(x) - kSqrtHalfBits;
491 const float k = static_cast<float>(static_cast<std::int32_t>(offset) >> 23);
492 const float m = std::bit_cast<float>((offset & 0x007fffffu) + kSqrtHalfBits);
493 const float f = m - 1.0f; // exact: m is within a factor of 2 of 1
494 const float s = f / (2.0f + f);
495 const float z = s * s;
496 const float series =
497 z * (1.0f / 3.0f + z * (1.0f / 5.0f + z * (1.0f / 7.0f + z * (1.0f / 9.0f))));
498 const float two_s = 2.0f * s;
499 const float ln_m = two_s * series + two_s;
500 // log10(2) and 1/ln(10). The high part of log10(2) has few enough
501 // significant bits that k * hi is exact for every float exponent.
502 constexpr float kLog10TwoHi = 0.30102539062f; // 0x3e9a2000
503 constexpr float kLog10TwoLo = 4.6050390e-06f; // log10(2) - hi
504 constexpr float kInvLn10 = 0.43429448190f;
505 return k * kLog10TwoHi + (ln_m * kInvLn10 + k * kLog10TwoLo);
506 }
507
521 static float exp10_accurate(float x) noexcept {
522 constexpr float kLog2Ten = 3.32192809489f;
523 constexpr float kLog10TwoHi = 0.30102539062f; // 0x3e9a2000, 11 significant bits
524 constexpr float kLog10TwoLo = 4.6050390e-06f; // log10(2) - hi
525 constexpr float kLnTen = 2.30258509299f;
526 // Past 10^+/-75 (about 250 octaves) the result is inf or 0 anyway;
527 // clamping there keeps |n| <= 250 (so n * hi stays exact) and each half
528 // of 2^n inside the normal exponent range.
529 const float xc = std::fmin(std::fmax(x, -75.0f), 75.0f);
530 const float n = std::nearbyint(xc * kLog2Ten);
531 const float d = (xc - n * kLog10TwoHi) - n * kLog10TwoLo;
532 const float t = d * kLnTen;
533 const float p =
534 1.0f +
535 t * (1.0f + t * (0.5f + t * (1.0f / 6.0f +
536 t * (1.0f / 24.0f +
537 t * (1.0f / 120.0f +
538 t * (1.0f / 720.0f + t * (1.0f / 5040.0f)))))));
539 const std::int32_t e = static_cast<std::int32_t>(n);
540 const std::int32_t e_half = e / 2;
541 const float scale_a = std::bit_cast<float>(static_cast<std::uint32_t>(e_half + 127) << 23);
542 const float scale_b =
543 std::bit_cast<float>(static_cast<std::uint32_t>(e - e_half + 127) << 23);
544 const float result = (p * scale_a) * scale_b;
545 return x == x ? result : x;
546 }
547
550 static float gain_to_db_accurate(float gain) noexcept {
551 return 20.0f * log10_accurate(gain);
552 }
553
555 static float db_to_gain_accurate(float db) noexcept {
556 return exp10_accurate(db / 20.0f);
557 }
558
560 static float rcp(float x) {
561 // One Newton-Raphson iteration from initial estimate
562 float estimate = 1.0f / x; // compiler will often use rcpss
563 return estimate;
564 }
565
567 static float rsqrt(float x) {
568 // Quake-style fast inverse sqrt with one NR iteration
569 uint32_t i;
570 std::memcpy(&i, &x, sizeof(i));
571 i = 0x5f3759df - (i >> 1);
572 float y;
573 std::memcpy(&y, &i, sizeof(y));
574 y = y * (1.5f - 0.5f * x * y * y); // Newton-Raphson
575 return y;
576 }
577
578private:
579 static constexpr float precise_c0 =
580 6.28318527379078585274731929079414949f;
581 static constexpr float precise_c1 =
582 -41.3416774783915252855640244027643612f;
583 static constexpr float precise_c2 =
584 81.6022312427274226421465134076212909f;
585 static constexpr float precise_c3 =
586 -76.5749921819992128192000934020817094f;
587 static constexpr float precise_c4 =
588 39.7109181438058471453004860893416233f;
589
590public:
591
593 static float clamp_unit(float x) {
594 return std::max(-1.0f, std::min(1.0f, x));
595 }
596
598 static float soft_clip(float x) {
599 if (x <= -1.5f) return -1.0f;
600 if (x >= 1.5f) return 1.0f;
601 return x - (x * x * x) / 6.75f;
602 }
603};
604
631
644#if defined(PULP_SIGNAL_LADDER_SATURATION)
645static_assert(PULP_SIGNAL_LADDER_SATURATION == 0 || PULP_SIGNAL_LADDER_SATURATION == 1 ||
646 PULP_SIGNAL_LADDER_SATURATION == 2,
647 "PULP_SIGNAL_LADDER_SATURATION must be 0 (exact), 1 (accurate) or "
648 "2 (realtime_efficient)");
650 PULP_SIGNAL_LADDER_SATURATION == 0 ? LadderSaturation::exact
651 : PULP_SIGNAL_LADDER_SATURATION == 2 ? LadderSaturation::realtime_efficient
653#elif defined(PULP_SIGNAL_FAST_LADDER_TANH)
655 PULP_SIGNAL_FAST_LADDER_TANH ? LadderSaturation::realtime_efficient : LadderSaturation::exact;
656#else
658#endif
659
666template <auto Saturation>
667 requires std::is_same_v<decltype(Saturation), LadderSaturation>
668inline float ladder_tanh_profiled(float x) {
669 if constexpr (Saturation == LadderSaturation::realtime_efficient)
671 else if constexpr (Saturation == LadderSaturation::accurate)
672 return FastMath::tanh_accurate(x);
673 else
674 return std::tanh(x);
675}
676
679template <auto RealtimeEfficient>
680 requires std::is_same_v<decltype(RealtimeEfficient), bool>
681[[deprecated("use ladder_tanh_profiled<LadderSaturation>")]]
682inline float ladder_tanh_profiled(float x) {
685}
686
688inline float ladder_tanh(float x) {
690}
691
710inline double lipschitz_tanh(double v) noexcept {
711 const double x = std::clamp(v, -3.0, 3.0);
712 const double x2 = x * x;
713 return x * (27.0 + x2) / (27.0 + 9.0 * x2);
714}
715
716namespace detail {
717
724template <typename T> inline T select_bits(bool condition, T if_true, T if_false) noexcept {
725 static_assert(sizeof(T) == 4 || sizeof(T) == 8);
726 using U = std::conditional_t<sizeof(T) == 4, std::uint32_t, std::uint64_t>;
727 const U mask = U{0} - static_cast<U>(condition);
728 return std::bit_cast<T>((std::bit_cast<U>(if_true) & mask) |
729 (std::bit_cast<U>(if_false) & ~mask));
730}
731
733template <typename T> inline T snap_to_zero_bits(T x) noexcept {
734#if PULP_DSP_ENABLE_SNAP_TO_ZERO
735 return select_bits(std::abs(x) < snap_threshold<T>(), T(0), x);
736#else
737 return x;
738#endif
739}
740
743inline float tanh_accurate_bits(float x) noexcept {
744 const float u = std::fmin(x * x, 81.0f);
745 const float r = FastMath::tanh_accurate_rational(x, u);
746 const float upper = select_bits(r > 1.0f, 1.0f, r);
747 return select_bits(upper < -1.0f, -1.0f, upper);
748}
749
750} // namespace detail
751
752} // namespace pulp::signal
T select_bits(bool condition, T if_true, T if_false) noexcept
condition ?
Definition fast_math.hpp:724
float tanh_accurate_bits(float x) noexcept
FastMath::tanh_accurate() with its clamps as bit operations; the same value for every input.
Definition fast_math.hpp:743
T snap_to_zero_bits(T x) noexcept
snap_to_zero() as bit operations (see select_bits()).
Definition fast_math.hpp:733
Definition additive_bank.hpp:218
constexpr T snap_threshold()
Threshold in absolute value below which a sample is treated as zero.
Definition denormal.hpp:36
double lipschitz_tanh(double v) noexcept
A [3/2] Padé tanh that is 1-LIPSCHITZ and bounded — the saturator to reach for inside a feedback loop...
Definition fast_math.hpp:710
float ladder_tanh(float x)
The ladder's default float saturator.
Definition fast_math.hpp:688
@ c
Definition spectrum_trace.hpp:35
FastTrigProfile
Semantic accuracy choices for bounded-cycle real-time trigonometry.
Definition fast_math.hpp:38
@ realtime_efficient
Lower-cost float path with maximum absolute error at most 1.2e-4 over one bounded cycle.
Definition fast_math.hpp:43
@ reference
Platform sine; the compatibility default.
Definition fast_math.hpp:40
@ realtime_precise
Float-precision path with maximum absolute error at most 2.5e-7 over one bounded cycle.
Definition fast_math.hpp:46
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
LadderSaturation
Saturator choices for the Moog ladder's per-stage non-linearity.
Definition fast_math.hpp:630
@ exact
Definition fast_math.hpp:630
@ accurate
Definition fast_math.hpp:630
@ realtime_efficient
Definition fast_math.hpp:630
constexpr LadderSaturation kDefaultLadderSaturation
Selecting the default ladder saturator at build time:
Definition fast_math.hpp:657
float ladder_tanh_profiled(float x)
The ladder's float saturator for an explicit choice.
Definition fast_math.hpp:668
Math helpers optimized for audio DSP.
Definition fast_math.hpp:88
static float tanh_minimax(float x)
Constrained-minimax [7/6] rational tanh: two more coefficients per polynomial than tanh()'s [5/4],...
Definition fast_math.hpp:296
static float rsqrt(float x)
Fast inverse square root (1/sqrt(x)).
Definition fast_math.hpp:567
static constexpr float kTanhMinimaxClamp
Definition fast_math.hpp:398
static float gain_to_db(float gain)
Fast linear gain to dB: 20 * log10(gain) = 20 * log2(gain) / log2(10).
Definition fast_math.hpp:467
static float tanh_minimax_recursive(float x) noexcept
tanh_minimax() for a serial recursion: the same fit, clamp and numerator scale, with each cubic evalu...
Definition fast_math.hpp:379
static float log10_accurate(float x) noexcept
log10(x) for finite, positive, normal x, within 4 ulps of the correctly rounded result (3....
Definition fast_math.hpp:488
static float pow(float base, float exp)
Fast pow(base, exp) via exp2(exp * log2(base)).
Definition fast_math.hpp:456
static constexpr float kTanhClamp
tanh() clamps its argument to [-kTanhClamp, kTanhClamp], where its rational reaches 1.
Definition fast_math.hpp:208
static float rcp(float x)
Fast reciprocal (1/x) via Newton-Raphson with SSE hint.
Definition fast_math.hpp:560
static float tanh_accurate_rational(float x, float u)
The unsaturated rational inside tanh_accurate(): x * P(u) / Q(u) with u = min(x^2,...
Definition fast_math.hpp:257
static float soft_clip(float x)
Soft clipping (polynomial saturation, no discontinuity).
Definition fast_math.hpp:598
static float cos(float x)
Fast cos approximation. Input in radians.
Definition fast_math.hpp:420
static float sin_cycles(float phase_cycles) noexcept
Computes sine for a normalized phase in [0, 1).
Definition fast_math.hpp:102
static float clamp_unit(float x)
Clamp to [-1, 1] without branching (for saturation).
Definition fast_math.hpp:593
static float tanh_accurate(float x)
tanh within 3.0e-7 absolute of std::tanh when the compiler fuses multiply-adds, and within 3....
Definition fast_math.hpp:242
static float db_to_gain_accurate(float db) noexcept
10^(dB / 20), within exp10_accurate()'s bound.
Definition fast_math.hpp:555
static float log2(float x)
Fast log2 approximation (max error ~0.007 for x > 0).
Definition fast_math.hpp:440
static float tanh(float x)
Fast tanh: an odd [5/4] rational, within 5.7e-5 of std::tanh for every float input,...
Definition fast_math.hpp:194
static float sin_cycles(float phase_cycles, FastTrigProfile profile) noexcept
Runtime profile dispatch for setup, control, and non-inner-loop use.
Definition fast_math.hpp:135
static float tanh_clamp(float x) noexcept
tanh()'s argument clamp, propagating NaN.
Definition fast_math.hpp:211
static float db_to_gain(float db)
Fast dB to linear gain: 10^(dB/20) = 2^(dB * log2(10)/20).
Definition fast_math.hpp:462
static T tanh_minimax_rational(T x) noexcept
The rational inside tanh_minimax() for an argument already clamped to [-7, 7], generic over float and...
Definition fast_math.hpp:350
static void tanh_minimax(const float *input, float *output, std::size_t count) noexcept
Block form of tanh_minimax(); output may equal input.
Definition fast_math.hpp:317
static float sin(float x)
Fast sin approximation (Bhaskara-style, max error ~0.001 for any x).
Definition fast_math.hpp:402
static float exp10_accurate(float x) noexcept
10^x within 1.2e-7 relative of the correctly rounded result, with or without fused multiply-add.
Definition fast_math.hpp:521
static float gain_to_db_accurate(float gain) noexcept
20 log10(gain) for a finite, positive, normal gain (clamp to a floor first); within log10_accurate()'...
Definition fast_math.hpp:550
static float exp2(float x)
Computes 2^x with the platform's standard float exp2 semantics.
Definition fast_math.hpp:435