DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Equalizer.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
41#include "Filters.h"
42#include "../Core/AudioSpec.h"
43#include "../Core/AudioBuffer.h"
44#include "../Core/Biquad.h"
45#include "../Core/DenormalGuard.h"
46#include "../Core/DspMath.h"
47#include "../Core/FFT.h"
48#include "../Core/StateBlob.h"
49
50#include <algorithm>
51#include <array>
52#include <atomic>
53#include <cassert>
54#include <cmath>
55#include <cstddef>
56#include <cstdint>
57#include <cstdio>
58#include <memory>
59#include <vector>
60
61namespace dspark {
62
70template <FloatType T, int MaxBands = 16>
72{
73public:
75 enum class FilterMode
76 {
79 };
80
82 ~Equalizer() = default;
83
85 enum class BandType
86 {
87 Peak,
88 LowShelf,
89 HighShelf,
90 LowPass,
91 HighPass,
92 Notch,
93 BandPass,
94 Tilt
95 };
96
101 {
102 T frequency = T(1000);
103 T gain = T(0);
104 T q = T(0.707);
106 int slope = 12;
107 bool enabled = true;
108 };
109
110 static_assert(std::atomic<T>::is_always_lock_free,
111 "audio-thread stores must not lock");
112 static_assert(MaxBands > 0, "an equalizer needs at least one band");
113
114 // -- Lifecycle --------------------------------------------------------------
115
127 void prepare(const AudioSpec& spec)
128 {
129 if (!spec.isValid()) return; // release-safe: keep previous state
130
131 spec_ = spec;
132 for (int i = 0; i < MaxBands; ++i)
133 bands_[i].prepare(spec);
134
135 // Rebuild the linear-phase engine. lpFft_ gates the LP audio path, so
136 // it is torn down first and re-created last (basic guarantee if an
137 // allocation throws mid-way).
138 lpFft_.reset();
139 lpBlock_ = spec.maxBlockSize;
140
141 // The FFT size is 4x the block size; cap it so the pow2 round-up can
142 // never overflow int with an absurd host block size (LP mode is then
143 // unavailable and getLatency() honestly reports 0).
145 {
146 lpBlock_ = 0;
147 lpFftSize_ = 0;
148 return;
149 }
150
151 // Overlap-save requires FFT size N >= L + M - 1
152 // Where L is maxBlockSize, M is impulse response length.
153 // We use M = 2 * L, so N must be >= 3 * L. We round up to next power of 2.
154 int targetFftSize = lpBlock_ * 4;
155 int fftPow2 = 1;
156 while (fftPow2 < targetFftSize) fftPow2 <<= 1;
157 lpFftSize_ = fftPow2;
158
159 // Complex kernel representation (Real, Imaginary interleaved)
160 lpKernel_.assign(static_cast<size_t>(lpFftSize_ + 2), T(0));
161
162 // Overlap-save needs (M-1) samples of history where M = 2*maxBlockSize
163 // is the FIR kernel length. Size the history buffer accordingly.
164 lpPrevBlock_.resize(static_cast<size_t>(spec.numChannels));
165 for (auto& pb : lpPrevBlock_)
166 pb.assign(static_cast<size_t>(lpBlock_ * 2), T(0));
167
168 lpFftIn_.assign(static_cast<size_t>(lpFftSize_), T(0));
169 lpFftOut_.assign(static_cast<size_t>(lpFftSize_ + 2), T(0));
170
171 // Pre-allocate recompute scratch so a live band change never allocates on
172 // the audio thread (recomputeLinearPhaseKernel runs there via processBlock).
173 lpMagScratch_.assign(static_cast<size_t>(lpFftSize_ / 2 + 1), T(1));
174 lpTempFreq_.assign(static_cast<size_t>(lpFftSize_ + 2), T(0));
175 lpImpulse_.assign(static_cast<size_t>(lpFftSize_), T(0));
176 lpKernelSpace_.assign(static_cast<size_t>(lpFftSize_), T(0));
177
178 lpDirty_.store(true, std::memory_order_release);
179 lpFft_ = std::make_unique<FFTReal<T>>(lpFftSize_); // gate opens last
180 }
181
193 void processBlock(AudioBufferView<T> buffer) noexcept
194 {
195 DenormalGuard guard;
196
197 // Front-door non-finite guard: the IIR bands (and the FFT overlap-save
198 // history in LinearPhase mode) latch a NaN/Inf input permanently.
199 // Scrub non-finite input to 0 before either path runs.
200 // No-op on finite input, so conformance metrics stay byte-identical.
201 {
202 const int gN = buffer.getNumSamples();
203 for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
204 {
205 T* d = buffer.getChannel(ch);
206 for (int i = 0; i < gN; ++i)
207 if (!std::isfinite(d[i])) d[i] = T(0);
208 }
209 }
210
211 // Check if config was updated from the UI thread (Lock-free acquire)
212 if (configDirty_.exchange(false, std::memory_order_acquire))
213 {
215 lpDirty_.store(true, std::memory_order_release);
216 }
217
218 FilterMode currentMode = filterMode_.load(std::memory_order_acquire);
219
220 if (currentMode == FilterMode::LinearPhase && lpFft_)
221 {
222 // If kernel needs recalculation, do it once.
223 if (lpDirty_.exchange(false, std::memory_order_acquire))
225
226 processLinearPhase(buffer);
227 return;
228 }
229
230 // Minimum Phase (IIR) Processing
231 const int activeBands = numBands_.load(std::memory_order_relaxed);
232 for (int i = 0; i < activeBands; ++i)
233 {
234 if (bandEnabled_[i].load(std::memory_order_relaxed))
235 bands_[i].processBlock(buffer);
236 }
237 }
238
250 [[nodiscard]] T processSample(T input, int channel) noexcept
251 {
252 // Plain load first: the exchange RMW is only paid when a publication
253 // is actually pending (this runs per sample).
254 if (configDirty_.load(std::memory_order_acquire)
255 && configDirty_.exchange(false, std::memory_order_acquire))
256 {
258 const int n = numBands_.load(std::memory_order_relaxed);
259 for (int i = 0; i < n; ++i)
260 bands_[i].applyParametersNow();
261 lpDirty_.store(true, std::memory_order_release);
262 }
263
264 T sample = input;
265 const int activeBands = numBands_.load(std::memory_order_relaxed);
266
267 for (int i = 0; i < activeBands; ++i)
268 {
269 if (bandEnabled_[i].load(std::memory_order_relaxed))
270 sample = bands_[i].processSample(sample, channel);
271 }
272 return sample;
273 }
274
278 void reset() noexcept
279 {
280 for (int i = 0; i < MaxBands; ++i)
281 bands_[i].reset();
282
283 for (auto& pb : lpPrevBlock_)
284 std::fill(pb.begin(), pb.end(), T(0));
285 }
286
287 // -- API --------------------------------------------------------------------
288
295 void setBand(int index, T frequency, T gainDb)
296 {
297 setBand(index, frequency, gainDb, T(0.707));
298 }
299
307 void setBand(int index, T frequency, T gainDb, T q)
308 {
309 BandConfig cfg;
310 cfg.frequency = frequency;
311 cfg.gain = gainDb;
312 cfg.q = q;
313 cfg.type = BandType::Peak;
314 cfg.slope = 12;
315 cfg.enabled = true;
316 setBand(index, cfg);
317 }
318
329 void setBand(int index, const BandConfig& config)
330 {
331 if (index < 0 || index >= MaxBands) return;
332
333 BandConfig cfg = config;
334 // The fallback reads the control thread's own private master, never the
335 // published words: a setter must not race the audio thread to decide
336 // what "the band's current value" is.
337 const BandConfig& prev = masterConfigs_[index];
338 if (!std::isfinite(cfg.frequency)) cfg.frequency = prev.frequency;
339 if (!std::isfinite(cfg.gain)) cfg.gain = prev.gain;
340 if (!std::isfinite(cfg.q)) cfg.q = prev.q;
341 cfg.type = static_cast<BandType>(std::clamp(static_cast<int>(cfg.type), 0,
342 static_cast<int>(BandType::Tilt)));
343 cfg.slope = std::clamp(cfg.slope, 6, 48);
344
345 masterConfigs_[index] = cfg;
346 staged_[index].publish(cfg);
347 bandEnabled_[index].store(cfg.enabled, std::memory_order_release);
348
349 int currentBands = numBands_.load(std::memory_order_relaxed);
350 if (index >= currentBands)
351 numBands_.store(index + 1, std::memory_order_relaxed);
352
353 // Signal audio thread to update filters
354 configDirty_.store(true, std::memory_order_release);
355 }
356
361 void setNumBands(int count)
362 {
363 int validCount = std::clamp(count, 1, MaxBands);
364 numBands_.store(validCount, std::memory_order_relaxed);
365
366 const T logMin = std::log(T(80));
367 const T logMax = std::log(T(16000));
368
369 for (int i = 0; i < validCount; ++i)
370 {
371 T t = (validCount > 1) ? static_cast<T>(i) / static_cast<T>(validCount - 1) : T(0.5);
372
373 BandConfig cfg;
374 cfg.frequency = std::exp(logMin + t * (logMax - logMin));
375 cfg.gain = T(0);
376 cfg.q = T(0.707);
377 cfg.type = BandType::Peak;
378 cfg.slope = 12;
379 cfg.enabled = true;
380
381 masterConfigs_[i] = cfg;
382 staged_[i].publish(cfg);
383 bandEnabled_[i].store(true, std::memory_order_release);
384 }
385 configDirty_.store(true, std::memory_order_release);
386 }
387
389 [[nodiscard]] int getNumBands() const noexcept
390 {
391 return numBands_.load(std::memory_order_relaxed);
392 }
393
406 void setMatchedBells(bool enabled) noexcept
407 {
408 matchedBells_.store(enabled, std::memory_order_relaxed);
409 configDirty_.store(true, std::memory_order_release);
410 }
411
413 [[nodiscard]] bool isMatchedBells() const noexcept
414 {
415 return matchedBells_.load(std::memory_order_relaxed);
416 }
417
428 [[nodiscard]] BandConfig getBandConfig(int index) const noexcept
429 {
430 if (index < 0 || index >= MaxBands) return {};
431 return staged_[index].read();
432 }
433
439 void setBandEnabled(int index, bool enabled) noexcept
440 {
441 if (index >= 0 && index < MaxBands)
442 {
443 masterConfigs_[index].enabled = enabled;
444 staged_[index].publish(masterConfigs_[index]);
445 bandEnabled_[index].store(enabled, std::memory_order_release);
446 configDirty_.store(true, std::memory_order_release);
447 }
448 }
449
459 void setFilterMode(FilterMode mode) noexcept
460 {
461 const int m = std::clamp(static_cast<int>(mode), 0,
462 static_cast<int>(FilterMode::LinearPhase));
463 filterMode_.store(static_cast<FilterMode>(m), std::memory_order_release);
464 if (static_cast<FilterMode>(m) == FilterMode::LinearPhase)
465 lpDirty_.store(true, std::memory_order_release);
466 }
467
469 [[nodiscard]] FilterMode getFilterMode() const noexcept
470 {
471 return filterMode_.load(std::memory_order_relaxed);
472 }
473
481 [[nodiscard]] int getLatency() const noexcept
482 {
483 return (filterMode_.load(std::memory_order_relaxed) == FilterMode::LinearPhase && lpFft_)
484 ? lpBlock_ : 0;
485 }
486
491 void setSoftMode(bool enabled) noexcept
492 {
493 softMode_.store(enabled, std::memory_order_relaxed);
494 configDirty_.store(true, std::memory_order_release);
495 }
496
498 [[nodiscard]] bool getSoftMode() const noexcept
499 {
500 return softMode_.load(std::memory_order_relaxed);
501 }
502
514 void getMagnitudeForFrequencyArray(const T* frequencies, T* magnitudes, int numPoints) const noexcept
515 {
516 for (int i = 0; i < numPoints; ++i)
517 magnitudes[i] = T(1);
518
519 const int activeBands = numBands_.load(std::memory_order_relaxed);
520 for (int b = 0; b < activeBands; ++b)
521 {
522 const BandConfig cfg = staged_[b].read();
523 if (!cfg.enabled) continue;
524
525 BiquadCoeffs st[5];
526 const int ns = buildBandStages(cfg, st);
527
528 for (int i = 0; i < numPoints; ++i)
529 {
530 double mag = 1.0;
531 for (int s = 0; s < ns; ++s)
532 mag *= st[s].getMagnitude(static_cast<double>(frequencies[i]), spec_.sampleRate);
533 magnitudes[i] = static_cast<T>(static_cast<double>(magnitudes[i]) * mag);
534 }
535 }
536 }
537
549 [[nodiscard]] FilterEngine<T>& getBandFilter(int index) noexcept
550 {
551 assert(index >= 0 && index < MaxBands);
552 return bands_[static_cast<std::size_t>(std::clamp(index, 0, MaxBands - 1))];
553 }
554
556 [[nodiscard]] const FilterEngine<T>& getBandFilter(int index) const noexcept
557 {
558 assert(index >= 0 && index < MaxBands);
559 return bands_[static_cast<std::size_t>(std::clamp(index, 0, MaxBands - 1))];
560 }
561
562
564 [[nodiscard]] std::vector<uint8_t> getState() const
565 {
566 StateWriter w(stateId("PEQZ"), 1);
567 const int n = numBands_.load(std::memory_order_relaxed);
568 w.write("numBands", n);
569 w.write("matchedBells", matchedBells_.load(std::memory_order_relaxed));
570 w.write("filterMode", static_cast<int32_t>(filterMode_.load(std::memory_order_relaxed)));
571 w.write("softMode", softMode_.load(std::memory_order_relaxed));
572 char key[24];
573 for (int i = 0; i < n; ++i)
574 {
575 const BandConfig cfg = getBandConfig(i);
576 std::snprintf(key, sizeof(key), "b%d.freq", i);
577 w.write(key, static_cast<float>(cfg.frequency));
578 std::snprintf(key, sizeof(key), "b%d.gain", i);
579 w.write(key, static_cast<float>(cfg.gain));
580 std::snprintf(key, sizeof(key), "b%d.q", i);
581 w.write(key, static_cast<float>(cfg.q));
582 std::snprintf(key, sizeof(key), "b%d.type", i);
583 w.write(key, static_cast<int32_t>(cfg.type));
584 std::snprintf(key, sizeof(key), "b%d.slope", i);
585 w.write(key, cfg.slope);
586 std::snprintf(key, sizeof(key), "b%d.on", i);
587 w.write(key, cfg.enabled);
588 }
589 return w.blob();
590 }
591
593 bool setState(const uint8_t* data, size_t size)
594 {
595 StateReader r(data, size);
596 if (!r.isValid() || r.processorId() != stateId("PEQZ")) return false;
597 const int n = std::clamp(r.read("numBands", 0), 0, MaxBands);
598 setMatchedBells(r.read("matchedBells", true));
599 // Older blobs carry no mode keys: keep the instance's current modes.
600 setFilterMode(static_cast<FilterMode>(
601 r.read("filterMode", static_cast<int32_t>(filterMode_.load(std::memory_order_relaxed)))));
602 setSoftMode(r.read("softMode", softMode_.load(std::memory_order_relaxed)));
603 char key[24];
604 for (int i = 0; i < n; ++i)
605 {
606 BandConfig cfg;
607 std::snprintf(key, sizeof(key), "b%d.freq", i);
608 cfg.frequency = static_cast<T>(r.read(key, 1000.0f));
609 std::snprintf(key, sizeof(key), "b%d.gain", i);
610 cfg.gain = static_cast<T>(r.read(key, 0.0f));
611 std::snprintf(key, sizeof(key), "b%d.q", i);
612 cfg.q = static_cast<T>(r.read(key, 0.707f));
613 std::snprintf(key, sizeof(key), "b%d.type", i);
614 cfg.type = static_cast<BandType>(r.read(key, 0));
615 std::snprintf(key, sizeof(key), "b%d.slope", i);
616 cfg.slope = r.read(key, 12);
617 std::snprintf(key, sizeof(key), "b%d.on", i);
618 cfg.enabled = r.read(key, true);
619 setBand(i, cfg);
620 }
621 numBands_.store(n, std::memory_order_relaxed);
622 configDirty_.store(true, std::memory_order_release);
623 return true;
624 }
625
626protected:
627
631 [[nodiscard]] T effectiveQ(const BandConfig& cfg) const noexcept
632 {
633 T q = cfg.q;
634 if (softMode_.load(std::memory_order_relaxed))
635 {
636 const T absGain = std::abs(cfg.gain);
637 const T maxQ = T(1) + T(8) / (absGain + T(1));
638 q = std::min(q, maxQ);
639 }
640 return q;
641 }
642
659 void updateActiveFilters() noexcept
660 {
661 const bool matched = matchedBells_.load(std::memory_order_relaxed);
662 int activeBands = numBands_.load(std::memory_order_relaxed);
663
664 for (int i = 0; i < activeBands; ++i)
665 {
666 auto& filter = bands_[i];
667 BandConfig cfg;
668 if (!staged_[i].tryRead(cfg))
669 {
670 // Bounded read gave up: keep this band's current coefficients
671 // and pick the publication up on a later call.
672 configDirty_.store(true, std::memory_order_release);
673 continue;
674 }
675
676 float freq = static_cast<float>(cfg.frequency);
677 float gain = static_cast<float>(cfg.gain);
678 float q = static_cast<float>(effectiveQ(cfg));
679
680 filter.setMatchedPeak(matched);
681 switch (cfg.type)
682 {
683 case BandType::Peak: filter.setPeaking(freq, gain, q); break;
684 // Shelves take a SLOPE (0..1], not a Q: convert with the RBJ
685 // S<->Q relation so the user's Q behaves consistently here and
686 // in the linear-phase kernel (which uses the same conversion).
687 case BandType::LowShelf: filter.setLowShelf(freq, gain,
688 static_cast<float>(shelfSlopeFromQ(q, gain))); break;
689 case BandType::HighShelf: filter.setHighShelf(freq, gain,
690 static_cast<float>(shelfSlopeFromQ(q, gain))); break;
691 case BandType::LowPass: filter.setLowPass(freq, q, cfg.slope); break;
692 case BandType::HighPass: filter.setHighPass(freq, q, cfg.slope); break;
693 case BandType::Notch: filter.setNotch(freq, q); break;
694 case BandType::BandPass: filter.setBandPass(freq, q); break;
695 case BandType::Tilt: filter.setTilt(freq, gain); break;
696 }
697 }
698 }
699
707 [[nodiscard]] static double shelfSlopeFromQ(double q, double gainDb) noexcept
708 {
709 q = std::max(q, 0.05);
710 const double A = std::pow(10.0, std::abs(gainDb) / 40.0);
711 const double denom = A + 1.0 / A;
712 const double invS = (1.0 / (q * q) - 2.0) / denom + 1.0;
713 if (invS <= 1.0) return 1.0; // steeper-than-standard requests clamp to S = 1
714 return std::clamp(1.0 / invS, 0.0001, 1.0);
715 }
716
722 [[nodiscard]] BiquadCoeffs computeBandCoeffs(const BandConfig& cfg) const noexcept
723 {
724 double sr = spec_.sampleRate;
725 double f = static_cast<double>(cfg.frequency);
726 double g = static_cast<double>(cfg.gain);
727 double q = static_cast<double>(cfg.q);
728
729 switch (cfg.type)
730 {
731 case BandType::Peak:
732 return matchedBells_.load(std::memory_order_relaxed)
733 ? BiquadCoeffs::makePeakMatched(sr, f, q, g)
734 : BiquadCoeffs::makePeak(sr, f, q, g);
737 case BandType::LowPass: return BiquadCoeffs::makeLowPass(sr, f, q);
738 case BandType::HighPass: return BiquadCoeffs::makeHighPass(sr, f, q);
739 case BandType::Notch: return BiquadCoeffs::makeNotch(sr, f, q);
740 case BandType::BandPass: return BiquadCoeffs::makeBandPass(sr, f, q);
741 case BandType::Tilt: return BiquadCoeffs::makeTilt(sr, f, g);
742 }
743 return {};
744 }
745
746public:
758 [[nodiscard]] int buildBandStages(const BandConfig& cfg, BiquadCoeffs* stages) const noexcept
759 {
760 const double sr = spec_.sampleRate;
761 // Mirror the FilterEngine's own clamps so the analysis matches the audio.
762 const double f = std::clamp(static_cast<double>(cfg.frequency), 10.0, sr * 0.499);
763 const T q = std::max(effectiveQ(cfg), T(0.1));
764
765 if (cfg.type == BandType::LowPass || cfg.type == BandType::HighPass)
766 {
767 const bool lp = (cfg.type == BandType::LowPass);
768 // The user Q scales the final cascade stage exactly like the engine.
769 auto casc = FilterEngine<T>::cascadeForSlope(cfg.slope, static_cast<float>(q));
770 int n = 0;
771 if (casc.hasFirstOrder)
772 stages[n++] = lp ? BiquadCoeffs::makeFirstOrderLowPass(sr, f)
774 for (int s = 0; s < casc.numSecondOrder; ++s)
775 {
776 const double stageQ = static_cast<double>(casc.qValues[s]);
777 stages[n++] = lp ? BiquadCoeffs::makeLowPass(sr, f, stageQ)
778 : BiquadCoeffs::makeHighPass(sr, f, stageQ);
779 }
780 return n;
781 }
782
783 BandConfig eff = cfg;
784 eff.frequency = static_cast<T>(f);
785 eff.q = q;
786 stages[0] = computeBandCoeffs(eff);
787 return 1;
788 }
789
790protected:
800 {
801 if (lpFftSize_ == 0) return;
802
803 const int numBins = lpFftSize_ / 2 + 1;
804 T* const mag = lpMagScratch_.data(); // pre-allocated scratch (no audio-thread alloc)
805 std::fill_n(mag, numBins, T(1));
806 const int activeBands = numBands_.load(std::memory_order_relaxed);
807
808 // 1. Accumulate the TRUE per-stage cascade magnitude of all active bands
809 // (matches what the IIR FilterEngine applies in MinimumPhase mode, so
810 // switching modes keeps the same magnitude response).
811 const double sr = spec_.sampleRate;
812 for (int b = 0; b < activeBands; ++b)
813 {
814 BandConfig cfg;
815 if (!staged_[b].tryRead(cfg))
816 {
817 // Bounded read gave up. Abandon the whole recompute and re-arm:
818 // this bail sits BEFORE any store into lpKernel_, so the kernel
819 // currently convolving stays intact and the next call rebuilds
820 // it from a complete band set. The scratch buffers may be left
821 // partially written; nothing reads them across calls.
822 lpDirty_.store(true, std::memory_order_release);
823 return;
824 }
825 if (!cfg.enabled) continue;
826
827 BiquadCoeffs st[5];
828 const int ns = buildBandStages(cfg, st);
829
830 for (int k = 0; k < numBins; ++k)
831 {
832 const double freq = sr * static_cast<double>(k) / static_cast<double>(lpFftSize_);
833 double m = 1.0;
834 for (int s = 0; s < ns; ++s)
835 m *= st[s].getMagnitude(freq, sr);
836 mag[k] = static_cast<T>(static_cast<double>(mag[k]) * m);
837 }
838 }
839
840 // 2. Prepare Zero-Phase Frequency buffer (Real = Mag, Imag = 0)
841 std::fill(lpTempFreq_.begin(), lpTempFreq_.end(), T(0));
842 for (int k = 0; k < numBins; ++k)
843 lpTempFreq_[2 * k] = mag[k];
844
845 // 3. IFFT to get temporal impulse response (wrapped around t=0)
846 lpFft_->inverse(lpTempFreq_.data(), lpImpulse_.data());
847
848 // 4. Shift, Windowing and Zero-pad for Overlap-Save
849 const int M = lpBlock_ * 2; // Desired Kernel length
850 const int halfM = M / 2;
851 std::fill(lpKernelSpace_.begin(), lpKernelSpace_.end(), T(0)); // zero-pad scratch
852
853 for (int i = 0; i < M; ++i)
854 {
855 // Circular read from center 0
856 int readIdx = (i - halfM + lpFftSize_) % lpFftSize_;
857
858 // Blackman-Harris window formulation
859 double t = static_cast<double>(i) / static_cast<double>(M - 1);
860 double window = 0.35875
861 - 0.48829 * std::cos(2.0 * pi<double> * t)
862 + 0.14128 * std::cos(4.0 * pi<double> * t)
863 - 0.01168 * std::cos(6.0 * pi<double> * t);
864
865 // No extra scaling: FFTReal::inverse already applies the full 1/N
866 // normalisation (verified by exact round-trip). The previous extra
867 // division by lpFftSize_ attenuated the whole linear-phase path by
868 // 20*log10(N) dB - about -60 dB with the default sizes.
869 lpKernelSpace_[i] = lpImpulse_[readIdx] * static_cast<T>(window);
870 }
871
872 // 5. Transform finalized zero-padded causal kernel to frequency domain
873 lpFft_->forward(lpKernelSpace_.data(), lpKernel_.data());
874 }
875
885 {
886 // Safety bound check: Prevent out-of-bounds if host pushes dynamic channel
887 // counts. Channels beyond the prepared count pass through untouched.
888 const int nCh = std::min(buffer.getNumChannels(), static_cast<int>(lpPrevBlock_.size()));
889 const int L = buffer.getNumSamples();
890 const int N = lpFftSize_;
891 const int M = lpBlock_ * 2; // Kernel length
892
893 // Safety check: block size cannot exceed pre-allocated maxBlockSize
894 if (L > lpBlock_) return;
895
896 const int overlapSize = M - 1; // FIR history overlap-save needs (kernel len - 1)
897
898 for (int ch = 0; ch < nCh; ++ch)
899 {
900 T* channelData = buffer.getChannel(ch);
901 auto& prev = lpPrevBlock_[ch];
902
903 // 1. Build overlap-save input: [history (overlapSize) | current (L) | zeros].
904 // The full (M-1)-sample history is required for a length-M FIR;
905 // the previous code only kept L samples, corrupting the output
906 // (especially for blocks smaller than maxBlockSize).
907 for (int i = 0; i < overlapSize; ++i)
908 lpFftIn_[i] = prev[i];
909 for (int i = 0; i < L; ++i)
910 lpFftIn_[overlapSize + i] = channelData[i];
911 for (int i = overlapSize + L; i < N; ++i)
912 lpFftIn_[i] = T(0);
913
914 // 2. Save the LAST overlapSize samples of [history | current] as the next
915 // block's history (read from lpFftIn_ before the inverse FFT reuses it).
916 for (int i = 0; i < overlapSize; ++i)
917 prev[i] = lpFftIn_[L + i];
918
919 // 3. Forward FFT
920 lpFft_->forward(lpFftIn_.data(), lpFftOut_.data());
921
922 // 4. Complex multiplication: H(k) * X(k)
923 int numBins = N / 2 + 1;
924 for (int k = 0; k < numBins; ++k)
925 {
926 T realX = lpFftOut_[2 * k];
927 T imagX = lpFftOut_[2 * k + 1];
928 T realH = lpKernel_[2 * k];
929 T imagH = lpKernel_[2 * k + 1];
930
931 lpFftOut_[2 * k] = realX * realH - imagX * imagH;
932 lpFftOut_[2 * k + 1] = realX * imagH + imagX * realH;
933 }
934
935 // 5. Inverse FFT
936 lpFft_->inverse(lpFftOut_.data(), lpFftIn_.data()); // Reusing lpFftIn_ to save memory
937
938 // 6. Overlap-save output extraction: valid data starts at index M - 1
939 int offset = M - 1;
940 for (int i = 0; i < L; ++i)
941 {
942 // Assign filtered output back to the channel
943 channelData[i] = lpFftIn_[offset + i];
944 }
945 }
946 }
947
948 // The pow2 round-up computes lpBlock_ * 4: cap the block size so that can
949 // never overflow int (hosts never get close; LP mode disables above it).
950 static constexpr int kLpMaxBlockSize = 1 << 18;
951
953 std::atomic<int> numBands_ { 0 };
954
955 std::array<FilterEngine<T>, MaxBands> bands_ {};
956
968 {
969 // Initialised FROM BandConfig's own defaults, never from a second copy
970 // of the literals: a staged band that starts life disagreeing with the
971 // BandConfig a caller reads back is a bug no test would obviously
972 // catch, and two hand-written default sets drift one field at a time.
973 std::atomic<T> frequency { BandConfig{}.frequency };
974 std::atomic<T> gain { BandConfig{}.gain };
975 std::atomic<T> q { BandConfig{}.q };
976 std::atomic<int> type { static_cast<int>(BandConfig{}.type) };
977 std::atomic<int> slope { BandConfig{}.slope };
978 std::atomic<bool> enabled { BandConfig{}.enabled };
979 std::atomic<unsigned> seq { 0 };
980
993 void publish(const BandConfig& c) noexcept
994 {
995 seq.fetch_add(1, std::memory_order_acq_rel); // -> odd
996 std::atomic_thread_fence(std::memory_order_release);
997 frequency.store(c.frequency, std::memory_order_relaxed);
998 gain.store(c.gain, std::memory_order_relaxed);
999 q.store(c.q, std::memory_order_relaxed);
1000 type.store(static_cast<int>(c.type), std::memory_order_relaxed);
1001 slope.store(c.slope, std::memory_order_relaxed);
1002 enabled.store(c.enabled, std::memory_order_relaxed);
1003 seq.fetch_add(1, std::memory_order_release); // -> even
1004 }
1005
1011 static constexpr int kSeqlockMaxAttempts = 3;
1012
1019 void loadWordsRelaxed(BandConfig& c) const noexcept
1020 {
1021 c.frequency = frequency.load(std::memory_order_relaxed);
1022 c.gain = gain.load(std::memory_order_relaxed);
1023 c.q = q.load(std::memory_order_relaxed);
1024 c.type = static_cast<BandType>(type.load(std::memory_order_relaxed));
1025 c.slope = slope.load(std::memory_order_relaxed);
1026 c.enabled = enabled.load(std::memory_order_relaxed);
1027 }
1028
1049 [[nodiscard]] bool tryRead(BandConfig& out) const noexcept
1050 {
1051 for (int attempt = 0; attempt < kSeqlockMaxAttempts; ++attempt)
1052 {
1053 const unsigned s0 = seq.load(std::memory_order_acquire);
1054 if ((s0 & 1u) != 0u) continue; // writer mid-publish: do not copy
1055 BandConfig c;
1057 std::atomic_thread_fence(std::memory_order_acquire);
1058 if (s0 == seq.load(std::memory_order_relaxed))
1059 {
1060 out = c; // commit only on validation
1061 return true;
1062 }
1063 }
1064 return false;
1065 }
1066
1083 [[nodiscard]] BandConfig read() const noexcept
1084 {
1085 BandConfig c;
1086 unsigned s0, s1;
1087 do {
1088 s0 = seq.load(std::memory_order_acquire);
1090 std::atomic_thread_fence(std::memory_order_acquire);
1091 s1 = seq.load(std::memory_order_relaxed);
1092 } while ((s0 & 1u) != 0u || s0 != s1);
1093 return c;
1094 }
1095 };
1096
1098 std::array<StagedBand, MaxBands> staged_ {};
1099
1104 std::array<BandConfig, MaxBands> masterConfigs_ {};
1105
1106 // Per-band enable flag read lock-free every block. The full BandConfig is only
1107 // read under the configDirty_ acquire gate; `enabled` is toggled often and read
1108 // on the hot path, so it gets its own atomic to avoid a torn/unsynchronized read.
1109 std::array<std::atomic<bool>, MaxBands> bandEnabled_ {};
1110
1111 std::atomic<bool> softMode_ { false };
1112 std::atomic<bool> configDirty_ { false };
1113 std::atomic<bool> matchedBells_ { true };
1114
1115 // Linear-phase state
1116 std::atomic<FilterMode> filterMode_ { FilterMode::MinimumPhase };
1117 std::atomic<bool> lpDirty_ { true };
1118
1119 std::unique_ptr<FFTReal<T>> lpFft_;
1120 int lpFftSize_ = 0;
1121 int lpBlock_ = 0;
1122
1123 std::vector<T> lpKernel_;
1124 std::vector<std::vector<T>> lpPrevBlock_;
1125 std::vector<T> lpFftIn_, lpFftOut_;
1126 // Pre-allocated recompute scratch (no audio-thread allocation on band changes).
1128};
1129
1130} // namespace dspark
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:72
std::vector< T > lpTempFreq_
Definition Equalizer.h:1127
bool setState(const uint8_t *data, size_t size)
Restores bands and modes from a blob (tolerant; rejects foreign ids).
Definition Equalizer.h:593
BandType
Filter type for each EQ band.
Definition Equalizer.h:86
@ 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:884
void setBand(int index, const BandConfig &config)
Configures a band with full control over all parameters. Thread-safe.
Definition Equalizer.h:329
bool getSoftMode() const noexcept
Returns whether soft mode is enabled.
Definition Equalizer.h:498
std::vector< T > lpFftOut_
Definition Equalizer.h:1125
std::vector< T > lpKernel_
Definition Equalizer.h:1123
std::atomic< bool > configDirty_
Definition Equalizer.h:1112
bool isMatchedBells() const noexcept
Returns whether Peak bands use the matched design (setMatchedBells()).
Definition Equalizer.h:413
T processSample(T input, int channel) noexcept
Processes a single sample through all enabled bands (IIR mode only).
Definition Equalizer.h:250
std::vector< uint8_t > getState() const
Serializes bands and modes (setup/UI threads; allocates).
Definition Equalizer.h:564
void setBand(int index, T frequency, T gainDb)
Configures a band with frequency and gain (Peak filter).
Definition Equalizer.h:295
std::atomic< bool > lpDirty_
Definition Equalizer.h:1117
void reset() noexcept
Resets all filter states to zero to prevent ringing on playback start.
Definition Equalizer.h:278
std::atomic< bool > matchedBells_
Matched (de-cramped) bells by default.
Definition Equalizer.h:1113
T effectiveQ(const BandConfig &cfg) const noexcept
The band Q the audio path actually uses (soft mode caps it by gain).
Definition Equalizer.h:631
std::array< StagedBand, MaxBands > staged_
Published band configurations. The ONLY band storage both threads reach.
Definition Equalizer.h:1098
void prepare(const AudioSpec &spec)
Prepares all bands and allocates necessary resources for processing.
Definition Equalizer.h:127
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:758
static constexpr int kLpMaxBlockSize
Definition Equalizer.h:950
std::vector< std::vector< T > > lpPrevBlock_
Definition Equalizer.h:1124
AudioSpec spec_
Definition Equalizer.h:952
FilterMode getFilterMode() const noexcept
Returns the current filter mode.
Definition Equalizer.h:469
std::array< BandConfig, MaxBands > masterConfigs_
Definition Equalizer.h:1104
std::atomic< int > numBands_
Definition Equalizer.h:953
std::vector< T > lpKernelSpace_
Definition Equalizer.h:1127
int getNumBands() const noexcept
Returns the number of active bands.
Definition Equalizer.h:389
FilterEngine< T > & getBandFilter(int index) noexcept
Direct access to a band's underlying FilterEngine.
Definition Equalizer.h:549
void setMatchedBells(bool enabled) noexcept
Switches Peak bands to the analog-matched (de-cramped) design.
Definition Equalizer.h:406
void setNumBands(int count)
Sets the number of active bands with auto-logarithmic spacing.
Definition Equalizer.h:361
std::unique_ptr< FFTReal< T > > lpFft_
Definition Equalizer.h:1119
void setBandEnabled(int index, bool enabled) noexcept
Enables or disables a band without changing its parameters.
Definition Equalizer.h:439
std::vector< T > lpMagScratch_
Definition Equalizer.h:1127
std::vector< T > lpImpulse_
Definition Equalizer.h:1127
void recomputeLinearPhaseKernel() noexcept
Mathematically robust Linear Phase kernel computation.
Definition Equalizer.h:799
FilterMode
Filter processing mode.
Definition Equalizer.h:76
@ 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:1125
void setFilterMode(FilterMode mode) noexcept
Sets the filter processing mode (Minimum Phase or Linear Phase).
Definition Equalizer.h:459
~Equalizer()=default
Non-virtual destructor to prevent vtable instantiation (zero virtual dispatch).
std::atomic< bool > softMode_
Definition Equalizer.h:1111
void setBand(int index, T frequency, T gainDb, T q)
Configures a band with frequency, gain, and Q.
Definition Equalizer.h:307
BandConfig getBandConfig(int index) const noexcept
Returns the current configuration of a band.
Definition Equalizer.h:428
std::array< std::atomic< bool >, MaxBands > bandEnabled_
Definition Equalizer.h:1109
static double shelfSlopeFromQ(double q, double gainDb) noexcept
Converts a user-facing shelf Q into the RBJ shelf slope S.
Definition Equalizer.h:707
void updateActiveFilters() noexcept
Translates BandConfigs into internal FilterEngine parameters safely.
Definition Equalizer.h:659
int getLatency() const noexcept
Returns the latency in samples.
Definition Equalizer.h:481
int lpBlock_
Max block size the LP engine was sized for (= its latency).
Definition Equalizer.h:1121
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place.
Definition Equalizer.h:193
std::atomic< FilterMode > filterMode_
Definition Equalizer.h:1116
void setSoftMode(bool enabled) noexcept
Enables soft mode (anti-ringing Q reduction dynamically based on gain).
Definition Equalizer.h:491
BiquadCoeffs computeBandCoeffs(const BandConfig &cfg) const noexcept
Computes single-biquad coefficients for a band (analysis/kernel).
Definition Equalizer.h:722
void getMagnitudeForFrequencyArray(const T *frequencies, T *magnitudes, int numPoints) const noexcept
Computes the combined magnitude response of all enabled bands.
Definition Equalizer.h:514
std::array< FilterEngine< T >, MaxBands > bands_
Definition Equalizer.h:955
const FilterEngine< T > & getBandFilter(int index) const noexcept
Const overload. Same clamping contract.
Definition Equalizer.h:556
Professional multi-mode filter with cascaded biquad stages.
Definition Filters.h:73
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:325
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:161
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:204
bool isValid() const noexcept
Definition StateBlob.h:199
uint32_t processorId() const noexcept
Definition StateBlob.h:200
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:53
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:105
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:71
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:651
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:53
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
Stores normalised biquad coefficients (b0, b1, b2, a1, a2), always double.
Definition Biquad.h:106
static BiquadCoeffs makeFirstOrderHighPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) high-pass filter.
Definition Biquad.h:449
static BiquadCoeffs makePeakMatched(double sampleRate, double freq, double Q, double gainDb) noexcept
Analog-matched ("de-cramped") peaking filter (Vicanek design).
Definition Biquad.h:247
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
Definition Biquad.h:142
static BiquadCoeffs makeBandPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Band-pass filter (constant 0 dB peak gain).
Definition Biquad.h:171
static BiquadCoeffs makePeak(double sampleRate, double freq, double Q, double gainDb) noexcept
Peak (parametric EQ) filter.
Definition Biquad.h:200
static BiquadCoeffs makeFirstOrderLowPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) low-pass filter.
Definition Biquad.h:426
static BiquadCoeffs makeTilt(double sampleRate, double pivotFreq, double gainDb) noexcept
Creates a first-order tilt filter.
Definition Biquad.h:475
static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Low-pass filter.
Definition Biquad.h:118
static BiquadCoeffs makeLowShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
Low-shelf filter.
Definition Biquad.h:314
static BiquadCoeffs makeHighShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
High-shelf filter.
Definition Biquad.h:344
static BiquadCoeffs makeNotch(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Notch (band-reject) filter.
Definition Biquad.h:373
Full configuration for a single EQ band.
Definition Equalizer.h:101
T q
Q factor (0.1 = wide, 10 = narrow).
Definition Equalizer.h:104
T gain
Gain in dB (Peak, Shelf, Tilt).
Definition Equalizer.h:103
bool enabled
False to bypass this band.
Definition Equalizer.h:107
int slope
Slope in dB/oct (LP/HP only: 6-48).
Definition Equalizer.h:106
BandType type
Filter type for this band.
Definition Equalizer.h:105
T frequency
Center/cutoff frequency in Hz.
Definition Equalizer.h:102
One band's published configuration: a seqlock over atomic words.
Definition Equalizer.h:968
std::atomic< T > frequency
Definition Equalizer.h:973
BandConfig read() const noexcept
Reads the band into a caller-private plain copy (readouts).
Definition Equalizer.h:1083
std::atomic< int > slope
Definition Equalizer.h:977
void loadWordsRelaxed(BandConfig &c) const noexcept
Relaxed copy of the six published words (no ordering of its own; the caller's fences and counter chec...
Definition Equalizer.h:1019
static constexpr int kSeqlockMaxAttempts
Definition Equalizer.h:1011
std::atomic< unsigned > seq
Definition Equalizer.h:979
void publish(const BandConfig &c) noexcept
Publishes a whole band configuration (control thread only).
Definition Equalizer.h:993
std::atomic< int > type
Definition Equalizer.h:976
std::atomic< bool > enabled
Definition Equalizer.h:978
bool tryRead(BandConfig &out) const noexcept
The bounded seqlock read for the AUDIO thread.
Definition Equalizer.h:1049