v2.0.0
Loading...
Searching...
No Matches
iirfilter.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "iirfilter.h"
18
19//=============================================================================================================
20// QT INCLUDES
21//=============================================================================================================
22
23#include <QDebug>
24
25//=============================================================================================================
26// C++ INCLUDES
27//=============================================================================================================
28
29#include <cmath>
30#include <complex>
31#include <algorithm>
32
33//=============================================================================================================
34// USED NAMESPACES
35//=============================================================================================================
36
37using namespace UTILSLIB;
38using namespace Eigen;
39
40//=============================================================================================================
41// CONSTANTS
42//=============================================================================================================
43
44static constexpr double PI = M_PI;
45
46//=============================================================================================================
47// PRIVATE HELPERS
48//=============================================================================================================
49
50namespace {
51
52//=============================================================================================================
56double evalMagnitude(const QVector<IirBiquad>& sos, double omega)
57{
58 std::complex<double> z(std::cos(omega), std::sin(omega));
59 std::complex<double> H(1.0, 0.0);
60 std::complex<double> z1(1.0, 0.0); // z^{-1} placeholder
61
62 for (const IirBiquad& bq : sos) {
63 std::complex<double> zinv = 1.0 / z;
64 std::complex<double> zinv2 = zinv * zinv;
65 std::complex<double> num = bq.b0 + bq.b1 * zinv + bq.b2 * zinv2;
66 std::complex<double> den = 1.0 + bq.a1 * zinv + bq.a2 * zinv2;
67 H *= num / den;
68 }
69 return std::abs(H);
70}
71
72} // anonymous namespace
73
74//=============================================================================================================
75// MEMBER DEFINITIONS
76//=============================================================================================================
77
78QVector<std::complex<double>> IirFilter::butterworthPrototypePoles(int n)
79{
80 // Butterworth analogue LP prototype poles (normalised cutoff = 1 rad/s):
81 // p_k = exp( j * PI * (2k - 1 + n) / (2n) ) for k = 1 .. n
82 // All poles lie on the unit circle in the left half-plane.
83 QVector<std::complex<double>> poles;
84 poles.reserve(n);
85 for (int k = 1; k <= n; ++k) {
86 double angle = PI * static_cast<double>(2 * k - 1 + n) / static_cast<double>(2 * n);
87 poles.append(std::complex<double>(std::cos(angle), std::sin(angle)));
88 }
89 return poles;
90}
91
92//=============================================================================================================
93
94IirBiquad IirFilter::poleToDigitalBiquad(std::complex<double> pole,
95 double dC,
96 double dGain)
97{
98 // Given complex conjugate pole pair (pole, conj(pole)), the analogue biquad is:
99 // H_a(s) = gain * omega0^2 / (s^2 - 2*Re(pole)*s + |pole|^2)
100 // where omega0 = |pole|
101 //
102 // Bilinear transform s = dC*(z-1)/(z+1):
103 // Let omega0 = |pole|, alpha = -Re(pole) (> 0), c = dC
104 //
105 // H_a(s) numerator coefficients: b2_a=0, b1_a=0, b0_a = gain*omega0^2
106 // H_a(s) denominator: a2_a=1, a1_a = 2*alpha, a0_a = omega0^2
107 //
108 // After bilinear (general formula for b2_a=0, b1_a=0):
109 // d0 = c^2 + 2*alpha*c + omega0^2
110 // b0 = gain * omega0^2 / d0
111 // b1 = 2 * gain * omega0^2 / d0
112 // b2 = gain * omega0^2 / d0
113 // a1 = 2*(omega0^2 - c^2) / d0
114 // a2 = (c^2 - 2*alpha*c + omega0^2) / d0
115
116 double omega0 = std::abs(pole);
117 double alpha = -pole.real(); // positive for stable (left-half-plane) poles
118
119 double omega0sq = omega0 * omega0;
120 double d0 = dC * dC + 2.0 * alpha * dC + omega0sq;
121
122 IirBiquad bq;
123 bq.b0 = dGain * omega0sq / d0;
124 bq.b1 = 2.0 * dGain * omega0sq / d0;
125 bq.b2 = dGain * omega0sq / d0;
126 bq.a1 = 2.0 * (omega0sq - dC * dC) / d0;
127 bq.a2 = (dC * dC - 2.0 * alpha * dC + omega0sq) / d0;
128 return bq;
129}
130
131//=============================================================================================================
132
133IirBiquad IirFilter::realPoleToDigitalSection(double dPoleReal, double dC, double dGain)
134{
135 // H_a(s) = gain * |pole| / (s - dPoleReal) (dPoleReal < 0)
136 // Bilinear: s = dC*(z-1)/(z+1)
137 // H(z) = gain * |pole| * (1 + z^{-1}) / ((dC + |pole|) - (dC - |pole|)*z^{-1})
138 // = [gain*|pole|/(dC+|pole|)] * (1 + z^{-1}) / (1 - [(dC-|pole|)/(dC+|pole|)]*z^{-1})
139
140 double absPole = std::abs(dPoleReal);
141 double sum = dC + absPole;
142
143 IirBiquad bq;
144 bq.b0 = dGain * absPole / sum;
145 bq.b1 = dGain * absPole / sum;
146 bq.b2 = 0.0;
147 bq.a1 = -(dC - absPole) / sum;
148 bq.a2 = 0.0;
149 return bq;
150}
151
152//=============================================================================================================
153
154QVector<IirBiquad> IirFilter::designButterworth(int iOrder,
155 FilterType type,
156 double dCutoffLow,
157 double dCutoffHigh,
158 double dSFreq)
159{
160 if (iOrder < 1) {
161 qWarning() << "IirFilter::designButterworth: order must be >= 1, got" << iOrder;
162 return {};
163 }
164
165 const double dC = 2.0 * dSFreq; // bilinear pre-warp constant
166
167 // Pre-warp analogue cutoff frequencies
168 double dOmegaLow = dC * std::tan(PI * dCutoffLow / dSFreq);
169 double dOmegaHigh = (type == BandPass || type == BandStop)
170 ? dC * std::tan(PI * dCutoffHigh / dSFreq)
171 : 0.0;
172
173 // Butterworth normalised LP prototype poles
174 QVector<std::complex<double>> protoPoles = butterworthPrototypePoles(iOrder);
175
176 QVector<IirBiquad> sos;
177
178 if (type == LowPass || type == HighPass) {
179 // ---- LP / HP -------------------------------------------------------
180 // LP: scale pole by Omega_c → p_k_lp = Omega_c * proto_k
181 // HP: invert-and-scale → p_k_hp = Omega_c / proto_k
182 //
183 // Process complex conjugate pairs (take poles with Im >= 0).
184 // If order is odd there will be one real pole at proto = -1.
185
186 for (int k = 0; k < iOrder; ++k) {
187 std::complex<double> proto = protoPoles[k];
188
189 // Skip the conjugate (imaginary part < 0); it is handled together with its pair.
190 if (proto.imag() < -1e-10) {
191 continue;
192 }
193
194 if (std::abs(proto.imag()) < 1e-10) {
195 // Real pole (only for odd order, proto ≈ -1)
196 double analogPole = (type == LowPass)
197 ? dOmegaLow * proto.real()
198 : dOmegaLow / proto.real();
199
200 // Evaluate passband gain at DC (LP) or Nyquist (HP) for normalisation.
201 // For a single first-order section: |H(0)| = |b0+b1|/|1+a1|
202 // We design with dGain=1 first, then normalise.
203 IirBiquad bq = realPoleToDigitalSection(analogPole, dC, 1.0);
204
205 // Normalise so passband gain ~ 1
206 double w_pb = (type == LowPass) ? 0.0 : PI;
207 double num = std::abs(bq.b0 + bq.b1 * std::cos(-w_pb)); // rough, imaginary part 0
208 double den = std::abs(1.0 + bq.a1 * std::cos(-w_pb));
209 double gain = (den > 1e-12 && num > 1e-12) ? den / num : 1.0;
210 bq.b0 *= gain;
211 bq.b1 *= gain;
212
213 sos.append(bq);
214 } else {
215 // Complex conjugate pair
216 std::complex<double> analogPole;
217 double sectionGain;
218
219 if (type == LowPass) {
220 analogPole = dOmegaLow * proto;
221 sectionGain = dOmegaLow * dOmegaLow; // LP: constant numerator = omega0^2
222 } else {
223 // HP: s -> Omega_c / s transforms pole p to Omega_c / p
224 analogPole = dOmegaLow / proto;
225 sectionGain = 1.0; // HP: numerator has s^2 -> gain handled below
226 }
227
228 if (type == LowPass) {
229 IirBiquad bq = poleToDigitalBiquad(analogPole, dC, sectionGain);
230 // Normalise: LP passband gain at DC (z=1, omega=0) should be 1
231 // At z=1: H = (b0+b1+b2)/(1+a1+a2)
232 double hDC = (bq.b0 + bq.b1 + bq.b2) / (1.0 + bq.a1 + bq.a2);
233 if (std::abs(hDC) > 1e-12) {
234 double scale = 1.0 / std::abs(hDC);
235 bq.b0 *= scale; bq.b1 *= scale; bq.b2 *= scale;
236 }
237 sos.append(bq);
238 } else {
239 // HP biquad: analogue section H(s) = s^2 / (s^2 - 2*Re(p)*s + |p|^2)
240 // b2_a=1, b1_a=0, b0_a=0; a2_a=1, a1_a=-2*Re(p), a0_a=|p|^2
241 // General bilinear for (b2_a=1, b1_a=0, b0_a=0):
242 double omega0 = std::abs(analogPole);
243 double alpha = -analogPole.real();
244 double omega0sq = omega0 * omega0;
245 double d0 = dC * dC + 2.0 * alpha * dC + omega0sq;
246
247 IirBiquad bq;
248 bq.b0 = dC * dC / d0;
249 bq.b1 = -2.0 * dC * dC / d0;
250 bq.b2 = dC * dC / d0;
251 bq.a1 = 2.0 * (omega0sq - dC * dC) / d0;
252 bq.a2 = (dC * dC - 2.0 * alpha * dC + omega0sq) / d0;
253
254 // Normalise: HP passband gain at Nyquist (z=-1, omega=pi) should be 1
255 // At z=-1: H = (b0-b1+b2)/(1-a1+a2)
256 double hNy = (bq.b0 - bq.b1 + bq.b2) / (1.0 - bq.a1 + bq.a2);
257 if (std::abs(hNy) > 1e-12) {
258 double scale = 1.0 / std::abs(hNy);
259 bq.b0 *= scale; bq.b1 *= scale; bq.b2 *= scale;
260 }
261 sos.append(bq);
262 }
263 }
264 }
265
266 } else {
267 // ---- BP / BS -------------------------------------------------------
268 // LP prototype pole p_k → BP/BS analogue poles via:
269 // BP: s^2 - Bw*p_k*s + Omega0^2 = 0
270 // BS: Bw*s / (s^2 + Omega0^2) = p_k (i.e., solve s^2 - (Bw/p_k)*s + Omega0^2 = 0)
271 // This doubles the order, producing 2*iOrder poles → iOrder biquad sections.
272
273 double dOmega0 = std::sqrt(dOmegaLow * dOmegaHigh);
274 double dBw = dOmegaHigh - dOmegaLow;
275
276 for (int k = 0; k < iOrder; ++k) {
277 std::complex<double> proto = protoPoles[k];
278
279 // Solve quadratic for the two BP or BS analogue poles from this prototype pole.
280 // BP quadratic: s^2 - Bw*proto*s + Omega0^2 = 0
281 // BS quadratic: s^2 - (Bw/proto)*s + Omega0^2 = 0
282 std::complex<double> mid;
283 if (type == BandPass) {
284 mid = dBw * proto;
285 } else {
286 mid = dBw / proto;
287 }
288
289 std::complex<double> discriminant = mid * mid - 4.0 * dOmega0 * dOmega0;
290 std::complex<double> sqrtDisc = std::sqrt(discriminant);
291 std::complex<double> pole1 = (mid + sqrtDisc) / 2.0;
292 std::complex<double> pole2 = (mid - sqrtDisc) / 2.0;
293
294 // Each of pole1, pole2 forms a complex conjugate pair with its own conjugate.
295 // Design one biquad section per pole (using poleToDigitalBiquad which handles
296 // the conjugate pair automatically).
297
298 for (auto& pole : {pole1, pole2}) {
299 if (type == BandPass) {
300 // BP analogue biquad numerator: Bw*s (zero at origin, zero at infinity)
301 // i.e., b2_a=0, b1_a=Bw, b0_a=0
302 // Bilinear of (b2_a=0, b1_a=Bw, b0_a=0) / (s^2 - 2Re(p)s + |p|^2):
303 double omega0 = std::abs(pole);
304 double alpha = -pole.real();
305 double omega0sq = omega0 * omega0;
306 double d0 = dC * dC + 2.0 * alpha * dC + omega0sq;
307
308 IirBiquad bq;
309 // num from bilinear(b1_a=Bw): b0=Bw*c, b1=0, b2=-Bw*c
310 bq.b0 = dBw * dC / d0;
311 bq.b1 = 0.0;
312 bq.b2 = -dBw * dC / d0;
313 bq.a1 = 2.0 * (omega0sq - dC * dC) / d0;
314 bq.a2 = (dC * dC - 2.0 * alpha * dC + omega0sq) / d0;
315 sos.append(bq);
316 } else {
317 // BS analogue biquad numerator: s^2 + Omega0^2 (zeros at ±j*Omega0)
318 // b2_a=1, b1_a=0, b0_a=Omega0^2
319 // Bilinear:
320 double omega0 = std::abs(pole);
321 double alpha = -pole.real();
322 double omega0sq = omega0 * omega0;
323 double dOmega0sq = dOmega0 * dOmega0;
324 double d0 = dC * dC + 2.0 * alpha * dC + omega0sq;
325
326 IirBiquad bq;
327 // num from bilinear(b2_a=1, b1_a=0, b0_a=Omega0^2):
328 // b0 = (c^2 + Omega0^2)/d0, b1 = 2*(Omega0^2-c^2)/d0, b2 = (c^2+Omega0^2)/d0
329 bq.b0 = (dC * dC + dOmega0sq) / d0;
330 bq.b1 = 2.0 * (dOmega0sq - dC * dC) / d0;
331 bq.b2 = (dC * dC + dOmega0sq) / d0;
332 bq.a1 = 2.0 * (omega0sq - dC * dC) / d0;
333 bq.a2 = (dC * dC - 2.0 * alpha * dC + omega0sq) / d0;
334 sos.append(bq);
335 }
336 }
337 }
338
339 // Normalise overall passband gain
340 // For BP: evaluate at centre frequency Omega0 (digital: omega_0 = Omega0 / fs * 2pi... use prewarped)
341 // For BS: evaluate at DC (omega = 0)
342 double omegaCheck = (type == BandPass)
343 ? 2.0 * std::atan(dOmega0 / dC) // bilinear inverse: omega_d = 2*atan(Omega_a/c)
344 : 0.0;
345 double totalGain = evalMagnitude(sos, omegaCheck);
346 if (totalGain > 1e-12) {
347 double scale = 1.0 / totalGain;
348 // Distribute scale evenly across sections
349 double perSection = std::pow(scale, 1.0 / sos.size());
350 for (IirBiquad& bq : sos) {
351 bq.b0 *= perSection;
352 bq.b1 *= perSection;
353 bq.b2 *= perSection;
354 }
355 }
356 }
357
358 return sos;
359}
360
361//=============================================================================================================
362
363RowVectorXd IirFilter::applySos(const RowVectorXd& vecData, const QVector<IirBiquad>& sos)
364{
365 if (sos.isEmpty() || vecData.size() == 0) {
366 return vecData;
367 }
368
369 RowVectorXd y = vecData;
370
371 // Cascade all biquad sections using Direct-Form II transposed structure:
372 // w[n] = x[n] - a1*w[n-1] - a2*w[n-2]
373 // y[n] = b0*w[n] + b1*w[n-1] + b2*w[n-2]
374 // Equivalent transposed: uses two state variables (s1, s2)
375
376 for (const IirBiquad& bq : sos) {
377 RowVectorXd x = y;
378 double s1 = 0.0, s2 = 0.0;
379
380 for (int i = 0; i < static_cast<int>(x.size()); ++i) {
381 double xi = x(i);
382 double yi = bq.b0 * xi + s1;
383 s1 = bq.b1 * xi - bq.a1 * yi + s2;
384 s2 = bq.b2 * xi - bq.a2 * yi;
385 y(i) = yi;
386 }
387 }
388
389 return y;
390}
391
392//=============================================================================================================
393
394RowVectorXd IirFilter::applyZeroPhase(const RowVectorXd& vecData, const QVector<IirBiquad>& sos)
395{
396 if (sos.isEmpty() || vecData.size() == 0) {
397 return vecData;
398 }
399
400 // Forward pass
401 RowVectorXd y = applySos(vecData, sos);
402
403 // Reverse
404 RowVectorXd yRev = y.reverse();
405
406 // Backward pass
407 RowVectorXd yRevFilt = applySos(yRev, sos);
408
409 // Reverse again
410 return yRevFilt.reverse();
411}
412
413//=============================================================================================================
414
415MatrixXd IirFilter::applyZeroPhaseMatrix(const MatrixXd& matData, const QVector<IirBiquad>& sos)
416{
417 if (sos.isEmpty() || matData.size() == 0) {
418 return matData;
419 }
420
421 MatrixXd matOut(matData.rows(), matData.cols());
422 for (int i = 0; i < static_cast<int>(matData.rows()); ++i) {
423 matOut.row(i) = applyZeroPhase(matData.row(i), sos);
424 }
425 return matOut;
426}
#define M_PI
Butterworth IIR filter design and application via numerically stable second-order sections.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Coefficients of one second-order IIR section (biquad).
Definition iirfilter.h:71
static Eigen::RowVectorXd applyZeroPhase(const Eigen::RowVectorXd &vecData, const QVector< IirBiquad > &sos)
static QVector< IirBiquad > designButterworth(int iOrder, FilterType type, double dCutoffLow, double dCutoffHigh, double dSFreq)
static Eigen::MatrixXd applyZeroPhaseMatrix(const Eigen::MatrixXd &matData, const QVector< IirBiquad > &sos)
FilterType
Filter type selector.
Definition iirfilter.h:106
static Eigen::RowVectorXd applySos(const Eigen::RowVectorXd &vecData, const QVector< IirBiquad > &sos)