DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Equalizer.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
27#include "Filters.h"
28#include "../Core/AudioSpec.h"
29#include "../Core/AudioBuffer.h"
30#include "../Core/Biquad.h"
31#include "../Core/DenormalGuard.h"
32#include "../Core/DspMath.h"
33#include "../Core/FFT.h"
34#include "../Core/StateBlob.h"
35
36#include <algorithm>
37#include <array>
38#include <atomic>
39#include <cmath>
40#include <cstddef>
41#include <cstdint>
42#include <cstdio>
43#include <memory>
44#include <vector>
45
46namespace dspark {
47
55template <FloatType T, int MaxBands = 16>
57{
58public:
60 enum class FilterMode
61 {
64 };
65
67 ~Equalizer() = default;
68
70 enum class BandType
71 {
72 Peak,
73 LowShelf,
74 HighShelf,
75 LowPass,
76 HighPass,
77 Notch,
78 BandPass,
79 Tilt
80 };
81
86 {
87 T frequency = T(1000);
88 T gain = T(0);
89 T q = T(0.707);
91 int slope = 12;
92 bool enabled = true;
93 };
94
95 // -- Lifecycle --------------------------------------------------------------
96
108 void prepare(const AudioSpec& spec)
109 {
110 if (!spec.isValid()) return; // release-safe: keep previous state
111
112 spec_ = spec;
113 for (int i = 0; i < MaxBands; ++i)
114 bands_[i].prepare(spec);
115
116 // Rebuild the linear-phase engine. lpFft_ gates the LP audio path, so
117 // it is torn down first and re-created last (basic guarantee if an
118 // allocation throws mid-way).
119 lpFft_.reset();
120 lpBlock_ = spec.maxBlockSize;
121
122 // The FFT size is 4x the block size; cap it so the pow2 round-up can
123 // never overflow int with an absurd host block size (LP mode is then
124 // unavailable and getLatency() honestly reports 0).
126 {
127 lpBlock_ = 0;
128 lpFftSize_ = 0;
129 return;
130 }
131
132 // Overlap-save requires FFT size N >= L + M - 1
133 // Where L is maxBlockSize, M is impulse response length.
134 // We use M = 2 * L, so N must be >= 3 * L. We round up to next power of 2.
135 int targetFftSize = lpBlock_ * 4;
136 int fftPow2 = 1;
137 while (fftPow2 < targetFftSize) fftPow2 <<= 1;
138 lpFftSize_ = fftPow2;
139
140 // Complex kernel representation (Real, Imaginary interleaved)
141 lpKernel_.assign(static_cast<size_t>(lpFftSize_ + 2), T(0));
142
143 // Overlap-save needs (M-1) samples of history where M = 2*maxBlockSize
144 // is the FIR kernel length. Size the history buffer accordingly.
145 lpPrevBlock_.resize(static_cast<size_t>(spec.numChannels));
146 for (auto& pb : lpPrevBlock_)
147 pb.assign(static_cast<size_t>(lpBlock_ * 2), T(0));
148
149 lpFftIn_.assign(static_cast<size_t>(lpFftSize_), T(0));
150 lpFftOut_.assign(static_cast<size_t>(lpFftSize_ + 2), T(0));
151
152 // Pre-allocate recompute scratch so a live band change never allocates on
153 // the audio thread (recomputeLinearPhaseKernel runs there via processBlock).
154 lpMagScratch_.assign(static_cast<size_t>(lpFftSize_ / 2 + 1), T(1));
155 lpTempFreq_.assign(static_cast<size_t>(lpFftSize_ + 2), T(0));
156 lpImpulse_.assign(static_cast<size_t>(lpFftSize_), T(0));
157 lpKernelSpace_.assign(static_cast<size_t>(lpFftSize_), T(0));
158
159 lpDirty_.store(true, std::memory_order_release);
160 lpFft_ = std::make_unique<FFTReal<T>>(lpFftSize_); // gate opens last
161 }
162
174 void processBlock(AudioBufferView<T> buffer) noexcept
175 {
176 DenormalGuard guard;
177
178 // Front-door non-finite guard (M-006 C1): the IIR bands (and the FFT
179 // overlap-save history in LinearPhase mode) latch a NaN/Inf input
180 // permanently. Scrub non-finite input to 0 before either path runs.
181 // No-op on finite input, so conformance metrics stay byte-identical.
182 {
183 const int gN = buffer.getNumSamples();
184 for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
185 {
186 T* d = buffer.getChannel(ch);
187 for (int i = 0; i < gN; ++i)
188 if (!std::isfinite(d[i])) d[i] = T(0);
189 }
190 }
191
192 // Check if config was updated from the UI thread (Lock-free acquire)
193 if (configDirty_.exchange(false, std::memory_order_acquire))
194 {
196 lpDirty_.store(true, std::memory_order_release);
197 }
198
199 FilterMode currentMode = filterMode_.load(std::memory_order_acquire);
200
201 if (currentMode == FilterMode::LinearPhase && lpFft_)
202 {
203 // If kernel needs recalculation, do it once.
204 if (lpDirty_.exchange(false, std::memory_order_acquire))
206
207 processLinearPhase(buffer);
208 return;
209 }
210
211 // Minimum Phase (IIR) Processing
212 const int activeBands = numBands_.load(std::memory_order_relaxed);
213 for (int i = 0; i < activeBands; ++i)
214 {
215 if (bandEnabled_[i].load(std::memory_order_relaxed))
216 bands_[i].processBlock(buffer);
217 }
218 }
219
231 [[nodiscard]] T processSample(T input, int channel) noexcept
232 {
233 // Plain load first: the exchange RMW is only paid when a publication
234 // is actually pending (this runs per sample).
235 if (configDirty_.load(std::memory_order_acquire)
236 && configDirty_.exchange(false, std::memory_order_acquire))
237 {
239 const int n = numBands_.load(std::memory_order_relaxed);
240 for (int i = 0; i < n; ++i)
241 bands_[i].applyParametersNow();
242 lpDirty_.store(true, std::memory_order_release);
243 }
244
245 T sample = input;
246 const int activeBands = numBands_.load(std::memory_order_relaxed);
247
248 for (int i = 0; i < activeBands; ++i)
249 {
250 if (bandEnabled_[i].load(std::memory_order_relaxed))
251 sample = bands_[i].processSample(sample, channel);
252 }
253 return sample;
254 }
255
259 void reset() noexcept
260 {
261 for (int i = 0; i < MaxBands; ++i)
262 bands_[i].reset();
263
264 for (auto& pb : lpPrevBlock_)
265 std::fill(pb.begin(), pb.end(), T(0));
266 }
267
268 // -- API --------------------------------------------------------------------
269
276 void setBand(int index, T frequency, T gainDb)
277 {
278 setBand(index, frequency, gainDb, T(0.707));
279 }
280
288 void setBand(int index, T frequency, T gainDb, T q)
289 {
290 BandConfig cfg;
291 cfg.frequency = frequency;
292 cfg.gain = gainDb;
293 cfg.q = q;
294 cfg.type = BandType::Peak;
295 cfg.slope = 12;
296 cfg.enabled = true;
297 setBand(index, cfg);
298 }
299
310 void setBand(int index, const BandConfig& config)
311 {
312 if (index < 0 || index >= MaxBands) return;
313
314 BandConfig cfg = config;
315 const BandConfig& prev = configs_[index];
316 if (!std::isfinite(cfg.frequency)) cfg.frequency = prev.frequency;
317 if (!std::isfinite(cfg.gain)) cfg.gain = prev.gain;
318 if (!std::isfinite(cfg.q)) cfg.q = prev.q;
319 cfg.type = static_cast<BandType>(std::clamp(static_cast<int>(cfg.type), 0,
320 static_cast<int>(BandType::Tilt)));
321 cfg.slope = std::clamp(cfg.slope, 6, 48);
322
323 configs_[index] = cfg;
324 bandEnabled_[index].store(cfg.enabled, std::memory_order_release);
325
326 int currentBands = numBands_.load(std::memory_order_relaxed);
327 if (index >= currentBands)
328 numBands_.store(index + 1, std::memory_order_relaxed);
329
330 // Signal audio thread to update filters
331 configDirty_.store(true, std::memory_order_release);
332 }
333
338 void setNumBands(int count)
339 {
340 int validCount = std::clamp(count, 1, MaxBands);
341 numBands_.store(validCount, std::memory_order_relaxed);
342
343 const T logMin = std::log(T(80));
344 const T logMax = std::log(T(16000));
345
346 for (int i = 0; i < validCount; ++i)
347 {
348 T t = (validCount > 1) ? static_cast<T>(i) / static_cast<T>(validCount - 1) : T(0.5);
349
350 BandConfig cfg;
351 cfg.frequency = std::exp(logMin + t * (logMax - logMin));
352 cfg.gain = T(0);
353 cfg.q = T(0.707);
354 cfg.type = BandType::Peak;
355 cfg.slope = 12;
356 cfg.enabled = true;
357
358 configs_[i] = cfg;
359 bandEnabled_[i].store(true, std::memory_order_release);
360 }
361 configDirty_.store(true, std::memory_order_release);
362 }
363
365 [[nodiscard]] int getNumBands() const noexcept
366 {
367 return numBands_.load(std::memory_order_relaxed);
368 }
369
380 void setMatchedBells(bool enabled) noexcept
381 {
382 matchedBells_.store(enabled, std::memory_order_relaxed);
383 configDirty_.store(true, std::memory_order_release);
384 }
385
391 [[nodiscard]] BandConfig getBandConfig(int index) const noexcept
392 {
393 if (index < 0 || index >= MaxBands) return {};
394 return configs_[index]; // Note: Struct copy is safe if mostly read from GUI
395 }
396
402 void setBandEnabled(int index, bool enabled) noexcept
403 {
404 if (index >= 0 && index < MaxBands)
405 {
406 configs_[index].enabled = enabled;
407 bandEnabled_[index].store(enabled, std::memory_order_release);
408 configDirty_.store(true, std::memory_order_release);
409 }
410 }
411
421 void setFilterMode(FilterMode mode) noexcept
422 {
423 const int m = std::clamp(static_cast<int>(mode), 0,
424 static_cast<int>(FilterMode::LinearPhase));
425 filterMode_.store(static_cast<FilterMode>(m), std::memory_order_release);
426 if (static_cast<FilterMode>(m) == FilterMode::LinearPhase)
427 lpDirty_.store(true, std::memory_order_release);
428 }
429
431 [[nodiscard]] FilterMode getFilterMode() const noexcept
432 {
433 return filterMode_.load(std::memory_order_relaxed);
434 }
435
443 [[nodiscard]] int getLatency() const noexcept
444 {
445 return (filterMode_.load(std::memory_order_relaxed) == FilterMode::LinearPhase && lpFft_)
446 ? lpBlock_ : 0;
447 }
448
453 void setSoftMode(bool enabled) noexcept
454 {
455 softMode_.store(enabled, std::memory_order_relaxed);
456 configDirty_.store(true, std::memory_order_release);
457 }
458
460 [[nodiscard]] bool getSoftMode() const noexcept
461 {
462 return softMode_.load(std::memory_order_relaxed);
463 }
464
476 void getMagnitudeForFrequencyArray(const T* frequencies, T* magnitudes, int numPoints) const noexcept
477 {
478 for (int i = 0; i < numPoints; ++i)
479 magnitudes[i] = T(1);
480
481 const int activeBands = numBands_.load(std::memory_order_relaxed);
482 for (int b = 0; b < activeBands; ++b)
483 {
484 if (!configs_[b].enabled) continue;
485
486 BiquadCoeffs st[5];
487 const int ns = buildBandStages(configs_[b], st);
488
489 for (int i = 0; i < numPoints; ++i)
490 {
491 double mag = 1.0;
492 for (int s = 0; s < ns; ++s)
493 mag *= st[s].getMagnitude(static_cast<double>(frequencies[i]), spec_.sampleRate);
494 magnitudes[i] = static_cast<T>(static_cast<double>(magnitudes[i]) * mag);
495 }
496 }
497 }
498
504 FilterEngine<T>& getBandFilter(int index) { return bands_[index]; }
505
507 const FilterEngine<T>& getBandFilter(int index) const { return bands_[index]; }
508
509
511 [[nodiscard]] std::vector<uint8_t> getState() const
512 {
513 StateWriter w(stateId("PEQZ"), 1);
514 const int n = numBands_.load(std::memory_order_relaxed);
515 w.write("numBands", n);
516 w.write("matchedBells", matchedBells_.load(std::memory_order_relaxed));
517 w.write("filterMode", static_cast<int32_t>(filterMode_.load(std::memory_order_relaxed)));
518 w.write("softMode", softMode_.load(std::memory_order_relaxed));
519 char key[24];
520 for (int i = 0; i < n; ++i)
521 {
522 const BandConfig cfg = getBandConfig(i);
523 std::snprintf(key, sizeof(key), "b%d.freq", i);
524 w.write(key, static_cast<float>(cfg.frequency));
525 std::snprintf(key, sizeof(key), "b%d.gain", i);
526 w.write(key, static_cast<float>(cfg.gain));
527 std::snprintf(key, sizeof(key), "b%d.q", i);
528 w.write(key, static_cast<float>(cfg.q));
529 std::snprintf(key, sizeof(key), "b%d.type", i);
530 w.write(key, static_cast<int32_t>(cfg.type));
531 std::snprintf(key, sizeof(key), "b%d.slope", i);
532 w.write(key, cfg.slope);
533 std::snprintf(key, sizeof(key), "b%d.on", i);
534 w.write(key, cfg.enabled);
535 }
536 return w.blob();
537 }
538
540 bool setState(const uint8_t* data, size_t size)
541 {
542 StateReader r(data, size);
543 if (!r.isValid() || r.processorId() != stateId("PEQZ")) return false;
544 const int n = std::clamp(r.read("numBands", 0), 0, MaxBands);
545 setMatchedBells(r.read("matchedBells", false));
546 // Older blobs carry no mode keys: keep the instance's current modes.
547 setFilterMode(static_cast<FilterMode>(
548 r.read("filterMode", static_cast<int32_t>(filterMode_.load(std::memory_order_relaxed)))));
549 setSoftMode(r.read("softMode", softMode_.load(std::memory_order_relaxed)));
550 char key[24];
551 for (int i = 0; i < n; ++i)
552 {
553 BandConfig cfg;
554 std::snprintf(key, sizeof(key), "b%d.freq", i);
555 cfg.frequency = static_cast<T>(r.read(key, 1000.0f));
556 std::snprintf(key, sizeof(key), "b%d.gain", i);
557 cfg.gain = static_cast<T>(r.read(key, 0.0f));
558 std::snprintf(key, sizeof(key), "b%d.q", i);
559 cfg.q = static_cast<T>(r.read(key, 0.707f));
560 std::snprintf(key, sizeof(key), "b%d.type", i);
561 cfg.type = static_cast<BandType>(r.read(key, 0));
562 std::snprintf(key, sizeof(key), "b%d.slope", i);
563 cfg.slope = r.read(key, 12);
564 std::snprintf(key, sizeof(key), "b%d.on", i);
565 cfg.enabled = r.read(key, true);
566 setBand(i, cfg);
567 }
568 numBands_.store(n, std::memory_order_relaxed);
569 configDirty_.store(true, std::memory_order_release);
570 return true;
571 }
572
573protected:
574
578 [[nodiscard]] T effectiveQ(const BandConfig& cfg) const noexcept
579 {
580 T q = cfg.q;
581 if (softMode_.load(std::memory_order_relaxed))
582 {
583 const T absGain = std::abs(cfg.gain);
584 const T maxQ = T(1) + T(8) / (absGain + T(1));
585 q = std::min(q, maxQ);
586 }
587 return q;
588 }
589
594 void updateActiveFilters() noexcept
595 {
596 const bool matched = matchedBells_.load(std::memory_order_relaxed);
597 int activeBands = numBands_.load(std::memory_order_relaxed);
598
599 for (int i = 0; i < activeBands; ++i)
600 {
601 auto& filter = bands_[i];
602 const auto& cfg = configs_[i]; // Thread-safe copy from atomic boundaries
603
604 float freq = static_cast<float>(cfg.frequency);
605 float gain = static_cast<float>(cfg.gain);
606 float q = static_cast<float>(effectiveQ(cfg));
607
608 filter.setMatchedPeak(matched);
609 switch (cfg.type)
610 {
611 case BandType::Peak: filter.setPeaking(freq, gain, q); break;
612 // Shelves take a SLOPE (0..1], not a Q: convert with the RBJ
613 // S<->Q relation so the user's Q behaves consistently here and
614 // in the linear-phase kernel (which uses the same conversion).
615 case BandType::LowShelf: filter.setLowShelf(freq, gain,
616 static_cast<float>(shelfSlopeFromQ(q, gain))); break;
617 case BandType::HighShelf: filter.setHighShelf(freq, gain,
618 static_cast<float>(shelfSlopeFromQ(q, gain))); break;
619 case BandType::LowPass: filter.setLowPass(freq, q, cfg.slope); break;
620 case BandType::HighPass: filter.setHighPass(freq, q, cfg.slope); break;
621 case BandType::Notch: filter.setNotch(freq, q); break;
622 case BandType::BandPass: filter.setBandPass(freq, q); break;
623 case BandType::Tilt: filter.setTilt(freq, gain); break;
624 }
625 }
626 }
627
635 [[nodiscard]] static double shelfSlopeFromQ(double q, double gainDb) noexcept
636 {
637 q = std::max(q, 0.05);
638 const double A = std::pow(10.0, std::abs(gainDb) / 40.0);
639 const double denom = A + 1.0 / A;
640 const double invS = (1.0 / (q * q) - 2.0) / denom + 1.0;
641 if (invS <= 1.0) return 1.0; // steeper-than-standard requests clamp to S = 1
642 return std::clamp(1.0 / invS, 0.0001, 1.0);
643 }
644
650 [[nodiscard]] BiquadCoeffs computeBandCoeffs(const BandConfig& cfg) const noexcept
651 {
652 double sr = spec_.sampleRate;
653 double f = static_cast<double>(cfg.frequency);
654 double g = static_cast<double>(cfg.gain);
655 double q = static_cast<double>(cfg.q);
656
657 switch (cfg.type)
658 {
659 case BandType::Peak:
660 return matchedBells_.load(std::memory_order_relaxed)
661 ? BiquadCoeffs::makePeakMatched(sr, f, q, g)
662 : BiquadCoeffs::makePeak(sr, f, q, g);
665 case BandType::LowPass: return BiquadCoeffs::makeLowPass(sr, f, q);
666 case BandType::HighPass: return BiquadCoeffs::makeHighPass(sr, f, q);
667 case BandType::Notch: return BiquadCoeffs::makeNotch(sr, f, q);
668 case BandType::BandPass: return BiquadCoeffs::makeBandPass(sr, f, q);
669 case BandType::Tilt: return BiquadCoeffs::makeTilt(sr, f, g);
670 }
671 return {};
672 }
673
674public:
685 [[nodiscard]] int buildBandStages(const BandConfig& cfg, BiquadCoeffs* stages) const noexcept
686 {
687 const double sr = spec_.sampleRate;
688 // Mirror the FilterEngine's own clamps so the analysis matches the audio.
689 const double f = std::clamp(static_cast<double>(cfg.frequency), 10.0, sr * 0.499);
690 const T q = std::max(effectiveQ(cfg), T(0.1));
691
692 if (cfg.type == BandType::LowPass || cfg.type == BandType::HighPass)
693 {
694 const bool lp = (cfg.type == BandType::LowPass);
695 // The user Q scales the final cascade stage exactly like the engine.
696 auto casc = FilterEngine<T>::cascadeForSlope(cfg.slope, static_cast<float>(q));
697 int n = 0;
698 if (casc.hasFirstOrder)
699 stages[n++] = lp ? BiquadCoeffs::makeFirstOrderLowPass(sr, f)
701 for (int s = 0; s < casc.numSecondOrder; ++s)
702 {
703 const double stageQ = static_cast<double>(casc.qValues[s]);
704 stages[n++] = lp ? BiquadCoeffs::makeLowPass(sr, f, stageQ)
705 : BiquadCoeffs::makeHighPass(sr, f, stageQ);
706 }
707 return n;
708 }
709
710 BandConfig eff = cfg;
711 eff.frequency = static_cast<T>(f);
712 eff.q = q;
713 stages[0] = computeBandCoeffs(eff);
714 return 1;
715 }
716
717protected:
727 {
728 if (lpFftSize_ == 0) return;
729
730 const int numBins = lpFftSize_ / 2 + 1;
731 T* const mag = lpMagScratch_.data(); // pre-allocated scratch (no audio-thread alloc)
732 std::fill_n(mag, numBins, T(1));
733 const int activeBands = numBands_.load(std::memory_order_relaxed);
734
735 // 1. Accumulate the TRUE per-stage cascade magnitude of all active bands
736 // (matches what the IIR FilterEngine applies in MinimumPhase mode, so
737 // switching modes keeps the same magnitude response).
738 const double sr = spec_.sampleRate;
739 for (int b = 0; b < activeBands; ++b)
740 {
741 if (!configs_[b].enabled) continue;
742
743 BiquadCoeffs st[5];
744 const int ns = buildBandStages(configs_[b], st);
745
746 for (int k = 0; k < numBins; ++k)
747 {
748 const double freq = sr * static_cast<double>(k) / static_cast<double>(lpFftSize_);
749 double m = 1.0;
750 for (int s = 0; s < ns; ++s)
751 m *= st[s].getMagnitude(freq, sr);
752 mag[k] = static_cast<T>(static_cast<double>(mag[k]) * m);
753 }
754 }
755
756 // 2. Prepare Zero-Phase Frequency buffer (Real = Mag, Imag = 0)
757 std::fill(lpTempFreq_.begin(), lpTempFreq_.end(), T(0));
758 for (int k = 0; k < numBins; ++k)
759 lpTempFreq_[2 * k] = mag[k];
760
761 // 3. IFFT to get temporal impulse response (wrapped around t=0)
762 lpFft_->inverse(lpTempFreq_.data(), lpImpulse_.data());
763
764 // 4. Shift, Windowing and Zero-pad for Overlap-Save
765 const int M = lpBlock_ * 2; // Desired Kernel length
766 const int halfM = M / 2;
767 std::fill(lpKernelSpace_.begin(), lpKernelSpace_.end(), T(0)); // zero-pad scratch
768
769 for (int i = 0; i < M; ++i)
770 {
771 // Circular read from center 0
772 int readIdx = (i - halfM + lpFftSize_) % lpFftSize_;
773
774 // Blackman-Harris window formulation
775 double t = static_cast<double>(i) / static_cast<double>(M - 1);
776 double window = 0.35875
777 - 0.48829 * std::cos(2.0 * pi<double> * t)
778 + 0.14128 * std::cos(4.0 * pi<double> * t)
779 - 0.01168 * std::cos(6.0 * pi<double> * t);
780
781 // No extra scaling: FFTReal::inverse already applies the full 1/N
782 // normalisation (verified by exact round-trip). The previous extra
783 // division by lpFftSize_ attenuated the whole linear-phase path by
784 // 20*log10(N) dB - about -60 dB with the default sizes.
785 lpKernelSpace_[i] = lpImpulse_[readIdx] * static_cast<T>(window);
786 }
787
788 // 5. Transform finalized zero-padded causal kernel to frequency domain
789 lpFft_->forward(lpKernelSpace_.data(), lpKernel_.data());
790 }
791
801 {
802 // Safety bound check: Prevent out-of-bounds if host pushes dynamic channel
803 // counts. Channels beyond the prepared count pass through untouched.
804 const int nCh = std::min(buffer.getNumChannels(), static_cast<int>(lpPrevBlock_.size()));
805 const int L = buffer.getNumSamples();
806 const int N = lpFftSize_;
807 const int M = lpBlock_ * 2; // Kernel length
808
809 // Safety check: block size cannot exceed pre-allocated maxBlockSize
810 if (L > lpBlock_) return;
811
812 const int overlapSize = M - 1; // FIR history overlap-save needs (kernel len - 1)
813
814 for (int ch = 0; ch < nCh; ++ch)
815 {
816 T* channelData = buffer.getChannel(ch);
817 auto& prev = lpPrevBlock_[ch];
818
819 // 1. Build overlap-save input: [history (overlapSize) | current (L) | zeros].
820 // The full (M-1)-sample history is required for a length-M FIR;
821 // the previous code only kept L samples, corrupting the output
822 // (especially for blocks smaller than maxBlockSize).
823 for (int i = 0; i < overlapSize; ++i)
824 lpFftIn_[i] = prev[i];
825 for (int i = 0; i < L; ++i)
826 lpFftIn_[overlapSize + i] = channelData[i];
827 for (int i = overlapSize + L; i < N; ++i)
828 lpFftIn_[i] = T(0);
829
830 // 2. Save the LAST overlapSize samples of [history | current] as the next
831 // block's history (read from lpFftIn_ before the inverse FFT reuses it).
832 for (int i = 0; i < overlapSize; ++i)
833 prev[i] = lpFftIn_[L + i];
834
835 // 3. Forward FFT
836 lpFft_->forward(lpFftIn_.data(), lpFftOut_.data());
837
838 // 4. Complex multiplication: H(k) * X(k)
839 int numBins = N / 2 + 1;
840 for (int k = 0; k < numBins; ++k)
841 {
842 T realX = lpFftOut_[2 * k];
843 T imagX = lpFftOut_[2 * k + 1];
844 T realH = lpKernel_[2 * k];
845 T imagH = lpKernel_[2 * k + 1];
846
847 lpFftOut_[2 * k] = realX * realH - imagX * imagH;
848 lpFftOut_[2 * k + 1] = realX * imagH + imagX * realH;
849 }
850
851 // 5. Inverse FFT
852 lpFft_->inverse(lpFftOut_.data(), lpFftIn_.data()); // Reusing lpFftIn_ to save memory
853
854 // 6. Overlap-save output extraction: valid data starts at index M - 1
855 int offset = M - 1;
856 for (int i = 0; i < L; ++i)
857 {
858 // Assign filtered output back to the channel
859 channelData[i] = lpFftIn_[offset + i];
860 }
861 }
862 }
863
864 // The pow2 round-up computes lpBlock_ * 4: cap the block size so that can
865 // never overflow int (hosts never get close; LP mode disables above it).
866 static constexpr int kLpMaxBlockSize = 1 << 18;
867
869 std::atomic<int> numBands_ { 0 };
870
871 std::array<FilterEngine<T>, MaxBands> bands_ {};
872 std::array<BandConfig, MaxBands> configs_ {};
873 // Per-band enable flag read lock-free every block. The full BandConfig is only
874 // read under the configDirty_ acquire gate; `enabled` is toggled often and read
875 // on the hot path, so it gets its own atomic to avoid a torn/unsynchronized read.
876 std::array<std::atomic<bool>, MaxBands> bandEnabled_ {};
877
878 std::atomic<bool> softMode_ { false };
879 std::atomic<bool> configDirty_ { false };
880 std::atomic<bool> matchedBells_ { false };
881
882 // Linear-phase state
883 std::atomic<FilterMode> filterMode_ { FilterMode::MinimumPhase };
884 std::atomic<bool> lpDirty_ { true };
885
886 std::unique_ptr<FFTReal<T>> lpFft_;
887 int lpFftSize_ = 0;
888 int lpBlock_ = 0;
889
890 std::vector<T> lpKernel_;
891 std::vector<std::vector<T>> lpPrevBlock_;
892 std::vector<T> lpFftIn_, lpFftOut_;
893 // Pre-allocated recompute scratch (no audio-thread allocation on band changes).
895};
896
897} // namespace dspark
Multi-mode cascaded biquad filter engine with real-time parameter smoothing.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Parametric multi-band EQ using cascaded biquads or FFT overlap-save convolution.
Definition Equalizer.h:57
std::vector< T > lpTempFreq_
Definition Equalizer.h:894
bool setState(const uint8_t *data, size_t size)
Restores bands and modes from a blob (tolerant; rejects foreign ids).
Definition Equalizer.h:540
BandType
Filter type for each EQ band.
Definition Equalizer.h:71
@ LowShelf
Shelf: boosts/cuts below frequency.
@ BandPass
Bandpass around frequency.
@ Tilt
Tilt EQ: pivots spectrum around frequency.
@ LowPass
Removes frequencies above cutoff.
@ Peak
Parametric bell (boost/cut around frequency).
@ Notch
Narrow rejection at frequency.
@ HighShelf
Shelf: boosts/cuts above frequency.
@ HighPass
Removes frequencies below cutoff.
void processLinearPhase(AudioBufferView< T > buffer) noexcept
Linear-phase processing via overlap-save FFT convolution.
Definition Equalizer.h:800
void setBand(int index, const BandConfig &config)
Configures a band with full control over all parameters. Thread-safe.
Definition Equalizer.h:310
bool getSoftMode() const noexcept
Returns whether soft mode is enabled.
Definition Equalizer.h:460
std::vector< T > lpFftOut_
Definition Equalizer.h:892
std::vector< T > lpKernel_
Definition Equalizer.h:890
std::atomic< bool > configDirty_
Definition Equalizer.h:879
T processSample(T input, int channel) noexcept
Processes a single sample through all enabled bands (IIR mode only).
Definition Equalizer.h:231
std::vector< uint8_t > getState() const
Serializes bands and modes (setup/UI threads; allocates).
Definition Equalizer.h:511
void setBand(int index, T frequency, T gainDb)
Configures a band with frequency and gain (Peak filter).
Definition Equalizer.h:276
std::atomic< bool > lpDirty_
Definition Equalizer.h:884
void reset() noexcept
Resets all filter states to zero to prevent ringing on playback start.
Definition Equalizer.h:259
FilterEngine< T > & getBandFilter(int index)
Direct access to a band's underlying FilterEngine.
Definition Equalizer.h:504
std::atomic< bool > matchedBells_
Definition Equalizer.h:880
T effectiveQ(const BandConfig &cfg) const noexcept
The band Q the audio path actually uses (soft mode caps it by gain).
Definition Equalizer.h:578
void prepare(const AudioSpec &spec)
Prepares all bands and allocates necessary resources for processing.
Definition Equalizer.h:108
int buildBandStages(const BandConfig &cfg, BiquadCoeffs *stages) const noexcept
Fills stages with the ACTUAL biquad cascade for a band (per-stage Butterworth Q for multi-stage LP/HP...
Definition Equalizer.h:685
static constexpr int kLpMaxBlockSize
Definition Equalizer.h:866
std::vector< std::vector< T > > lpPrevBlock_
Definition Equalizer.h:891
AudioSpec spec_
Definition Equalizer.h:868
FilterMode getFilterMode() const noexcept
Returns the current filter mode.
Definition Equalizer.h:431
std::atomic< int > numBands_
Definition Equalizer.h:869
std::vector< T > lpKernelSpace_
Definition Equalizer.h:894
int getNumBands() const noexcept
Returns the number of active bands.
Definition Equalizer.h:365
void setMatchedBells(bool enabled) noexcept
Switches Peak bands to the Orfanidis matched (de-cramped) design.
Definition Equalizer.h:380
void setNumBands(int count)
Sets the number of active bands with auto-logarithmic spacing.
Definition Equalizer.h:338
std::unique_ptr< FFTReal< T > > lpFft_
Definition Equalizer.h:886
void setBandEnabled(int index, bool enabled) noexcept
Enables or disables a band without changing its parameters.
Definition Equalizer.h:402
std::vector< T > lpMagScratch_
Definition Equalizer.h:894
std::vector< T > lpImpulse_
Definition Equalizer.h:894
void recomputeLinearPhaseKernel() noexcept
Mathematically robust Linear Phase kernel computation.
Definition Equalizer.h:726
FilterMode
Filter processing mode.
Definition Equalizer.h:61
@ LinearPhase
FFT-based overlap-save (block-size latency, zero phase distortion).
@ MinimumPhase
IIR biquads (zero latency, minimum phase shift). Default.
std::vector< T > lpFftIn_
Definition Equalizer.h:892
void setFilterMode(FilterMode mode) noexcept
Sets the filter processing mode (Minimum Phase or Linear Phase).
Definition Equalizer.h:421
~Equalizer()=default
Non-virtual destructor to prevent vtable instantiation (zero virtual dispatch).
const FilterEngine< T > & getBandFilter(int index) const
Const overload.
Definition Equalizer.h:507
std::atomic< bool > softMode_
Definition Equalizer.h:878
void setBand(int index, T frequency, T gainDb, T q)
Configures a band with frequency, gain, and Q.
Definition Equalizer.h:288
BandConfig getBandConfig(int index) const noexcept
Returns the current configuration of a band.
Definition Equalizer.h:391
std::array< std::atomic< bool >, MaxBands > bandEnabled_
Definition Equalizer.h:876
std::array< BandConfig, MaxBands > configs_
Definition Equalizer.h:872
static double shelfSlopeFromQ(double q, double gainDb) noexcept
Converts a user-facing shelf Q into the RBJ shelf slope S.
Definition Equalizer.h:635
void updateActiveFilters() noexcept
Translates BandConfigs into internal FilterEngine parameters safely. Called by processBlock when conf...
Definition Equalizer.h:594
int getLatency() const noexcept
Returns the latency in samples.
Definition Equalizer.h:443
int lpBlock_
Max block size the LP engine was sized for (= its latency).
Definition Equalizer.h:888
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place.
Definition Equalizer.h:174
std::atomic< FilterMode > filterMode_
Definition Equalizer.h:883
void setSoftMode(bool enabled) noexcept
Enables soft mode (anti-ringing Q reduction dynamically based on gain).
Definition Equalizer.h:453
BiquadCoeffs computeBandCoeffs(const BandConfig &cfg) const noexcept
Computes single-biquad coefficients for a band (analysis/kernel).
Definition Equalizer.h:650
void getMagnitudeForFrequencyArray(const T *frequencies, T *magnitudes, int numPoints) const noexcept
Computes the combined magnitude response of all enabled bands.
Definition Equalizer.h:476
std::array< FilterEngine< T >, MaxBands > bands_
Definition Equalizer.h:871
Professional multi-mode filter with cascaded biquad stages.
Definition Filters.h:72
static CascadeInfo cascadeForSlope(int slopeDb, float userQ=0.707f) noexcept
Returns the exact Butterworth cascade (first-order flag + per-stage Q values) used internally for a g...
Definition Filters.h:297
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:51
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
Stores normalised biquad coefficients (b0, b1, b2, a1, a2), always double.
Definition Biquad.h:91
static BiquadCoeffs makeFirstOrderHighPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) high-pass filter.
Definition Biquad.h:418
static BiquadCoeffs makePeakMatched(double sampleRate, double freq, double Q, double gainDb) noexcept
Peaking filter with prescribed Nyquist gain (Orfanidis design).
Definition Biquad.h:223
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
Definition Biquad.h:127
static BiquadCoeffs makeBandPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Band-pass filter (constant 0 dB peak gain).
Definition Biquad.h:156
static BiquadCoeffs makePeak(double sampleRate, double freq, double Q, double gainDb) noexcept
Peak (parametric EQ) filter.
Definition Biquad.h:185
static BiquadCoeffs makeFirstOrderLowPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) low-pass filter.
Definition Biquad.h:395
static BiquadCoeffs makeTilt(double sampleRate, double pivotFreq, double gainDb) noexcept
Creates a first-order tilt filter.
Definition Biquad.h:444
static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Low-pass filter.
Definition Biquad.h:103
static BiquadCoeffs makeLowShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
Low-shelf filter.
Definition Biquad.h:283
static BiquadCoeffs makeHighShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
High-shelf filter.
Definition Biquad.h:313
static BiquadCoeffs makeNotch(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Notch (band-reject) filter.
Definition Biquad.h:342
Full configuration for a single EQ band.
Definition Equalizer.h:86
T q
Q factor (0.1 = wide, 10 = narrow).
Definition Equalizer.h:89
T gain
Gain in dB (Peak, Shelf, Tilt).
Definition Equalizer.h:88
bool enabled
False to bypass this band.
Definition Equalizer.h:92
int slope
Slope in dB/oct (LP/HP only: 6-48).
Definition Equalizer.h:91
BandType type
Filter type for this band.
Definition Equalizer.h:90
T frequency
Center/cutoff frequency in Hz.
Definition Equalizer.h:87