GCC Code Coverage Report


Directory: src/
File: src/LowPassFilter.cpp
Date: 2026-09-16 13:46:10
Exec Total Coverage
Lines: 106 106 100.0%
Branches: 70 106 66.0%

Line Branch Exec Source
1 /*****************************************************************************
2 *
3 * Copyright (C) 2021 - 2022 Jonathan Grahl <jonathan.grahl@igh.de>
4 * 2021 - 2026 Florian Pose <florian.pose@igh.de>
5 *
6 * This file is part of the reta library (realtime-automation).
7 *
8 * The reta library is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published
10 * by the Free Software Foundation, either version 3 of the License, or (at
11 * your option) any later version.
12 *
13 * The reta library is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with the reta library. If not, see <http://www.gnu.org/licenses/>.
20 *
21 ****************************************************************************/
22
23 #include "reta/LowPassFilter.h"
24
25 #include "Base.h"
26
27 #include <cmath> // cos(), sin()
28 #include <sstream>
29 #include <stdexcept>
30
31 using std::function;
32 using std::invalid_argument;
33 using std::make_unique;
34 using std::shared_ptr;
35 using std::string;
36 using std::stringstream;
37 using std::vector;
38
39 using namespace Reta;
40
41 /****************************************************************************/
42
43 39 struct RETA_LOCAL LowPassFilter::Impl : public Base
44 {
45 // Direct Form II Transposed biquad section (RBJ audio cookbook). For
46 // order == 1, a1/b0 are set up as a plain single-pole filter and b1/b2/a2
47 // stay 0, so the same process() reduces to a first-order filter without
48 // any special-casing in update().
49 62 struct Biquad
50 {
51 double b0 {1.0}, b1 {0.0}, b2 {0.0}, a1 {0.0}, a2 {0.0};
52 double z1 {0.0}, z2 {0.0};
53
54 322549 double process(double in)
55 {
56 322549 double out = b0 * in + z1;
57 322549 z1 = b1 * in - a1 * out + z2;
58 322549 z2 = b2 * in - a2 * out;
59 322549 return out;
60 }
61 };
62
63 Impl(shared_ptr<Task>,
64 const string &,
65 double,
66 unsigned int,
67 unsigned int);
68
69 void
70 update(const vector<double> &,
71 const vector<uint8_t> & = vector<uint8_t>());
72 void updateCoefficients(unsigned int, double);
73
74 shared_ptr<Task> task;
75
76 // order == 1: single real pole, one section
77 // Even order >= 2: (order / 2) RBJ Butterworth biquad sections
78 unsigned int order;
79
80 vector<double> tau;
81 vector<double> output;
82
83 // Set (order >= 2 only) whenever tau had to be clamped to stay clear
84 // of the Nyquist frequency, see updateCoefficients().
85 vector<uint8_t> tauLimited;
86
87 // tau the cascade coefficients were last computed for, per channel, so
88 // that the (comparatively expensive) trigonometry in
89 // updateCoefficients() only runs when tau actually changes.
90 vector<double> stageTau;
91 vector<vector<Biquad>> stages;
92 };
93
94 /****************************************************************************/
95
96 41 LowPassFilter::Impl::Impl(
97 shared_ptr<Task> task,
98 const string &prefix,
99 double initTau,
100 unsigned int width,
101 41 unsigned int filterOrder) :
102 Base {prefix},
103 task {task},
104 order {filterOrder},
105 tau(width, initTau),
106 output(width, 0.0),
107 tauLimited(width, 0),
108 stageTau(width, 0.0),
109
5/10
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 41 times.
✗ Branch 10 not taken.
✓ Branch 14 taken 41 times.
✗ Branch 15 not taken.
✓ Branch 19 taken 41 times.
✗ Branch 20 not taken.
✓ Branch 24 taken 41 times.
✗ Branch 25 not taken.
43 stages(width)
110 {
111
2/2
✓ Branch 1 taken 40 times.
✓ Branch 2 taken 1 times.
41 checkZeroWidth(width);
112
113
5/6
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 35 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
40 if (order == 0 || (order > 1 && order % 2 != 0)) {
114
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 stringstream str;
115 str << __PRETTY_FUNCTION__ << " : " << prefix
116
5/10
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
✓ Branch 13 taken 1 times.
✗ Branch 14 not taken.
1 << " Invalid filter order " << order
117
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 << " (must be 1, or an even number >= 2)!";
118
2/4
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
1 throw invalid_argument(str.str());
119 }
120
121
2/2
✓ Branch 3 taken 62 times.
✓ Branch 4 taken 39 times.
101 for (auto &s : stages) {
122
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 58 times.
✓ Branch 3 taken 62 times.
✗ Branch 4 not taken.
62 s.resize(order == 1 ? 1 : order / 2);
123 }
124
125
1/2
✓ Branch 2 taken 39 times.
✗ Branch 3 not taken.
39 pdserv *pdserv {task->getPdServ()};
126
1/2
✓ Branch 2 taken 39 times.
✗ Branch 3 not taken.
39 pdtask *pdtask {task->getPdTask()};
127
128
1/2
✓ Branch 3 taken 39 times.
✗ Branch 4 not taken.
78 pdserv_parameter(
129
1/2
✓ Branch 2 taken 39 times.
✗ Branch 3 not taken.
78 pdserv, (prefix + "/Tau").c_str(), 0666, pd_double_T, tau.data(),
130 tau.size(), NULL, NULL, NULL);
131
1/2
✓ Branch 3 taken 39 times.
✗ Branch 4 not taken.
78 pdserv_signal(
132
1/2
✓ Branch 1 taken 39 times.
✗ Branch 2 not taken.
78 pdtask, 1, (prefix + "/Output").c_str(), pd_double_T,
133 39 output.data(), output.size(), NULL);
134
1/2
✓ Branch 3 taken 39 times.
✗ Branch 4 not taken.
78 pdserv_signal(
135
1/2
✓ Branch 1 taken 39 times.
✗ Branch 2 not taken.
78 pdtask, 1, (prefix + "/TauLimited").c_str(), pd_boolean_T,
136 39 tauLimited.data(), tauLimited.size(), NULL);
137 39 }
138
139 /****************************************************************************/
140
141 13 void LowPassFilter::Impl::updateCoefficients(unsigned int i, double t)
142 {
143 13 double period {task->getPeriod()};
144
145
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 6 times.
13 if (order == 1) {
146 // Discretized single-pole filter, equivalent to Euler-integrating
147 // dy/dt = (x - y) / tau:
148 // y[n] = y[n-1] + (period / tau) * (x[n] - y[n-1]).
149 7 Biquad &s {stages.at(i).at(0)};
150 7 s.b0 = period / t;
151 7 s.b1 = 0.0;
152 7 s.b2 = 0.0;
153 7 s.a1 = period / t - 1.0;
154 7 s.a2 = 0.0;
155
156 7 stageTau.at(i) = t;
157 7 return;
158 }
159
160 6 double sampleRate {1.0 / period};
161 6 double nyquist {sampleRate / 2.0};
162 6 double cutoffHz {1.0 / (2.0 * M_PI * t)};
163 6 tauLimited.at(i) = cutoffHz > nyquist * 0.99;
164
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 5 times.
6 if (tauLimited.at(i)) {
165 // stay clear of the Nyquist limit, where cos(w0) approaches -1
166 1 cutoffHz = nyquist * 0.99;
167 }
168
169 6 double w0 {2.0 * M_PI * cutoffHz / sampleRate};
170 6 double cosw0 {cos(w0)};
171 6 double sinw0 {sin(w0)};
172
173 6 unsigned int nSections {order / 2};
174
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 6 times.
12 for (unsigned int k = 1; k <= nSections; k++) {
175 // Per-section Q from the Butterworth pole angles, so that the
176 // cascade of these biquads reproduces the N-th order Butterworth
177 // response (maximally flat passband).
178 6 double theta {M_PI * (2.0 * k - 1.0) / (2.0 * order)};
179 6 double Q {1.0 / (2.0 * cos(theta))};
180 6 double alpha {sinw0 / (2.0 * Q)};
181
182 // Standard bilinear-transform lowpass biquad ("RBJ cookbook").
183 6 double a0 {1.0 + alpha};
184 6 Biquad &s {stages.at(i).at(k - 1)};
185 6 s.b0 = ((1.0 - cosw0) / 2.0) / a0;
186 6 s.b1 = (1.0 - cosw0) / a0;
187 6 s.b2 = s.b0;
188 6 s.a1 = (-2.0 * cosw0) / a0;
189 6 s.a2 = (1.0 - alpha) / a0;
190 }
191
192 6 stageTau.at(i) = t;
193 }
194
195 /****************************************************************************/
196
197 321566 void LowPassFilter::Impl::update(
198 const vector<double> &input,
199 const vector<uint8_t> &reset)
200 {
201
2/2
✓ Branch 1 taken 323569 times.
✓ Branch 2 taken 321566 times.
645135 for (unsigned int i = 0; i < output.size(); i++) {
202
6/6
✓ Branch 1 taken 323549 times.
✓ Branch 2 taken 20 times.
✓ Branch 4 taken 322549 times.
✓ Branch 5 taken 1000 times.
✓ Branch 6 taken 322549 times.
✓ Branch 7 taken 1020 times.
323569 if (tau.at(i) > 0.0 && !reset.at(i)) {
203
2/2
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 322536 times.
322549 if (tau.at(i) != stageTau.at(i)) {
204 13 updateCoefficients(i, tau.at(i));
205 }
206
207 322549 double out {input.at(i)};
208
3/4
✓ Branch 1 taken 322549 times.
✗ Branch 2 not taken.
✓ Branch 6 taken 322549 times.
✓ Branch 7 taken 322549 times.
645098 for (auto &s : stages.at(i)) {
209 322549 out = s.process(out);
210 }
211 322549 output.at(i) = out;
212 }
213 else {
214 1020 output.at(i) = input.at(i);
215
216 // Drop the filter memory so that it starts fresh from the
217 // current input once tau becomes positive again.
218
3/4
✓ Branch 1 taken 1020 times.
✗ Branch 2 not taken.
✓ Branch 6 taken 1020 times.
✓ Branch 7 taken 1020 times.
2040 for (auto &s : stages.at(i)) {
219 1020 s.z1 = s.z2 = 0.0;
220 }
221 }
222 }
223 321566 }
224
225 /****************************************************************************/
226
227 29 LowPassFilter::LowPassFilter(
228 shared_ptr<Task> task,
229 const std::string &prefix,
230 double initTau,
231 29 unsigned int width) :
232
2/2
✓ Branch 2 taken 28 times.
✓ Branch 3 taken 1 times.
30 LowPassFilter {task, prefix, initTau, width, 1}
233 28 {}
234
235 /****************************************************************************/
236
237 41 LowPassFilter::LowPassFilter(
238 shared_ptr<Task> task,
239 const std::string &prefix,
240 double initTau,
241 unsigned int width,
242 41 unsigned int order) :
243 41 impl {make_unique<Impl>(task, prefix, initTau, width, order)}
244 39 {}
245
246 /****************************************************************************/
247
248 39 LowPassFilter::~LowPassFilter()
249 39 {}
250
251 /****************************************************************************/
252
253 2 void LowPassFilter::setTau(double newTau, unsigned int i)
254 {
255 2 impl->tau.at(i) = newTau;
256 2 }
257
258 /****************************************************************************/
259
260 320554 void LowPassFilter::update(double input, bool reset)
261 {
262
3/6
✓ Branch 3 taken 320554 times.
✗ Branch 4 not taken.
✓ Branch 7 taken 320554 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 320554 times.
✗ Branch 11 not taken.
320554 impl->update(vector<double> {input}, vector<uint8_t> {reset});
263 320554 }
264
265 /****************************************************************************/
266
267 12 void LowPassFilter::update(
268 const vector<double> &input,
269 const vector<uint8_t> &reset)
270 {
271 12 impl->update(input, reset);
272 12 }
273
274 /****************************************************************************/
275
276 1000 void LowPassFilter::update(
277 function<double(unsigned int)> input,
278 function<bool(unsigned int)> reset)
279 {
280
1/2
✓ Branch 4 taken 1000 times.
✗ Branch 5 not taken.
2000 vector<double> inputVector(impl->output.size());
281
1/2
✓ Branch 4 taken 1000 times.
✗ Branch 5 not taken.
2000 vector<uint8_t> resetVector(impl->output.size());
282
283
2/2
✓ Branch 2 taken 3000 times.
✓ Branch 3 taken 1000 times.
4000 for (unsigned int i = 0; i < impl->output.size(); i++) {
284
2/4
✓ Branch 1 taken 3000 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 3000 times.
✗ Branch 5 not taken.
3000 inputVector.at(i) = input(i);
285
2/4
✓ Branch 1 taken 3000 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 3000 times.
✗ Branch 5 not taken.
3000 resetVector.at(i) = reset(i);
286 }
287
288
1/2
✓ Branch 2 taken 1000 times.
✗ Branch 3 not taken.
1000 impl->update(inputVector, resetVector);
289 1000 }
290
291 /****************************************************************************/
292
293 316070 double LowPassFilter::getOutput(unsigned int i) const
294 {
295 316070 return impl->output.at(i);
296 }
297
298 /****************************************************************************/
299
300 4 bool LowPassFilter::isTauLimited(unsigned int i) const
301 {
302 4 return impl->tauLimited.at(i);
303 }
304
305 /****************************************************************************/
306