Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
iir_design.hpp
Go to the documentation of this file.
1#pragma once
2
22
26
27#include <cmath>
28#include <complex>
29#include <span>
30#include <vector>
31#include <algorithm>
32
33namespace pulp::signal {
34
52struct IirDesign {
54
55 // ── Chebyshev Type I ─────────────────────────────────────────────────
56
65 static std::vector<Coefficients>
66 chebyshev1_lowpass(int order, float freq_hz, float ripple_db, float sample_rate) {
67 return cheb1_cascade(order, freq_hz, ripple_db, sample_rate, /*highpass=*/false);
68 }
69
71 static std::vector<Coefficients>
72 chebyshev1_highpass(int order, float freq_hz, float ripple_db, float sample_rate) {
73 return cheb1_cascade(order, freq_hz, ripple_db, sample_rate, /*highpass=*/true);
74 }
75
76 // ── Chebyshev Type II (Inverse Chebyshev) ────────────────────────────
77
86 static std::vector<Coefficients>
87 chebyshev2_lowpass(int order, float stop_freq_hz, float stop_atten_db, float sample_rate) {
88 return cheb2_cascade(order, stop_freq_hz, stop_atten_db, sample_rate, /*highpass=*/false);
89 }
90
92 static std::vector<Coefficients>
93 chebyshev2_highpass(int order, float stop_freq_hz, float stop_atten_db, float sample_rate) {
94 return cheb2_cascade(order, stop_freq_hz, stop_atten_db, sample_rate, /*highpass=*/true);
95 }
96
97 // ── Elliptic (Cauer) ─────────────────────────────────────────────────
98 //
99 // Equiripple in BOTH passband (ripple_db) and stopband (stopband_attn_db).
100 // For a given order it achieves the steepest transition of the four
101 // classic designs. Passband edge is at @p freq_hz; the stopband edge is
102 // implied by the order and the two ripple specifications via the
103 // Cauer degree equation:
104 //
105 // N · K(k')·K(k1) = K(k)·K(k1')
106 //
107 // where k is the filter selectivity (Ω_p/Ω_s) and k1 = ε/√(10^(As/10)−1).
108 // We solve for k given N + ripples; the resulting stopband edge is
109 // @c freq_hz / k. Callers that need a specific stopband edge should
110 // pick the order that places it; @c elliptic_required_order is a
111 // helper that returns the minimum order for given specs.
112
122 static std::vector<Coefficients>
123 elliptic_lowpass(int order, float freq_hz,
124 float passband_ripple_db, float stopband_attn_db,
125 float sample_rate) {
126 return elliptic_cascade(order, freq_hz, passband_ripple_db,
127 stopband_attn_db, sample_rate,
128 /*highpass=*/false);
129 }
130
132 static std::vector<Coefficients>
133 elliptic_highpass(int order, float freq_hz,
134 float passband_ripple_db, float stopband_attn_db,
135 float sample_rate) {
136 return elliptic_cascade(order, freq_hz, passband_ripple_db,
137 stopband_attn_db, sample_rate,
138 /*highpass=*/true);
139 }
140
141private:
142 static constexpr double kPi = 3.14159265358979323846;
143
144 // ── Common: bilinear transform of an SOS section from analog s-plane ─
145 //
146 // Given an analog biquad H(s) = (B0 s^2 + B1 s + B2) / (A0 s^2 + A1 s + A2)
147 // and a sample period T, return the digital biquad. Pre-warping is the
148 // caller's responsibility (i.e. analog cutoff already chosen so warped
149 // digital cutoff is at the desired Hz).
150 static Coefficients bilinear_sos(double B0, double B1, double B2,
151 double A0, double A1, double A2,
152 double T) {
153 // s = (2/T) * (z-1)/(z+1)
154 double K = 2.0 / T;
155 double K2 = K * K;
156 double a0_d = A0 * K2 + A1 * K + A2;
157 double a1_d = 2.0 * (A2 - A0 * K2);
158 double a2_d = A0 * K2 - A1 * K + A2;
159 double b0_d = B0 * K2 + B1 * K + B2;
160 double b1_d = 2.0 * (B2 - B0 * K2);
161 double b2_d = B0 * K2 - B1 * K + B2;
162 double inv = 1.0 / a0_d;
163 return Coefficients{
164 static_cast<float>(b0_d * inv),
165 static_cast<float>(b1_d * inv),
166 static_cast<float>(b2_d * inv),
167 static_cast<float>(a1_d * inv),
168 static_cast<float>(a2_d * inv),
169 };
170 }
171
172 static double prewarp_omega(double freq_hz, double sample_rate) {
173 double T = 1.0 / sample_rate;
174 return (2.0 / T) * std::tan(kPi * freq_hz / sample_rate);
175 }
176
177 // ── Chebyshev I poles (analog prototype, unit cutoff) ────────────────
178 //
179 // For order N and passband ripple R dB,
180 // eps = sqrt(10^(R/10) - 1)
181 // mu = (1/N) * asinh(1/eps)
182 // s_k = -sinh(mu) * sin(theta_k) + j * cosh(mu) * cos(theta_k)
183 // theta_k = pi * (2k - 1) / (2N), k = 1..N
184 static std::vector<std::complex<double>>
185 cheb1_prototype_poles(int order, double ripple_db) {
186 double eps = std::sqrt(std::pow(10.0, ripple_db / 10.0) - 1.0);
187 double mu = std::asinh(1.0 / eps) / static_cast<double>(order);
188 double sinh_mu = std::sinh(mu);
189 double cosh_mu = std::cosh(mu);
190
191 std::vector<std::complex<double>> poles;
192 poles.reserve(static_cast<size_t>(order));
193 for (int k = 1; k <= order; ++k) {
194 double theta = kPi * (2.0 * k - 1.0) / (2.0 * order);
195 poles.emplace_back(-sinh_mu * std::sin(theta),
196 cosh_mu * std::cos(theta));
197 }
198 return poles;
199 }
200
201 static std::vector<Coefficients>
202 cheb1_cascade(int order, double freq_hz, double ripple_db,
203 double sample_rate, bool highpass) {
204 if (order < 2 || (order & 1) != 0) return {};
205 if (freq_hz <= 0.0 || freq_hz >= 0.5 * sample_rate) return {};
206 if (ripple_db <= 0.0) return {};
207
208 double T = 1.0 / sample_rate;
209 double wc = prewarp_omega(freq_hz, sample_rate);
210
211 auto poles = cheb1_prototype_poles(order, ripple_db);
212 int N = order;
213 int num_sections = N / 2;
214
215 // DC gain of even-N Cheb I sits at the bottom of the ripple band:
216 // |H(j0)| = 1 / sqrt(1 + eps^2)
217 // Apply that scale to the first section's numerator after the per-
218 // section gains are normalized to 1.
219 double eps = std::sqrt(std::pow(10.0, ripple_db / 10.0) - 1.0);
220 double target_dc = 1.0 / std::sqrt(1.0 + eps * eps); // even N
221
222 // Sort poles by imag part for consistent pairing — purely numerical,
223 // doesn't affect overall cascade response.
224 std::sort(poles.begin(), poles.end(),
225 [](const std::complex<double>& a, const std::complex<double>& b) {
226 return a.imag() < b.imag();
227 });
228
229 std::vector<Coefficients> result;
230 result.reserve(static_cast<size_t>(num_sections));
231
232 for (int sec = 0; sec < num_sections; ++sec) {
233 auto p = poles[static_cast<size_t>(sec)];
234 // Per pair: denom = (s - p)(s - conj(p)) = s^2 - 2 Re(p) s + |p|^2
235 double A0 = 1.0;
236 double A1 = -2.0 * p.real();
237 double A2 = p.real() * p.real() + p.imag() * p.imag();
238 // Lowpass numerator = A2 (so prototype section DC gain = 1).
239 double B0 = 0.0, B1 = 0.0, B2 = A2;
240
241 // Frequency-scale prototype (cutoff at omega=1) to actual wc:
242 // s -> s/wc, multiplied through to keep A0 normalized.
243 A1 *= wc; A2 *= wc * wc;
244 B1 *= wc; B2 *= wc * wc;
245
246 if (highpass) {
247 // HP prototype: 2 zeros at s=0, poles at 1/s_k.
248 std::complex<double> p_hp = 1.0 / poles[static_cast<size_t>(sec)];
249 A0 = 1.0;
250 A1 = -2.0 * p_hp.real() * wc;
251 A2 = (p_hp.real() * p_hp.real() + p_hp.imag() * p_hp.imag()) * wc * wc;
252 // numer = s^2 (after freq scaling, scaled by wc^0 since prototype
253 // was wc-independent for the zeros-at-origin case)
254 B0 = 1.0; B1 = 0.0; B2 = 0.0;
255 }
256
257 result.push_back(bilinear_sos(B0, B1, B2, A0, A1, A2, T));
258 }
259
260 // Apply overall DC/Nyquist gain correction.
261 // LP: target |H(z=1)| = 1/sqrt(1+eps^2)
262 // HP: target |H(z=-1)| = 1/sqrt(1+eps^2) (symmetric via zeros at origin)
263 if (!result.empty()) {
264 float scale = static_cast<float>(target_dc);
265 result[0].b0 *= scale;
266 result[0].b1 *= scale;
267 result[0].b2 *= scale;
268 }
269 return result;
270 }
271
272 // ── Chebyshev II (inverse Chebyshev) ─────────────────────────────────
273 //
274 // Inverse Chebyshev poles + zeros (analog prototype, unit stopband edge):
275 // eps = 1 / sqrt(10^(As/10) - 1)
276 // mu = (1/N) * asinh(1/eps)
277 // theta_k = pi*(2k-1)/(2N)
278 // pole_k = 1 / (-sinh(mu) sin(theta_k) + j cosh(mu) cos(theta_k))
279 // zero_k = j / cos(theta_k) (pure imag, on the j-axis)
280 static std::vector<Coefficients>
281 cheb2_cascade(int order, double stop_freq_hz, double stop_atten_db,
282 double sample_rate, bool highpass) {
283 if (order < 2 || (order & 1) != 0) return {};
284 if (stop_freq_hz <= 0.0 || stop_freq_hz >= 0.5 * sample_rate) return {};
285 if (stop_atten_db <= 0.0) return {};
286
287 double T = 1.0 / sample_rate;
288 double ws = prewarp_omega(stop_freq_hz, sample_rate);
289
290 double eps = 1.0 / std::sqrt(std::pow(10.0, stop_atten_db / 10.0) - 1.0);
291 double mu = std::asinh(1.0 / eps) / static_cast<double>(order);
292 double sinh_mu = std::sinh(mu);
293 double cosh_mu = std::cosh(mu);
294
295 int N = order;
296 int num_sections = N / 2;
297
298 std::vector<std::complex<double>> poles;
299 std::vector<std::complex<double>> zeros;
300 poles.reserve(static_cast<size_t>(N));
301 zeros.reserve(static_cast<size_t>(N));
302
303 for (int k = 1; k <= N; ++k) {
304 double theta = kPi * (2.0 * k - 1.0) / (2.0 * N);
305 std::complex<double> cheb_pole(-sinh_mu * std::sin(theta),
306 cosh_mu * std::cos(theta));
307 poles.push_back(1.0 / cheb_pole);
308 double c = std::cos(theta);
309 zeros.emplace_back(0.0, 1.0 / c);
310 }
311
312 // Pair conjugates by sorted imag part.
313 std::sort(poles.begin(), poles.end(),
314 [](const std::complex<double>& a, const std::complex<double>& b) {
315 return a.imag() < b.imag();
316 });
317 std::sort(zeros.begin(), zeros.end(),
318 [](const std::complex<double>& a, const std::complex<double>& b) {
319 return a.imag() < b.imag();
320 });
321
322 std::vector<Coefficients> result;
323 result.reserve(static_cast<size_t>(num_sections));
324
325 for (int sec = 0; sec < num_sections; ++sec) {
326 auto p = poles[static_cast<size_t>(sec)];
327 auto z = zeros[static_cast<size_t>(sec)];
328 double A0 = 1.0;
329 double A1 = -2.0 * p.real();
330 double A2 = p.real() * p.real() + p.imag() * p.imag();
331 // Numer: (s - z)(s - conj(z)) = s^2 + |z|^2 (z is pure imag)
332 double B0 = 1.0;
333 double B1 = 0.0;
334 double B2 = z.imag() * z.imag();
335 // Normalize section DC gain to 1: multiply numer by (A2 / B2).
336 double scale_num = A2 / B2;
337 B0 *= scale_num;
338 B1 *= scale_num;
339 B2 *= scale_num;
340
341 // Frequency-scale prototype (stop edge at omega=1) to actual ws.
342 A1 *= ws; A2 *= ws * ws;
343 B1 *= ws; B2 *= ws * ws;
344
345 if (highpass) {
346 // Reciprocate prototype poles+zeros, then freq-scale by ws.
347 std::complex<double> p_hp_proto = 1.0 / poles[static_cast<size_t>(sec)];
348 std::complex<double> z_hp_proto = 1.0 / zeros[static_cast<size_t>(sec)];
349 A0 = 1.0;
350 A1 = -2.0 * p_hp_proto.real() * ws;
351 A2 = (p_hp_proto.real() * p_hp_proto.real() +
352 p_hp_proto.imag() * p_hp_proto.imag()) * ws * ws;
353 double zr = z_hp_proto.real();
354 double zi = z_hp_proto.imag();
355 B0 = 1.0;
356 B1 = -2.0 * zr * ws;
357 B2 = (zr * zr + zi * zi) * ws * ws;
358 result.push_back(bilinear_sos(B0, B1, B2, A0, A1, A2, T));
359 continue;
360 }
361
362 result.push_back(bilinear_sos(B0, B1, B2, A0, A1, A2, T));
363 }
364 return result;
365 }
366
367 // ── Elliptic (Cauer) implementation ──────────────────────────────────
368 //
369 // Real-modulus Jacobi machinery is provided by `special_functions.hpp`
370 // (Pulp's shared analytic toolbox):
371 // • special::elliptic_K(m) AGM
372 // • special::jacobi_sncndn(u, m, …) NR §6.12 AGM back-substitution
373 // • special::jacobi_asn(x, m) Carlson R_F inverse
374 // • special::jacobi_nome(m) q = exp(−π K'/K)
375 //
376 // Pole/zero placement transcribes scipy.signal._filter_design.ellipap
377 // (BSD-3, reference-grade): the formula collapses the complex
378 // Jacobi-cd machinery into real-valued sn/cn/dn at two distinct
379 // arguments, yielding bilinear-ready biquad coefficients.
380
392 static double elliptic_solve_k(int N, double k1) {
393 if (k1 <= 0.0) return 0.0;
394 if (k1 >= 1.0) return 1.0 - 1e-15;
395
396 double q1 = special::jacobi_nome(k1 * k1);
397 double q = std::pow(q1, 1.0 / static_cast<double>(N));
398
399 // ϑ₂(q) = 2 q^(1/4) · Σ_{n=0..∞} q^(n(n+1))
400 // ϑ₃(q) = 1 + 2 Σ_{n=1..∞} q^(n²)
401 double theta2 = 0.0;
402 double theta3 = 1.0;
403 for (int n = 0; n < 30; ++n) {
404 theta2 += std::pow(q, n * (n + 1));
405 }
406 theta2 *= 2.0 * std::pow(q, 0.25);
407 for (int n = 1; n < 30; ++n) {
408 theta3 += 2.0 * std::pow(q, static_cast<double>(n) * n);
409 }
410 double r = theta2 / theta3;
411 double k = r * r;
412 if (k <= 0.0) k = 1e-15;
413 if (k >= 1.0) k = 1.0 - 1e-15;
414 return k;
415 }
416
417 static std::vector<Coefficients>
418 elliptic_cascade(int order, double freq_hz,
419 double passband_ripple_db, double stopband_attn_db,
420 double sample_rate, bool highpass) {
421 if (order < 2 || (order & 1) != 0) return {};
422 if (freq_hz <= 0.0 || freq_hz >= 0.5 * sample_rate) return {};
423 if (passband_ripple_db <= 0.0) return {};
424 if (stopband_attn_db <= passband_ripple_db) return {};
425
426 double eps = std::sqrt(std::pow(10.0, passband_ripple_db / 10.0) - 1.0);
427 double As_lin = std::sqrt(std::pow(10.0, stopband_attn_db / 10.0) - 1.0);
428 double k1 = eps / As_lin;
429
430 // Sanity: k1 must lie in (0, 1).
431 if (!(k1 > 0.0 && k1 < 1.0)) return {};
432
433 double k = elliptic_solve_k(order, k1);
434 double m = k * k;
435 double K = special::elliptic_K(m);
436
437 // Design-parameter v0 (SciPy ellipap convention,
438 // _filter_design.py):
439 //
440 // v0 = K(m) · arc_jac_sc1(1/ε, k1²) / (N · K(k1²))
441 //
442 // where arc_jac_sc1(w, k1²) is the *real* inverse Jacobi sc with
443 // complementary modulus, defined by w = sc(z, 1 − k1²). It is the
444 // imaginary part of the complex sn⁻¹(j·w, k1²) under Jacobi's
445 // imaginary transformation:
446 //
447 // sn(j·u, m) = j · sc(u, 1 − m)
448 // ⇒ sn⁻¹(j·w, m) = j · sc⁻¹(w, 1 − m)
449 //
450 // The previous implementation used `jacobi_asn(1/ε, k1²)` directly,
451 // but `jacobi_asn` clamps its input to [−1, 1]. For any reasonable
452 // ripple spec (Rp < ~3 dB ⇒ ε < 1 ⇒ 1/ε > 1), the input was
453 // silently clamped to 1 and v0 collapsed to a constant independent
454 // of the user-supplied passband_ripple_db.
455 //
456 // The real `sc⁻¹(w, m')` for w ≥ 0, m' ∈ (0,1) can be reduced to a
457 // domain-safe asn call. From sc² = sn²/cn² = sn²/(1−sn²):
458 //
459 // sn(sc⁻¹(w, m'); m') = w / sqrt(1 + w²)
460 // ⇒ sc⁻¹(w, m') = asn( w / sqrt(1 + w²), m' )
461 //
462 // The asn argument here is bounded in [0, 1) for all real w, so
463 // no clamp is hit and v0 tracks the requested ripple properly.
464 double K_k1 = special::elliptic_K(k1 * k1);
465 const double inv_eps = 1.0 / eps;
466 const double sn_arg = inv_eps / std::sqrt(1.0 + inv_eps * inv_eps);
467 const double m_prime = 1.0 - k1 * k1;
468 double r = special::jacobi_asn(sn_arg, m_prime); // sc⁻¹(1/ε, 1−k1²)
469 double v0 = (K * r) / (static_cast<double>(order) * K_k1);
470 double sv, cv, dv;
471 // jacobi_sncndn(u, m, ...): note `m` is the parameter (k²),
472 // and for v0 we evaluate at modulus k' (m' = 1 - m).
473 special::jacobi_sncndn(v0, 1.0 - m, &sv, &cv, &dv);
474
475 // Per-section pole+zero placement follows the classic Cauer
476 // formula (cf. scipy.signal._filter_design.ellipap, BSD-3,
477 // and Antoniou ch. 12) so the design is interoperable with a
478 // well-trodden reference lane:
479 //
480 // For j = 1, 3, 5, …, N−1 (N/2 conjugate pairs):
481 // s, c, d = sn(j·K/N, k), cn(j·K/N, k), dn(j·K/N, k)
482 // ω_z = 1 / (k · |s|) (zero on jω)
483 // p = −(c·d·sv·cv + j·s·dv) / (1 − (d·sv)²)
484 // where (sv, cv, dv) = (sn, cn, dn)(v0, k').
485 //
486 // The pole's conjugate p* gives the second member of the pair;
487 // we collapse each pair into one biquad section.
488
489 int N = order;
490 int L = N / 2;
491 double T = 1.0 / sample_rate;
492 double wc = prewarp_omega(freq_hz, sample_rate);
493
494 std::vector<Coefficients> result;
495 result.reserve(static_cast<size_t>(L));
496
497 for (int idx = 0; idx < L; ++idx) {
498 int jpos = 2 * idx + 1; // 1, 3, 5, …
499 double u_k = jpos * K / static_cast<double>(N);
500
501 double s_u, c_u, d_u;
502 special::jacobi_sncndn(u_k, m, &s_u, &c_u, &d_u);
503
504 // ── zero on jω axis ──
505 double omega_z = 1.0 / (k * std::abs(s_u));
506
507 // ── pole pair ──
508 double dsv = d_u * sv;
509 double inv = 1.0 / (1.0 - dsv * dsv);
510 double p_re = -(c_u * d_u * sv * cv) * inv;
511 double p_im = -(s_u * dv) * inv;
512 // Take the upper half-plane representative; conjugate
513 // contributes the other half of the biquad denominator.
514 p_im = std::abs(p_im);
515
516 // Safety: stability requires Re(p) < 0.
517 if (p_re >= 0.0) p_re = -std::abs(p_re);
518
519 // Prototype (normalized to passband edge ωp = 1):
520 // pole pair → (s − p)(s − p*) = s² − 2 Re(p) s + |p|²
521 // zero pair → (s − jω_z)(s + jω_z) = s² + ω_z²
522 double pa1_proto = -2.0 * p_re;
523 double pa2_proto = p_re * p_re + p_im * p_im;
524 double pb2_proto = omega_z * omega_z; // pb0 = 1, pb1 = 0
525
526 double A0, A1, A2, B0, B1, B2;
527 if (!highpass) {
528 // LP: substitute s → s/wc ⇒ multiply A1,B1 by wc and
529 // A2,B2 by wc². Then normalize section DC gain to 1.
530 A0 = 1.0;
531 A1 = pa1_proto * wc;
532 A2 = pa2_proto * wc * wc;
533 B0 = 1.0;
534 B1 = 0.0;
535 B2 = pb2_proto * wc * wc;
536 double scale_num = A2 / B2;
537 B0 *= scale_num;
538 B1 *= scale_num;
539 B2 *= scale_num;
540 } else {
541 // HP: substitute s → wc/s in the prototype
542 // (s² + ω_z²) / (s² + a·s + b), giving
543 // (ω_z²·s² + wc²) / (b·s² + a·wc·s + wc²).
544 // Normalize cascade-level by dividing each numerator
545 // by ω_z²/b so per-section gain at s=∞ becomes 1.
546 // Then divide everything by b to get A0=1 form:
547 // A0 = 1
548 // A1 = (-2·Re(p))·wc / |p|²
549 // A2 = wc² / |p|²
550 // B0 = 1
551 // B1 = 0
552 // B2 = wc² / ω_z²
553 // At s=0 (DC), section gain = B2·b/wc² = b/ω_z² < 1
554 // (DC blocked), product across cascade gives full HP
555 // stopband attenuation.
556 double pole_mag2 = pa2_proto; // |p|² in prototype
557 A0 = 1.0;
558 A1 = (pa1_proto * wc) / pole_mag2;
559 A2 = (wc * wc) / pole_mag2;
560 B0 = 1.0;
561 B1 = 0.0;
562 B2 = (wc * wc) / pb2_proto; // = wc²/ω_z²
563 }
564
565 result.push_back(bilinear_sos(B0, B1, B2, A0, A1, A2, T));
566 }
567
568 // Overall passband-floor scaling. Even-order elliptic LP has
569 // |H_LP(0)| = 1 / √(1+ε²)
570 // (the floor of the ripple band). The HP dual sits at z=−1
571 // (Nyquist), where after bilinear the analog s→∞ maps; the
572 // section construction above leaves cascade-level analog
573 // |H_HP(∞)| = 1 (each section's b0 = 1, leading-order ratio
574 // 1/1). To pull the HP passband peaks to 0 dB matching the
575 // LP convention, apply the same √(1+ε²) ripple-floor scale.
576 if (!result.empty()) {
577 double target = 1.0 / std::sqrt(1.0 + eps * eps);
578 float scale = static_cast<float>(target);
579 result[0].b0 *= scale;
580 result[0].b1 *= scale;
581 result[0].b2 *= scale;
582 }
583 return result;
584 }
585};
586
587// ── Cascade evaluation helpers (utility) ─────────────────────────────────
588
589// cascade_magnitude() lives in frequency_response.hpp (included above) and
590// takes a std::span, so the std::vector a design helper returns converts
591// implicitly. It is deliberately not reimplemented here: one evaluator of
592// H(z), shared by the tests, the UI curve widgets, and this header.
593
596inline bool cascade_is_stable(std::span<const FilterDesign::Coefficients> sos) {
597 for (const auto& c : sos) {
598 if (!biquad_is_stable(c)) return false;
599 }
600 return true;
601}
602
603} // namespace pulp::signal
Automated filter coefficient generation for biquad and high-order IIR filters.
Evaluate what a biquad (or a cascade of them) does to the spectrum.
double jacobi_nome(double m)
Jacobi nome q(m) = exp(-pi * K(1 - m) / K(m)).
Definition special_functions.hpp:180
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
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
@ c
Definition spectrum_trace.hpp:35
bool biquad_is_stable(const BiquadCoefficientsT< SampleType > &c) noexcept
Return whether a normalized real biquad's poles are strictly inside the unit circle.
Definition biquad.hpp:34
@ a
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ highpass
Definition filter_morph.hpp:19
bool cascade_is_stable(std::span< const FilterDesign::Coefficients > sos)
Returns true if every biquad section's poles lie strictly inside the unit circle (i....
Definition iir_design.hpp:596
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
@ v0
Definition design_ir.hpp:40
@ r
Definition input_events.hpp:41
@ m
Definition input_events.hpp:40
@ q
Definition input_events.hpp:41
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ z
Definition input_events.hpp:42
BiquadCoefficients Coefficients
Biquad coefficient set (normalized: a0 = 1).
Definition filter_design.hpp:37
Chebyshev / Elliptic IIR design utilities.
Definition iir_design.hpp:52
static std::vector< Coefficients > chebyshev1_lowpass(int order, float freq_hz, float ripple_db, float sample_rate)
Chebyshev Type I lowpass cascade.
Definition iir_design.hpp:66
FilterDesign::Coefficients Coefficients
Definition iir_design.hpp:53
static std::vector< Coefficients > elliptic_lowpass(int order, float freq_hz, float passband_ripple_db, float stopband_attn_db, float sample_rate)
Elliptic (Cauer) lowpass cascade — equiripple in both bands.
Definition iir_design.hpp:123
static std::vector< Coefficients > elliptic_highpass(int order, float freq_hz, float passband_ripple_db, float stopband_attn_db, float sample_rate)
Elliptic highpass cascade.
Definition iir_design.hpp:133
static std::vector< Coefficients > chebyshev2_lowpass(int order, float stop_freq_hz, float stop_atten_db, float sample_rate)
Chebyshev Type II lowpass cascade.
Definition iir_design.hpp:87
static std::vector< Coefficients > chebyshev1_highpass(int order, float freq_hz, float ripple_db, float sample_rate)
Chebyshev Type I highpass cascade.
Definition iir_design.hpp:72
static std::vector< Coefficients > chebyshev2_highpass(int order, float stop_freq_hz, float stop_atten_db, float sample_rate)
Chebyshev Type II highpass cascade.
Definition iir_design.hpp:93