Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
graphic_eq.hpp
Go to the documentation of this file.
1#pragma once
2
5
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <cstddef>
13#include <limits>
14#include <span>
15#include <type_traits>
16
17namespace pulp::signal {
18
24
38
39template <typename SampleType> struct GraphicEqBandT {
40 SampleType frequency_hz = SampleType{1000};
41 SampleType gain_db = SampleType{0};
42 SampleType q = SampleType{1};
43
44 friend constexpr bool operator==(const GraphicEqBandT&, const GraphicEqBandT&) = default;
45};
46
48
70template <typename SampleType = float, std::size_t MaxBands = 31> class GraphicEqT {
71 static_assert(std::is_floating_point_v<SampleType>,
72 "GraphicEqT requires a floating-point sample type");
73 static_assert(MaxBands > 0, "GraphicEqT requires non-zero band storage");
74
75 public:
78
79 static constexpr SampleType minimum_sample_rate_hz = SampleType{8000};
80 static constexpr SampleType maximum_sample_rate_hz = SampleType{384000};
81 static constexpr SampleType minimum_frequency_hz = SampleType{20};
82 static constexpr SampleType maximum_frequency_hz = SampleType{20000};
83 static constexpr SampleType minimum_gain_db = SampleType{-24};
84 static constexpr SampleType maximum_gain_db = SampleType{24};
85 static constexpr SampleType minimum_q = SampleType{0.1};
86 static constexpr SampleType maximum_q = SampleType{12};
87
89 std::size_t band_capacity = MaxBands) noexcept {
90 if (!(std::isfinite(sample_rate) && sample_rate >= minimum_sample_rate_hz &&
93 if (band_capacity == 0 || band_capacity > MaxBands)
95
96 // SosCascadeT uses fixed storage, so both calls are infallible after
97 // the bounds above. Keep the mutation after validation nevertheless.
98 (void)cascades_[0].prepare(band_capacity);
99 (void)cascades_[1].prepare(band_capacity);
100 sample_rate_ = sample_rate;
101 capacity_ = band_capacity;
102 active_index_ = 0;
103 active_bands_ = {};
104 requested_bands_ = {};
105 requested_coefficients_ = {};
106 active_count_ = 0;
107 requested_count_ = 0;
108 transition_total_ = 0;
109 transition_remaining_ = 0;
110 fault_count_ = 0;
111 healthy_ = true;
113 }
114
115 GraphicEqConfigureStatus configure(std::span<const Band> bands,
116 std::size_t transition_samples = 0) noexcept {
117 if (!prepared())
119 if (transitioning())
121 if (bands.size() > capacity_)
123 if (transition_samples == std::numeric_limits<std::size_t>::max())
125
126 std::array<Band, MaxBands> candidate_bands{};
127 std::array<Coefficients, MaxBands> candidate_coefficients{};
128 SampleType previous_frequency = SampleType{0};
129 const SampleType frequency_ceiling = supported_frequency_ceiling_hz();
130
131 for (std::size_t index = 0; index < bands.size(); ++index) {
132 const Band band = bands[index];
133 if (!(std::isfinite(band.frequency_hz) && std::isfinite(band.gain_db) &&
134 std::isfinite(band.q)))
136 if (band.frequency_hz < minimum_frequency_hz || band.frequency_hz > frequency_ceiling)
138 if (index != 0 && !(band.frequency_hz > previous_frequency))
140 if (band.gain_db < minimum_gain_db || band.gain_db > maximum_gain_db)
142 if (band.q < minimum_q || band.q > maximum_q)
144
145 candidate_bands[index] = band;
146 candidate_coefficients[index] = design_band(band);
147 if (!coefficients_are_finite(candidate_coefficients[index]))
149 if (!biquad_is_stable(candidate_coefficients[index]))
151 previous_frequency = band.frequency_hz;
152 }
153
154 if (same_requested_configuration(bands))
156
157 const std::size_t staging_index = 1 - active_index_;
158 const auto installed = cascades_[staging_index].set_coefficients(
159 std::span<const Coefficients>(candidate_coefficients.data(), bands.size()));
160 switch (installed) {
162 break;
171 }
172
173 requested_bands_ = candidate_bands;
174 requested_coefficients_ = candidate_coefficients;
175 requested_count_ = bands.size();
176
177 if (transition_samples == 0) {
178 active_index_ = staging_index;
179 commit_requested_endpoint();
180 cascades_[1 - active_index_].reset();
181 } else {
182 transition_total_ = transition_samples;
183 transition_remaining_ = transition_samples;
184 }
185 healthy_ = true;
187 }
188
189 SampleType process(SampleType input) noexcept {
190 if (!prepared())
191 return input;
192 if (!std::isfinite(input))
193 return recover_from_fault();
194
195 const SampleType from = cascades_[active_index_].process(input);
196 SampleType output = from;
197 if (transitioning()) {
198 const SampleType to = cascades_[1 - active_index_].process(input);
199 const std::size_t completed = transition_total_ - transition_remaining_ + 1;
200 const SampleType mix =
201 static_cast<SampleType>(completed) / static_cast<SampleType>(transition_total_);
202 // The final transition frame is the destination, not merely a
203 // mathematically equivalent interpolation expression. Besides
204 // making the endpoint exact, std::lerp avoids avoidable overflow
205 // when two earlier finite endpoints have opposite signs.
206 output = transition_remaining_ == 1 ? to : std::lerp(from, to, mix);
207 if (--transition_remaining_ == 0) {
208 active_index_ = 1 - active_index_;
209 commit_requested_endpoint();
210 cascades_[1 - active_index_].reset();
211 }
212 }
213
214 if (!std::isfinite(output))
215 return recover_from_fault();
216 healthy_ = true;
217 return output;
218 }
219
232 bool process_block(SampleType* samples, std::size_t frames) noexcept {
233 if (samples == nullptr && frames != 0)
234 return false;
235 if (!prepared())
236 return true;
238 frames, [&] { return transitioning(); },
239 [&](std::size_t i) { samples[i] = process(samples[i]); },
240 [&](std::size_t first, std::size_t count) { process_steady(samples + first, count); });
241 return true;
242 }
243
246 void reset() noexcept {
247 if (!prepared())
248 return;
249 if (transitioning()) {
250 active_index_ = 1 - active_index_;
251 commit_requested_endpoint();
252 }
253 cascades_[0].reset();
254 cascades_[1].reset();
255 transition_total_ = 0;
256 transition_remaining_ = 0;
257 healthy_ = true;
258 }
259
260 bool prepared() const noexcept {
261 return capacity_ != 0;
262 }
263 bool transitioning() const noexcept {
264 return transition_remaining_ != 0;
265 }
266 bool healthy() const noexcept {
267 return healthy_;
268 }
269 std::size_t fault_count() const noexcept {
270 return fault_count_;
271 }
272 std::size_t capacity() const noexcept {
273 return capacity_;
274 }
275 std::size_t band_count() const noexcept {
276 return requested_count_;
277 }
278 SampleType sample_rate() const noexcept {
279 return sample_rate_;
280 }
281 SampleType supported_frequency_ceiling_hz() const noexcept {
282 return std::min(maximum_frequency_hz, sample_rate_ * SampleType{0.49});
283 }
284 Band band(std::size_t index) const noexcept {
285 return index < requested_count_ ? requested_bands_[index] : Band{};
286 }
287 Coefficients coefficients(std::size_t index) const noexcept {
288 return index < requested_count_ ? requested_coefficients_[index] : Coefficients{};
289 }
290
294 static constexpr int latency_samples() noexcept {
295 return 0;
296 }
297 int tail_samples() const noexcept {
298 const auto has_non_neutral_band = [](const auto& bands, std::size_t count) {
299 for (std::size_t index = 0; index < count; ++index)
300 if (bands[index].gain_db != SampleType{0})
301 return true;
302 return false;
303 };
304 return has_non_neutral_band(active_bands_, active_count_) ||
305 has_non_neutral_band(requested_bands_, requested_count_)
306 ? -1
307 : 0;
308 }
309
312 double magnitude(double frequency_hz) const noexcept {
313 if (!prepared() || !std::isfinite(frequency_hz) || frequency_hz < 0.0 ||
314 frequency_hz > static_cast<double>(sample_rate_) * 0.5)
315 return std::numeric_limits<double>::quiet_NaN();
316
317 double result = 1.0;
318 const double omega = angular_frequency(frequency_hz, static_cast<double>(sample_rate_));
319 for (std::size_t index = 0; index < requested_count_; ++index)
320 result *= section_magnitude(requested_coefficients_[index], omega);
321 return result;
322 }
323
324 double magnitude_db(double frequency_hz) const noexcept {
325 const double linear = magnitude(frequency_hz);
326 if (!std::isfinite(linear))
327 return linear;
328 if (!(linear > 0.0))
329 return static_cast<double>(min_response_db);
330 const double db = 20.0 * std::log10(linear);
331 return !std::isfinite(db) || db < static_cast<double>(min_response_db)
332 ? static_cast<double>(min_response_db)
333 : db;
334 }
335
336 static constexpr std::size_t storage_capacity() noexcept {
337 return MaxBands;
338 }
339
340 private:
341 static bool coefficients_are_finite(const Coefficients& c) noexcept {
342 return std::isfinite(c.b0) && std::isfinite(c.b1) && std::isfinite(c.b2) &&
343 std::isfinite(c.a1) && std::isfinite(c.a2);
344 }
345
346 Coefficients design_band(const Band& band) const noexcept {
347 if (band.gain_db == SampleType{0})
348 return Coefficients{};
349 BiquadT<double> designer;
350 designer.set_coefficients(BiquadT<double>::Type::peaking,
351 static_cast<double>(band.frequency_hz),
352 static_cast<double>(band.q), static_cast<double>(sample_rate_),
353 static_cast<double>(band.gain_db));
354 const auto c = designer.coefficients();
355 return {static_cast<SampleType>(c.b0), static_cast<SampleType>(c.b1),
356 static_cast<SampleType>(c.b2), static_cast<SampleType>(c.a1),
357 static_cast<SampleType>(c.a2)};
358 }
359
360 bool same_requested_configuration(std::span<const Band> bands) const noexcept {
361 if (bands.size() != requested_count_)
362 return false;
363 for (std::size_t index = 0; index < bands.size(); ++index)
364 if (!(bands[index] == requested_bands_[index]))
365 return false;
366 return true;
367 }
368
369 void commit_requested_endpoint() noexcept {
370 active_bands_ = requested_bands_;
371 active_count_ = requested_count_;
372 transition_total_ = 0;
373 transition_remaining_ = 0;
374 }
375
376 // The block path with no transition pending (see process_block()).
377 void process_steady(SampleType* samples, std::size_t frames) noexcept {
378 constexpr SampleType kMaxFinite = std::numeric_limits<SampleType>::max();
379 std::size_t frame = 0;
380 while (frame < frames) {
381 const std::size_t n = std::min(frames - frame, kSnapChunkSamples);
382 SampleType* chunk = samples + frame;
383 // Finite prefix of the input: it runs on the block path; the
384 // first non-finite input, if any, goes to process() on its own.
385 std::size_t finite = 0;
386 while (finite < n && std::abs(chunk[finite]) <= kMaxFinite)
387 ++finite;
388 if (finite == 0) {
389 chunk[0] = process(chunk[0]);
390 ++frame;
391 continue;
392 }
393 SampleType input[kSnapChunkSamples];
394 std::copy(chunk, chunk + finite, input);
395 cascades_[active_index_].process(chunk, finite);
396 std::size_t good = 0;
397 while (good < finite && std::abs(chunk[good]) <= kMaxFinite)
398 ++good;
399 if (good > 0)
400 healthy_ = true;
401 if (good == finite) {
402 frame += finite;
403 continue;
404 }
405 // The cascade overflowed at `good`: recover there, as process()
406 // would, and continue from the next input on the reset cascade.
407 chunk[good] = recover_from_fault();
408 std::copy(input + good + 1, input + finite, chunk + good + 1);
409 frame += good + 1;
410 }
411 }
412
413 SampleType recover_from_fault() noexcept {
414 cascades_[0].reset();
415 cascades_[1].reset();
416 ++fault_count_;
417 healthy_ = false;
418 return SampleType{0};
419 }
420
421 std::array<SosCascadeT<SampleType, MaxBands>, 2> cascades_{};
422 std::array<Band, MaxBands> active_bands_{};
423 std::array<Band, MaxBands> requested_bands_{};
424 std::array<Coefficients, MaxBands> requested_coefficients_{};
425 SampleType sample_rate_ = SampleType{0};
426 std::size_t capacity_ = 0;
427 std::size_t active_index_ = 0;
428 std::size_t active_count_ = 0;
429 std::size_t requested_count_ = 0;
430 std::size_t transition_total_ = 0;
431 std::size_t transition_remaining_ = 0;
432 std::size_t fault_count_ = 0;
433 bool healthy_ = true;
434};
435
438
439} // namespace pulp::signal
@ peaking
Definition biquad.hpp:58
A bounded cascade of configurable peaking bands for one audio channel.
Definition graphic_eq.hpp:70
bool prepared() const noexcept
Definition graphic_eq.hpp:260
GraphicEqPrepareStatus prepare(SampleType sample_rate, std::size_t band_capacity=MaxBands) noexcept
Definition graphic_eq.hpp:88
int tail_samples() const noexcept
Definition graphic_eq.hpp:297
Coefficients coefficients(std::size_t index) const noexcept
Definition graphic_eq.hpp:287
GraphicEqConfigureStatus configure(std::span< const Band > bands, std::size_t transition_samples=0) noexcept
Definition graphic_eq.hpp:115
bool healthy() const noexcept
Definition graphic_eq.hpp:266
double magnitude(double frequency_hz) const noexcept
Stationary response of the requested endpoint.
Definition graphic_eq.hpp:312
SampleType supported_frequency_ceiling_hz() const noexcept
Definition graphic_eq.hpp:281
static constexpr float maximum_sample_rate_hz
Definition graphic_eq.hpp:80
static constexpr float minimum_sample_rate_hz
Definition graphic_eq.hpp:79
static constexpr float minimum_frequency_hz
Definition graphic_eq.hpp:81
static constexpr int latency_samples() noexcept
The cascade is causal IIR and adds no bulk delay.
Definition graphic_eq.hpp:294
std::size_t band_count() const noexcept
Definition graphic_eq.hpp:275
SampleType process(SampleType input) noexcept
Definition graphic_eq.hpp:189
Band band(std::size_t index) const noexcept
Definition graphic_eq.hpp:284
bool process_block(SampleType *samples, std::size_t frames) noexcept
Process caller-owned storage in place.
Definition graphic_eq.hpp:232
GraphicEqBandT< SampleType > Band
Definition graphic_eq.hpp:76
static constexpr float maximum_gain_db
Definition graphic_eq.hpp:84
static constexpr float maximum_frequency_hz
Definition graphic_eq.hpp:82
BiquadCoefficientsT< SampleType > Coefficients
Definition graphic_eq.hpp:77
void reset() noexcept
Clear recursive history and, if a transition is active, select its requested endpoint.
Definition graphic_eq.hpp:246
static constexpr float minimum_q
Definition graphic_eq.hpp:85
std::size_t fault_count() const noexcept
Definition graphic_eq.hpp:269
static constexpr float maximum_q
Definition graphic_eq.hpp:86
static constexpr float minimum_gain_db
Definition graphic_eq.hpp:83
std::size_t capacity() const noexcept
Definition graphic_eq.hpp:272
float sample_rate() const noexcept
Definition graphic_eq.hpp:278
static constexpr std::size_t storage_capacity() noexcept
Definition graphic_eq.hpp:336
double magnitude_db(double frequency_hz) const noexcept
Definition graphic_eq.hpp:324
bool transitioning() const noexcept
Definition graphic_eq.hpp:263
Evaluate what a biquad (or a cascade of them) does to the spectrum.
@ index
Definition gpu_pass.hpp:140
void transition_then_block(std::size_t frames, Transitioning &&transitioning, Sample &&sample, Block &&block)
The block path the cascade EQs share: a design transition (crossfade or coefficient ramp) runs sample...
Definition sos_cascade.hpp:39
Definition additive_bank.hpp:218
@ first
Definition dither.hpp:23
constexpr float min_response_db
Floor applied to dB conversions.
Definition frequency_response.hpp:32
double section_magnitude(const BiquadCoefficientsT< SampleType > &c, double omega)
Linear magnitude |H(e^jω)| of one section at normalized angular frequency ω = 2π·f/fs (radians/sample...
Definition frequency_response.hpp:45
@ c
Definition spectrum_trace.hpp:35
GraphicEqBandT< float > GraphicEqBand
Definition graphic_eq.hpp:47
constexpr std::size_t kSnapChunkSamples
Samples a block path runs between denormal snaps of its recursive state.
Definition denormal.hpp:113
GraphicEqT< float > GraphicEq
Definition graphic_eq.hpp:436
@ not_prepared
Definition comb_filter.hpp:33
@ non_finite
Definition sos_cascade.hpp:20
@ over_capacity
Definition sos_cascade.hpp:19
@ installed
Definition sos_cascade.hpp:17
@ unstable
Definition sos_cascade.hpp:21
@ not_prepared
Definition sos_cascade.hpp:18
@ invalid_sample_rate
Definition auto_ducked_send.hpp:18
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
GraphicEqConfigureStatus
Definition graphic_eq.hpp:25
@ frequency_out_of_range
Definition graphic_eq.hpp:31
@ non_finite
Definition graphic_eq.hpp:30
@ over_capacity
Definition graphic_eq.hpp:28
@ configured
Definition graphic_eq.hpp:26
@ q_out_of_range
Definition graphic_eq.hpp:34
@ unstable
Definition graphic_eq.hpp:35
@ frequencies_not_strictly_increasing
Definition graphic_eq.hpp:32
@ invalid_transition
Definition graphic_eq.hpp:29
@ not_prepared
Definition graphic_eq.hpp:27
@ transition_in_progress
Definition graphic_eq.hpp:36
@ gain_out_of_range
Definition graphic_eq.hpp:33
@ i
Definition formant_filter_bank.hpp:20
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
double angular_frequency(double freq_hz, double sample_rate)
Angular frequency (radians/sample) of freq_hz at sample_rate, clamped to [0, π].
Definition frequency_response.hpp:97
GraphicEqT< double > GraphicEq64
Definition graphic_eq.hpp:437
@ prepared
Definition finite_stretch_builder.hpp:19
GraphicEqPrepareStatus
Definition graphic_eq.hpp:19
@ invalid_sample_rate
Definition graphic_eq.hpp:21
@ prepared
Definition graphic_eq.hpp:20
@ invalid_capacity
Definition graphic_eq.hpp:22
@ non_finite
Definition formant_filter_bank.hpp:26
@ configured
Definition formant_filter_bank.hpp:23
@ unstable
Definition formant_filter_bank.hpp:29
@ n
Definition input_events.hpp:40
Fixed-capacity runtime executor for cascades of second-order IIR sections.
Definition biquad.hpp:24
Definition graphic_eq.hpp:39
float gain_db
Definition graphic_eq.hpp:41
float q
Definition graphic_eq.hpp:42
float frequency_hz
Definition graphic_eq.hpp:40
friend constexpr bool operator==(const GraphicEqBandT &, const GraphicEqBandT &)=default