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"
70template <FloatType T,
int MaxBands = 16>
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");
132 for (
int i = 0; i < MaxBands; ++i)
156 while (fftPow2 < targetFftSize) fftPow2 <<= 1;
166 pb.assign(
static_cast<size_t>(
lpBlock_ * 2), T(0));
178 lpDirty_.store(
true, std::memory_order_release);
202 const int gN = buffer.getNumSamples();
203 for (
int ch = 0; ch < buffer.getNumChannels(); ++ch)
205 T* d = buffer.getChannel(ch);
206 for (
int i = 0; i < gN; ++i)
207 if (!std::isfinite(d[i])) d[i] = T(0);
212 if (
configDirty_.exchange(
false, std::memory_order_acquire))
215 lpDirty_.store(
true, std::memory_order_release);
223 if (
lpDirty_.exchange(
false, std::memory_order_acquire))
231 const int activeBands =
numBands_.load(std::memory_order_relaxed);
232 for (
int i = 0; i < activeBands; ++i)
235 bands_[i].processBlock(buffer);
255 &&
configDirty_.exchange(
false, std::memory_order_acquire))
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);
265 const int activeBands =
numBands_.load(std::memory_order_relaxed);
267 for (
int i = 0; i < activeBands; ++i)
270 sample =
bands_[i].processSample(sample, channel);
280 for (
int i = 0; i < MaxBands; ++i)
284 std::fill(pb.begin(), pb.end(), T(0));
295 void setBand(
int index, T frequency, T gainDb)
297 setBand(index, frequency, gainDb, T(0.707));
307 void setBand(
int index, T frequency, T gainDb, T q)
331 if (index < 0 || index >= MaxBands)
return;
340 if (!std::isfinite(cfg.
q)) cfg.
q = prev.
q;
349 int currentBands =
numBands_.load(std::memory_order_relaxed);
350 if (index >= currentBands)
351 numBands_.store(index + 1, std::memory_order_relaxed);
363 int validCount = std::clamp(count, 1, MaxBands);
364 numBands_.store(validCount, std::memory_order_relaxed);
366 const T logMin = std::log(T(80));
367 const T logMax = std::log(T(16000));
369 for (
int i = 0; i < validCount; ++i)
371 T t = (validCount > 1) ?
static_cast<T
>(i) /
static_cast<T
>(validCount - 1) : T(0.5);
374 cfg.
frequency = std::exp(logMin + t * (logMax - logMin));
391 return numBands_.load(std::memory_order_relaxed);
430 if (index < 0 || index >= MaxBands)
return {};
441 if (index >= 0 && index < MaxBands)
445 bandEnabled_[index].store(enabled, std::memory_order_release);
461 const int m = std::clamp(
static_cast<int>(mode), 0,
465 lpDirty_.store(
true, std::memory_order_release);
471 return filterMode_.load(std::memory_order_relaxed);
493 softMode_.store(enabled, std::memory_order_relaxed);
500 return softMode_.load(std::memory_order_relaxed);
516 for (
int i = 0; i < numPoints; ++i)
517 magnitudes[i] = T(1);
519 const int activeBands =
numBands_.load(std::memory_order_relaxed);
520 for (
int b = 0; b < activeBands; ++b)
528 for (
int i = 0; i < numPoints; ++i)
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);
551 assert(index >= 0 && index < MaxBands);
552 return bands_[
static_cast<std::size_t
>(std::clamp(index, 0, MaxBands - 1))];
558 assert(index >= 0 && index < MaxBands);
559 return bands_[
static_cast<std::size_t
>(std::clamp(index, 0, MaxBands - 1))];
564 [[nodiscard]] std::vector<uint8_t>
getState()
const
567 const int n =
numBands_.load(std::memory_order_relaxed);
568 w.
write(
"numBands", n);
570 w.
write(
"filterMode",
static_cast<int32_t
>(
filterMode_.load(std::memory_order_relaxed)));
573 for (
int i = 0; i < n; ++i)
576 std::snprintf(key,
sizeof(key),
"b%d.freq", i);
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);
586 std::snprintf(key,
sizeof(key),
"b%d.on", i);
597 const int n = std::clamp(r.
read(
"numBands", 0), 0, MaxBands);
601 r.
read(
"filterMode",
static_cast<int32_t
>(
filterMode_.load(std::memory_order_relaxed)))));
604 for (
int i = 0; i < n; ++i)
607 std::snprintf(key,
sizeof(key),
"b%d.freq", i);
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);
615 std::snprintf(key,
sizeof(key),
"b%d.slope", i);
617 std::snprintf(key,
sizeof(key),
"b%d.on", i);
621 numBands_.store(n, std::memory_order_relaxed);
634 if (
softMode_.load(std::memory_order_relaxed))
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);
661 const bool matched =
matchedBells_.load(std::memory_order_relaxed);
662 int activeBands =
numBands_.load(std::memory_order_relaxed);
664 for (
int i = 0; i < activeBands; ++i)
676 float freq =
static_cast<float>(cfg.
frequency);
677 float gain =
static_cast<float>(cfg.
gain);
678 float q =
static_cast<float>(
effectiveQ(cfg));
680 filter.setMatchedPeak(matched);
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;
714 return std::clamp(1.0 / invS, 0.0001, 1.0);
725 double f =
static_cast<double>(cfg.frequency);
726 double g =
static_cast<double>(cfg.gain);
727 double q =
static_cast<double>(cfg.q);
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));
771 if (casc.hasFirstOrder)
774 for (
int s = 0; s < casc.numSecondOrder; ++s)
776 const double stageQ =
static_cast<double>(casc.qValues[s]);
805 std::fill_n(mag, numBins, T(1));
806 const int activeBands =
numBands_.load(std::memory_order_relaxed);
812 for (
int b = 0; b < activeBands; ++b)
822 lpDirty_.store(
true, std::memory_order_release);
830 for (
int k = 0; k < numBins; ++k)
832 const double freq = sr *
static_cast<double>(k) /
static_cast<double>(
lpFftSize_);
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);
842 for (
int k = 0; k < numBins; ++k)
850 const int halfM = M / 2;
853 for (
int i = 0; i < M; ++i)
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);
888 const int nCh = std::min(buffer.getNumChannels(),
static_cast<int>(
lpPrevBlock_.size()));
889 const int L = buffer.getNumSamples();
896 const int overlapSize = M - 1;
898 for (
int ch = 0; ch < nCh; ++ch)
900 T* channelData = buffer.getChannel(ch);
907 for (
int i = 0; i < overlapSize; ++i)
909 for (
int i = 0; i < L; ++i)
910 lpFftIn_[overlapSize + i] = channelData[i];
911 for (
int i = overlapSize + L; i < N; ++i)
916 for (
int i = 0; i < overlapSize; ++i)
923 int numBins = N / 2 + 1;
924 for (
int k = 0; k < numBins; ++k)
931 lpFftOut_[2 * k] = realX * realH - imagX * imagH;
932 lpFftOut_[2 * k + 1] = realX * imagH + imagX * realH;
940 for (
int i = 0; i < L; ++i)
943 channelData[i] =
lpFftIn_[offset + i];
955 std::array<FilterEngine<T>, MaxBands>
bands_ {};
979 std::atomic<unsigned>
seq { 0 };
995 seq.fetch_add(1, std::memory_order_acq_rel);
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);
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);
1053 const unsigned s0 =
seq.load(std::memory_order_acquire);
1054 if ((s0 & 1u) != 0u)
continue;
1057 std::atomic_thread_fence(std::memory_order_acquire);
1058 if (s0 ==
seq.load(std::memory_order_relaxed))
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);
Non-owning view over audio channel data.
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Parametric multi-band EQ using cascaded biquads or FFT overlap-save convolution.
std::vector< T > lpTempFreq_
bool setState(const uint8_t *data, size_t size)
Restores bands and modes from a blob (tolerant; rejects foreign ids).
BandType
Filter type for each EQ band.
@ 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.
void setBand(int index, const BandConfig &config)
Configures a band with full control over all parameters. Thread-safe.
bool getSoftMode() const noexcept
Returns whether soft mode is enabled.
std::vector< T > lpFftOut_
std::vector< T > lpKernel_
std::atomic< bool > configDirty_
bool isMatchedBells() const noexcept
Returns whether Peak bands use the matched design (setMatchedBells()).
T processSample(T input, int channel) noexcept
Processes a single sample through all enabled bands (IIR mode only).
std::vector< uint8_t > getState() const
Serializes bands and modes (setup/UI threads; allocates).
void setBand(int index, T frequency, T gainDb)
Configures a band with frequency and gain (Peak filter).
std::atomic< bool > lpDirty_
void reset() noexcept
Resets all filter states to zero to prevent ringing on playback start.
std::atomic< bool > matchedBells_
Matched (de-cramped) bells by default.
T effectiveQ(const BandConfig &cfg) const noexcept
The band Q the audio path actually uses (soft mode caps it by gain).
std::array< StagedBand, MaxBands > staged_
Published band configurations. The ONLY band storage both threads reach.
void prepare(const AudioSpec &spec)
Prepares all bands and allocates necessary resources for processing.
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...
static constexpr int kLpMaxBlockSize
std::vector< std::vector< T > > lpPrevBlock_
FilterMode getFilterMode() const noexcept
Returns the current filter mode.
std::array< BandConfig, MaxBands > masterConfigs_
std::atomic< int > numBands_
std::vector< T > lpKernelSpace_
int getNumBands() const noexcept
Returns the number of active bands.
FilterEngine< T > & getBandFilter(int index) noexcept
Direct access to a band's underlying FilterEngine.
void setMatchedBells(bool enabled) noexcept
Switches Peak bands to the analog-matched (de-cramped) design.
void setNumBands(int count)
Sets the number of active bands with auto-logarithmic spacing.
std::unique_ptr< FFTReal< T > > lpFft_
void setBandEnabled(int index, bool enabled) noexcept
Enables or disables a band without changing its parameters.
std::vector< T > lpMagScratch_
std::vector< T > lpImpulse_
void recomputeLinearPhaseKernel() noexcept
Mathematically robust Linear Phase kernel computation.
FilterMode
Filter processing mode.
@ LinearPhase
FFT-based overlap-save (block-size latency, zero phase distortion).
@ MinimumPhase
IIR biquads (zero latency, minimum phase shift). Default.
std::vector< T > lpFftIn_
void setFilterMode(FilterMode mode) noexcept
Sets the filter processing mode (Minimum Phase or Linear Phase).
~Equalizer()=default
Non-virtual destructor to prevent vtable instantiation (zero virtual dispatch).
std::atomic< bool > softMode_
void setBand(int index, T frequency, T gainDb, T q)
Configures a band with frequency, gain, and Q.
BandConfig getBandConfig(int index) const noexcept
Returns the current configuration of a band.
std::array< std::atomic< bool >, MaxBands > bandEnabled_
static double shelfSlopeFromQ(double q, double gainDb) noexcept
Converts a user-facing shelf Q into the RBJ shelf slope S.
void updateActiveFilters() noexcept
Translates BandConfigs into internal FilterEngine parameters safely.
int getLatency() const noexcept
Returns the latency in samples.
int lpBlock_
Max block size the LP engine was sized for (= its latency).
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place.
std::atomic< FilterMode > filterMode_
void setSoftMode(bool enabled) noexcept
Enables soft mode (anti-ringing Q reduction dynamically based on gain).
BiquadCoeffs computeBandCoeffs(const BandConfig &cfg) const noexcept
Computes single-biquad coefficients for a band (analysis/kernel).
void getMagnitudeForFrequencyArray(const T *frequencies, T *magnitudes, int numPoints) const noexcept
Computes the combined magnitude response of all enabled bands.
std::array< FilterEngine< T >, MaxBands > bands_
const FilterEngine< T > & getBandFilter(int index) const noexcept
Const overload. Same clamping contract.
Professional multi-mode filter with cascaded biquad stages.
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...
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
bool isValid() const noexcept
uint32_t processorId() const noexcept
Serializes key/value parameters into a versioned blob.
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
void write(const char *key, float value)
Writes a float parameter.
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").
Describes the audio environment for a DSP processor.
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
int maxBlockSize
Maximum number of samples per processing block.
double sampleRate
Sample rate in Hz.
Stores normalised biquad coefficients (b0, b1, b2, a1, a2), always double.
static BiquadCoeffs makeFirstOrderHighPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) high-pass filter.
static BiquadCoeffs makePeakMatched(double sampleRate, double freq, double Q, double gainDb) noexcept
Analog-matched ("de-cramped") peaking filter (Vicanek design).
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
static BiquadCoeffs makeBandPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Band-pass filter (constant 0 dB peak gain).
static BiquadCoeffs makePeak(double sampleRate, double freq, double Q, double gainDb) noexcept
Peak (parametric EQ) filter.
static BiquadCoeffs makeFirstOrderLowPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) low-pass filter.
static BiquadCoeffs makeTilt(double sampleRate, double pivotFreq, double gainDb) noexcept
Creates a first-order tilt filter.
static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Low-pass filter.
static BiquadCoeffs makeLowShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
Low-shelf filter.
static BiquadCoeffs makeHighShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
High-shelf filter.
static BiquadCoeffs makeNotch(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Notch (band-reject) filter.
Full configuration for a single EQ band.
T q
Q factor (0.1 = wide, 10 = narrow).
T gain
Gain in dB (Peak, Shelf, Tilt).
bool enabled
False to bypass this band.
int slope
Slope in dB/oct (LP/HP only: 6-48).
BandType type
Filter type for this band.
T frequency
Center/cutoff frequency in Hz.
One band's published configuration: a seqlock over atomic words.
std::atomic< T > frequency
BandConfig read() const noexcept
Reads the band into a caller-private plain copy (readouts).
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...
static constexpr int kSeqlockMaxAttempts
std::atomic< unsigned > seq
void publish(const BandConfig &c) noexcept
Publishes a whole band configuration (control thread only).
std::atomic< bool > enabled
bool tryRead(BandConfig &out) const noexcept
The bounded seqlock read for the AUDIO thread.