Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
special_functions.hpp
Go to the documentation of this file.
1#pragma once
2
3// Special mathematical functions for DSP and filter design.
4//
5// RT contract: the scalar free functions and nested `special::*` helpers are
6// stateless and allocate no memory. They may be computationally expensive and
7// are often better suited to prepare/design paths, but allocation behavior is
8// fixed.
9
10#include <algorithm>
11#include <cmath>
12#include <limits>
13
14namespace pulp::signal {
15
17inline float sinc(float x) {
18 if (std::abs(x) < 1e-7f) return 1.0f;
19 float px = 3.14159265358979f * x;
20 return std::sin(px) / px;
21}
22
25inline float bessel_i0(float x) {
26 float sum = 1.0f;
27 float term = 1.0f;
28 float x_half = x * 0.5f;
29
30 for (int k = 1; k < 25; ++k) {
31 term *= (x_half / static_cast<float>(k));
32 term *= (x_half / static_cast<float>(k));
33 sum += term;
34 if (term < sum * 1e-10f) break;
35 }
36 return sum;
37}
38
40inline float gamma_fn(float x) {
41 return std::tgamma(x);
42}
43
45inline float erf_fn(float x) {
46 return std::erf(x);
47}
48
50inline float erfc_fn(float x) {
51 return std::erfc(x);
52}
53
55inline float lanczos(float x, int a) {
56 if (std::abs(x) < 1e-7f) return 1.0f;
57 if (std::abs(x) >= static_cast<float>(a)) return 0.0f;
58 return sinc(x) * sinc(x / static_cast<float>(a));
59}
60
62inline float db_to_linear(float db) {
63 return std::pow(10.0f, db / 20.0f);
64}
65
67inline float linear_to_db(float linear) {
68 return 20.0f * std::log10(std::max(linear, 1e-10f));
69}
70
72inline float freq_to_midi(float freq) {
73 return 69.0f + 12.0f * std::log2(freq / 440.0f);
74}
75
77inline float midi_to_freq(float note) {
78 return 440.0f * std::pow(2.0f, (note - 69.0f) / 12.0f);
79}
80
81// ---------------------------------------------------------------------------
82// Elliptic / Jacobi special functions
83//
84// These primitives are needed by elliptic (Cauer) IIR filter design and by
85// future analog-prototype filter routines. They live in a nested namespace
86// so the surface is self-contained and easy to reference from filter design
87// code (e.g. `pulp::signal::special::elliptic_K(m)`), without colliding with
88// the existing free functions above.
89//
90// All routines are header-only and `double`-precision because filter design
91// requires more headroom than the audio path: coefficient quantization is
92// the bottleneck, so we keep design-time math in double and downcast at
93// the biquad-cascade boundary.
94//
95// References:
96// * Abramowitz & Stegun, "Handbook of Mathematical Functions", §17.
97// * Numerical Recipes in C, §6.11 (elliptic integrals and functions).
98// * Vaidyanathan, "Multirate Systems and Filter Banks", §3.
99//
100// All functions take the *parameter* m = k^2 (Abramowitz convention), not
101// the modulus k. Callers used to SciPy's `ellipk(m)` or MATLAB's
102// `ellipke(m)` will find this familiar.
103// ---------------------------------------------------------------------------
104
105namespace special {
106
108inline constexpr double kPi = 3.141592653589793238462643383279502884;
109
119inline double elliptic_K(double m) {
120 if (m < 0.0 || m >= 1.0) {
121 if (m == 1.0) return std::numeric_limits<double>::infinity();
122 // Caller passed something out of [0, 1); clamp into the valid range
123 // rather than NaN-poisoning downstream filter design code. For m < 0
124 // there are extension formulas, but we currently have no caller that
125 // needs them.
126 if (m < 0.0) m = 0.0;
127 else m = std::nextafter(1.0, 0.0);
128 }
129 double a = 1.0;
130 double b = std::sqrt(1.0 - m);
131 for (int i = 0; i < 60; ++i) {
132 const double a_next = 0.5 * (a + b);
133 const double b_next = std::sqrt(a * b);
134 if (std::abs(a - b) < 1e-16 * std::abs(a)) {
135 a = a_next;
136 break;
137 }
138 a = a_next;
139 b = b_next;
140 }
141 return kPi / (2.0 * a);
142}
143
153inline double elliptic_E(double m) {
154 if (m <= 0.0) return kPi / 2.0;
155 if (m >= 1.0) return 1.0;
156 double a = 1.0;
157 double b = std::sqrt(1.0 - m);
158 double c = std::sqrt(m); // c_0
159 double pow2 = 0.5; // 2^{n-1} for n=0
160 double sum = pow2 * c * c; // first term: m/2
161 for (int i = 1; i < 60; ++i) {
162 const double a_next = 0.5 * (a + b);
163 const double b_next = std::sqrt(a * b);
164 c = 0.5 * (a - b); // c_i
165 a = a_next;
166 b = b_next;
167 pow2 *= 2.0; // now 2^{i-1}
168 sum += pow2 * c * c;
169 if (std::abs(c) < 1e-17 * std::abs(a)) break;
170 }
171 const double K = kPi / (2.0 * a);
172 return K * (1.0 - sum);
173}
174
180inline double jacobi_nome(double m) {
181 if (m <= 0.0) return 0.0;
182 if (m >= 1.0) return 1.0;
183 const double K = elliptic_K(m);
184 const double Kp = elliptic_K(1.0 - m);
185 return std::exp(-kPi * Kp / K);
186}
187
201inline void jacobi_sncndn(double u, double m,
202 double* sn_out, double* cn_out, double* dn_out) {
203 if (m <= 0.0) {
204 if (sn_out) *sn_out = std::sin(u);
205 if (cn_out) *cn_out = std::cos(u);
206 if (dn_out) *dn_out = 1.0;
207 return;
208 }
209 if (m >= 1.0) {
210 const double th = std::tanh(u);
211 const double sech = 1.0 / std::cosh(u);
212 if (sn_out) *sn_out = th;
213 if (cn_out) *cn_out = sech;
214 if (dn_out) *dn_out = sech;
215 return;
216 }
217
218 // Numerical Recipes 3rd ed. §6.12 `sncndn`. The variable names stay
219 // close to the published code so it's easy to cross-check, but Pulp
220 // uses descriptive comments instead of one-letter mystery.
221 constexpr int kMaxN = 14;
222 constexpr double kEps = 1.0e-15;
223 double em[kMaxN]; // sequence of a values (AGM upper)
224 double en[kMaxN]; // sequence of complementary moduli
225 double emc = 1.0 - m; // complementary parameter (1 - m)
226 double a = 1.0;
227 double dn = 1.0;
228 double c = 0.0;
229 int last_i = 0;
230 for (int i = 0; i < kMaxN; ++i) {
231 last_i = i;
232 em[i] = a;
233 emc = std::sqrt(emc);
234 en[i] = emc;
235 c = 0.5 * (a + emc);
236 if (std::abs(a - emc) <= kEps * a) break;
237 emc = emc * a; // emc <- a * emc
238 a = c;
239 }
240 u *= c; // u <- final c * u (not a * u)
241 double sn = std::sin(u);
242 double cn = std::cos(u);
243 if (sn != 0.0) {
244 a = cn / sn;
245 c *= a;
246 for (int i = last_i; i >= 0; --i) {
247 const double b = em[i];
248 a *= c;
249 c *= dn;
250 dn = (en[i] + a) / (b + a);
251 a = c / b;
252 }
253 a = 1.0 / std::sqrt(c * c + 1.0);
254 sn = (sn >= 0.0) ? a : -a;
255 cn = sn * c;
256 }
257 if (sn_out) *sn_out = sn;
258 if (cn_out) *cn_out = cn;
259 if (dn_out) *dn_out = dn;
260}
261
263inline double jacobi_sn(double u, double m) {
264 double sn = 0.0;
265 jacobi_sncndn(u, m, &sn, nullptr, nullptr);
266 return sn;
267}
268
270inline double jacobi_cn(double u, double m) {
271 double cn = 0.0;
272 jacobi_sncndn(u, m, nullptr, &cn, nullptr);
273 return cn;
274}
275
277inline double jacobi_dn(double u, double m) {
278 double dn = 0.0;
279 jacobi_sncndn(u, m, nullptr, nullptr, &dn);
280 return dn;
281}
282
296inline double carlson_RF(double x, double y, double z) {
297 if (x < 0.0) x = 0.0;
298 if (y < 0.0) y = 0.0;
299 if (z < 0.0) z = 0.0;
300 constexpr double kErrTol = 0.0025; // ~ machine eps^(1/6); NR recommendation.
301 for (int i = 0; i < 60; ++i) {
302 const double sqrt_x = std::sqrt(x);
303 const double sqrt_y = std::sqrt(y);
304 const double sqrt_z = std::sqrt(z);
305 const double lambda = sqrt_x * sqrt_y + sqrt_y * sqrt_z + sqrt_z * sqrt_x;
306 x = 0.25 * (x + lambda);
307 y = 0.25 * (y + lambda);
308 z = 0.25 * (z + lambda);
309 const double avg = (x + y + z) / 3.0;
310 const double dx = (avg - x) / avg;
311 const double dy = (avg - y) / avg;
312 const double dz = (avg - z) / avg;
313 const double max_d = std::max({std::abs(dx), std::abs(dy), std::abs(dz)});
314 if (max_d < kErrTol) {
315 const double e2 = dx * dy - dz * dz;
316 const double e3 = dx * dy * dz;
317 // 7th-order series expansion (NR eq. 6.12.17):
318 return (1.0 + e2 * (-0.1 + e2 / 24.0 - 3.0 * e3 / 44.0)
319 + e3 / 14.0) / std::sqrt(avg);
320 }
321 }
322 return 1.0 / std::sqrt((x + y + z) / 3.0); // fallback
323}
324
336inline double jacobi_asn(double x, double m) {
337 if (x > 1.0) x = 1.0;
338 if (x < -1.0) x = -1.0;
339 if (m <= 0.0) return std::asin(x);
340 if (m >= 1.0) return std::atanh(x);
341 const double y = 1.0 - x * x;
342 const double z = 1.0 - m * x * x;
343 return x * carlson_RF(y, z, 1.0);
344}
345
346} // namespace special
347
348} // namespace pulp::signal
Definition special_functions.hpp:105
double jacobi_nome(double m)
Jacobi nome q(m) = exp(-pi * K(1 - m) / K(m)).
Definition special_functions.hpp:180
double jacobi_dn(double u, double m)
Convenience: dn(u, m).
Definition special_functions.hpp:277
double jacobi_asn(double x, double m)
Inverse Jacobi sn: returns u such that sn(u, m) = x, for x in [-1, 1].
Definition special_functions.hpp:336
double elliptic_K(double m)
Complete elliptic integral of the first kind, K(m), m = k^2 in [0, 1).
Definition special_functions.hpp:119
double jacobi_cn(double u, double m)
Convenience: cn(u, m).
Definition special_functions.hpp:270
double elliptic_E(double m)
Complete elliptic integral of the second kind, E(m), m = k^2 in [0, 1].
Definition special_functions.hpp:153
double carlson_RF(double x, double y, double z)
Carlson's symmetric elliptic integral of the first kind, RF(x, y, z).
Definition special_functions.hpp:296
constexpr double kPi
Pi as a double-precision constant. Used by every routine below.
Definition special_functions.hpp:108
double jacobi_sn(double u, double m)
Convenience: sn(u, m).
Definition special_functions.hpp:263
void jacobi_sncndn(double u, double m, double *sn_out, double *cn_out, double *dn_out)
Jacobi elliptic functions sn(u, m), cn(u, m), dn(u, m) — all three at once because the AGM-based sche...
Definition special_functions.hpp:201
Definition additive_bank.hpp:218
float db_to_linear(float db)
dB to linear gain conversion
Definition special_functions.hpp:62
float sinc(float x)
Sinc function: sin(pi*x) / (pi*x), with sinc(0) = 1.
Definition special_functions.hpp:17
float midi_to_freq(float note)
MIDI note number to frequency.
Definition special_functions.hpp:77
float lanczos(float x, int a)
Lanczos kernel (used in high-quality resampling).
Definition special_functions.hpp:55
@ c
Definition spectrum_trace.hpp:35
float linear_to_db(float linear)
Linear gain to dB conversion.
Definition special_functions.hpp:67
float bessel_i0(float x)
Modified Bessel function of the first kind, order 0 Used in Kaiser window design.
Definition special_functions.hpp:25
float freq_to_midi(float freq)
Frequency to MIDI note number (A4 = 69).
Definition special_functions.hpp:72
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
float gamma_fn(float x)
Gamma function approximation (Stirling's for large x, series for small).
Definition special_functions.hpp:40
float erfc_fn(float x)
Complementary error function.
Definition special_functions.hpp:50
float erf_fn(float x)
Error function.
Definition special_functions.hpp:45