DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
DynamicEQ.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
57#include "../Core/AudioBuffer.h"
58#include "../Core/AudioSpec.h"
59#include "../Core/Biquad.h"
60#include "../Core/DspMath.h"
61#include "../Core/Hilbert.h"
62#include "../Core/Oversampling.h"
63#include "../Core/RingBuffer.h"
64#include "../Core/DenormalGuard.h"
65#include "../Core/StateBlob.h"
66
67#include <algorithm>
68#include <array>
69#include <atomic>
70#include <cmath>
71#include <cstddef>
72#include <cstdint>
73#include <cstdio>
74#include <memory>
75#include <type_traits>
76#include <vector>
77
78namespace dspark {
79
87template <FloatType T, int MaxBands = 8>
89{
90public:
92 enum class BandShape
93 {
94 Bell,
95 LowShelf,
97 };
98
104 {
105 T frequency = T(1000);
106 T q = T(1.0);
107 T threshold = T(-20);
109 bool enabled = true;
110
111 T aboveRatio = T(1);
113 T aboveReleaseMs = T(50);
114 T aboveRangeDb = T(12);
115 bool aboveBoost = false;
116
117 T belowRatio = T(1);
118 T belowAttackMs = T(10);
119 T belowReleaseMs = T(100);
120 T belowRangeDb = T(12);
121 bool belowBoost = false;
122 };
123
124 static_assert(std::is_trivially_copyable_v<BandConfig>, "BandConfig must be trivially copyable for std::atomic");
125 static_assert(std::atomic<T>::is_always_lock_free,
126 "audio-thread stores must not lock");
127
129 {
130 for (int i = 0; i < MaxBands; ++i) {
131 masterConfigs_[i] = BandConfig{};
132 staged_[static_cast<size_t>(i)].publish(masterConfigs_[i]);
133 paramsDirty_[i].store(true, std::memory_order_relaxed);
134 }
135 }
136
141 void prepare(const AudioSpec& spec)
142 {
143 if (!spec.isValid()) return; // invalid specs are ignored (state kept)
144 isPrepared_.store(false, std::memory_order_relaxed); // basic guarantee
145 spec_ = spec;
146 sampleRate_ = spec.sampleRate;
147
148 if (oversamplingFactor_ > 1)
149 {
150 // Two oversamplers: the audio-path one owns the buffer that we
151 // both process and downsample back; the sidechain one only acts
152 // as a level-detection upsampler so its buffer is read-only.
153 oversampler_ = std::make_unique<Oversampling<T>>(oversamplingFactor_);
154 oversamplerSc_ = std::make_unique<Oversampling<T>>(oversamplingFactor_);
155 oversampler_ ->prepare(spec);
156 oversamplerSc_->prepare(spec);
157 }
158 else
159 {
160 oversampler_.reset();
161 oversamplerSc_.reset();
162 }
163
164 analytic_.assign(static_cast<size_t>(MaxBands * kMaxChannels), HilbertIIR<double> {});
165
166 int maxLaSamples = static_cast<int>(sampleRate_ * oversamplingFactor_ * 0.01) + 1;
167 for (int ch = 0; ch < kMaxChannels; ++ch)
168 lookaheadBuf_[ch].prepare(maxLaSamples);
169
170 updateLookahead();
171 reset();
172 isPrepared_.store(true, std::memory_order_relaxed);
173 }
174
179 void processBlock(AudioBufferView<T> buffer) noexcept
180 {
181 processBlock(buffer, buffer);
182 }
183
189 void processBlock(AudioBufferView<T> audio, AudioBufferView<T> sidechain) noexcept
190 {
191 if (!isPrepared_.load(std::memory_order_relaxed)) return;
192
193 // A sidechain shorter than the audio block would be read past its
194 // end; fall back to self-keying instead of over-reading the caller.
195 if (sidechain.getNumChannels() <= 0 ||
196 sidechain.getNumSamples() < audio.getNumSamples())
197 {
198 if (audio.getNumChannels() <= 0) return;
199 sidechain = audio;
200 }
201
202 DenormalGuard guard;
203
204 if (oversamplingFactor_ > 1 && oversampler_ && oversamplerSc_)
205 {
206 // Up-sample audio and sidechain through their dedicated oversamplers.
207 // Each Oversampling instance owns one internal high-rate buffer, so
208 // we cannot share one between the two streams; processing them
209 // separately keeps each stream's polyphase filter state consistent.
210 auto upAudio = oversampler_->upsample(audio);
211 auto upSc = oversamplerSc_->upsample(sidechain);
212
213 processCore(upAudio, upSc, sampleRate_ * oversamplingFactor_);
214
215 oversampler_->downsample(audio);
216 }
217 else
218 {
219 // Standard processing path
220 processCore(audio, sidechain, sampleRate_);
221 }
222 }
223
232 void setBand(int band, const BandConfig& config) noexcept
233 {
234 if (band < 0 || band >= MaxBands) return;
235
236 BandConfig c = config;
237 // The fallback reads the control thread's own private master, never the
238 // published words: a setter must not race the audio thread to decide
239 // what "the band's previously published value" is.
240 const BandConfig& prev = masterConfigs_[static_cast<size_t>(band)];
241 auto keep = [](T v, T fallback) { return std::isfinite(v) ? v : fallback; };
242 c.frequency = std::max(keep(c.frequency, prev.frequency), T(1));
243 c.q = keep(c.q, prev.q);
244 c.threshold = keep(c.threshold, prev.threshold);
245 c.shape = static_cast<BandShape>(std::clamp(static_cast<int>(c.shape), 0, 2));
246 c.aboveRatio = keep(c.aboveRatio, prev.aboveRatio);
247 c.aboveAttackMs = keep(c.aboveAttackMs, prev.aboveAttackMs);
249 c.aboveRangeDb = std::max(keep(c.aboveRangeDb, prev.aboveRangeDb), T(0));
250 c.belowRatio = keep(c.belowRatio, prev.belowRatio);
251 c.belowAttackMs = keep(c.belowAttackMs, prev.belowAttackMs);
253 c.belowRangeDb = std::max(keep(c.belowRangeDb, prev.belowRangeDb), T(0));
254
255 masterConfigs_[static_cast<size_t>(band)] = c;
256 staged_[static_cast<size_t>(band)].publish(c);
257 paramsDirty_[band].store(true, std::memory_order_release);
258 }
259
260 void setNumBands(int n) noexcept
261 {
262 numBands_.store(std::clamp(n, 1, MaxBands), std::memory_order_relaxed);
263 }
264
284 void setOversampling(int factor) noexcept
285 {
286 oversamplingFactor_ = std::clamp(factor, 1, 4);
287 if (oversamplingFactor_ == 3) oversamplingFactor_ = 4;
288 isPrepared_.store(false, std::memory_order_relaxed); // Forces user to call prepare()
289 }
290
293 void setLookahead(T ms) noexcept
294 {
295 if (!std::isfinite(ms)) return;
296 lookaheadMs_ = std::clamp(ms, T(0), T(10));
297 updateLookahead();
298 }
299
308 [[nodiscard]] int getLatency() const noexcept
309 {
310 const int factor = std::max(oversamplingFactor_, 1);
311 const int la = lookaheadSamples_.load(std::memory_order_relaxed) / factor;
312 const int os = oversampler_ ? oversampler_->getLatency() : 0;
313 return la + os;
314 }
315
316 [[nodiscard]] T getBandGainDb(int band) const noexcept
317 {
318 if (band < 0 || band >= MaxBands) return T(0);
319 return meterGainDb_[band].load(std::memory_order_relaxed);
320 }
321
322 void reset() noexcept
323 {
324 for (int b = 0; b < MaxBands; ++b)
325 {
326 bandDetector_[b].reset();
327 bandFilter_[b].reset();
328 currentGainDb_[b] = T(0);
329 meterGainDb_[b].store(T(0), std::memory_order_relaxed);
330 paramsDirty_[b].store(true, std::memory_order_relaxed);
331 }
332 for (int ch = 0; ch < kMaxChannels; ++ch)
333 lookaheadBuf_[ch].reset();
334 }
335
336
338 [[nodiscard]] std::vector<uint8_t> getState() const
339 {
340 StateWriter w(stateId("DYEQ"), 1);
341 const int n = numBands_.load(std::memory_order_relaxed);
342 w.write("numBands", n);
343 char key[28];
344 for (int i = 0; i < n; ++i)
345 {
346 // Seqlock read into a private copy: reading the band by reference
347 // here made getState() a plain cross-thread read of the words
348 // setBand() writes.
349 const BandConfig c = staged_[static_cast<size_t>(i)].read();
350 std::snprintf(key, sizeof(key), "b%d.freq", i);
351 w.write(key, static_cast<float>(c.frequency));
352 std::snprintf(key, sizeof(key), "b%d.q", i);
353 w.write(key, static_cast<float>(c.q));
354 std::snprintf(key, sizeof(key), "b%d.thresh", i);
355 w.write(key, static_cast<float>(c.threshold));
356 std::snprintf(key, sizeof(key), "b%d.shape", i);
357 w.write(key, static_cast<int>(c.shape));
358 std::snprintf(key, sizeof(key), "b%d.on", i);
359 w.write(key, c.enabled);
360 std::snprintf(key, sizeof(key), "b%d.aRatio", i);
361 w.write(key, static_cast<float>(c.aboveRatio));
362 std::snprintf(key, sizeof(key), "b%d.aAtk", i);
363 w.write(key, static_cast<float>(c.aboveAttackMs));
364 std::snprintf(key, sizeof(key), "b%d.aRel", i);
365 w.write(key, static_cast<float>(c.aboveReleaseMs));
366 std::snprintf(key, sizeof(key), "b%d.aRange", i);
367 w.write(key, static_cast<float>(c.aboveRangeDb));
368 std::snprintf(key, sizeof(key), "b%d.aBoost", i);
369 w.write(key, c.aboveBoost);
370 std::snprintf(key, sizeof(key), "b%d.bRatio", i);
371 w.write(key, static_cast<float>(c.belowRatio));
372 std::snprintf(key, sizeof(key), "b%d.bAtk", i);
373 w.write(key, static_cast<float>(c.belowAttackMs));
374 std::snprintf(key, sizeof(key), "b%d.bRel", i);
375 w.write(key, static_cast<float>(c.belowReleaseMs));
376 std::snprintf(key, sizeof(key), "b%d.bRange", i);
377 w.write(key, static_cast<float>(c.belowRangeDb));
378 std::snprintf(key, sizeof(key), "b%d.bBoost", i);
379 w.write(key, c.belowBoost);
380 }
381 return w.blob();
382 }
383
385 bool setState(const uint8_t* data, size_t size)
386 {
387 StateReader r(data, size);
388 if (!r.isValid() || r.processorId() != stateId("DYEQ")) return false;
389 const int n = std::clamp(r.read("numBands", 0), 0, MaxBands);
390 char key[28];
391 for (int i = 0; i < n; ++i)
392 {
393 BandConfig c;
394 std::snprintf(key, sizeof(key), "b%d.freq", i);
395 c.frequency = static_cast<T>(r.read(key, 1000.0f));
396 std::snprintf(key, sizeof(key), "b%d.q", i);
397 c.q = static_cast<T>(r.read(key, 1.0f));
398 std::snprintf(key, sizeof(key), "b%d.thresh", i);
399 c.threshold = static_cast<T>(r.read(key, -20.0f));
400 std::snprintf(key, sizeof(key), "b%d.shape", i);
401 c.shape = static_cast<BandShape>(std::clamp(r.read(key, 0), 0, 2));
402 std::snprintf(key, sizeof(key), "b%d.on", i);
403 c.enabled = r.read(key, true);
404 std::snprintf(key, sizeof(key), "b%d.aRatio", i);
405 c.aboveRatio = static_cast<T>(r.read(key, 1.0f));
406 std::snprintf(key, sizeof(key), "b%d.aAtk", i);
407 c.aboveAttackMs = static_cast<T>(r.read(key, 5.0f));
408 std::snprintf(key, sizeof(key), "b%d.aRel", i);
409 c.aboveReleaseMs = static_cast<T>(r.read(key, 50.0f));
410 std::snprintf(key, sizeof(key), "b%d.aRange", i);
411 c.aboveRangeDb = static_cast<T>(r.read(key, 12.0f));
412 std::snprintf(key, sizeof(key), "b%d.aBoost", i);
413 c.aboveBoost = r.read(key, false);
414 std::snprintf(key, sizeof(key), "b%d.bRatio", i);
415 c.belowRatio = static_cast<T>(r.read(key, 1.0f));
416 std::snprintf(key, sizeof(key), "b%d.bAtk", i);
417 c.belowAttackMs = static_cast<T>(r.read(key, 10.0f));
418 std::snprintf(key, sizeof(key), "b%d.bRel", i);
419 c.belowReleaseMs = static_cast<T>(r.read(key, 100.0f));
420 std::snprintf(key, sizeof(key), "b%d.bRange", i);
421 c.belowRangeDb = static_cast<T>(r.read(key, 12.0f));
422 std::snprintf(key, sizeof(key), "b%d.bBoost", i);
423 c.belowBoost = r.read(key, false);
424 setBand(i, c);
425 }
426 setNumBands(n);
427 return true;
428 }
429
430private:
431 static constexpr int kMaxChannels = 16;
432 static constexpr T kMinLevelDb = T(-100.0);
433 static constexpr T kMinEnvelope = T(1e-12); // Prevents NaN in log10
434
435 struct BandState
436 {
437 BandConfig cfg;
438 T aboveAtkCoeff, aboveRelCoeff;
439 T belowAtkCoeff, belowRelCoeff;
440 };
441
442 void processCore(AudioBufferView<T>& audio, AudioBufferView<T>& sidechain, double currentFs) noexcept
443 {
444 const int nCh = std::min(audio.getNumChannels(), kMaxChannels);
445 const int scCh = sidechain.getNumChannels();
446 const int nS = audio.getNumSamples();
447 if (scCh <= 0) return; // a 0-channel sidechain would index getChannel(-1)
448 const int nb = numBands_.load(std::memory_order_relaxed);
449 const int laSamples = lookaheadSamples_.load(std::memory_order_relaxed);
450
451 // 1. Thread-Safe State Update
452 for (int b = 0; b < nb; ++b)
453 {
454 if (paramsDirty_[b].exchange(false, std::memory_order_acquire))
455 updateBandInternalState(b, currentFs);
456 }
457
458 // 2. Chunks: the detection of every band runs first, band by band and
459 // channel by channel, so each detector (and its allpass pair) keeps
460 // its state in registers; the gain computers and filters then run
461 // sample by sample. Feed-forward, so the order changes nothing.
462 for (int chunkStart = 0; chunkStart < nS; chunkStart += kDetChunk)
463 {
464 const int chunkLen = std::min(kDetChunk, nS - chunkStart);
465
466 for (int b = 0; b < nb; ++b)
467 {
468 if (!states_[b].cfg.enabled) continue;
469 T* levels = detLevelDb_[static_cast<size_t>(b)].data();
470 std::fill(levels, levels + chunkLen, kMinLevelDb);
471
472 // Sidechain Detection (Stereo Linked by Max Peak)
473 for (int ch = 0; ch < nCh; ++ch)
474 {
475 const T* sc = sidechain.getChannel(std::min(ch, scCh - 1)) + chunkStart;
476 for (int j = 0; j < chunkLen; ++j)
477 detBand_[static_cast<size_t>(j)] =
478 static_cast<double>(bandDetector_[b].processSample(sc[j], ch));
479 analytic_[static_cast<size_t>(b * kMaxChannels + ch)].magnitudeBlock(
480 detBand_.data(), detMag_.data(), chunkLen);
481 for (int j = 0; j < chunkLen; ++j)
482 {
483 // max(|x|, analytic magnitude): a steady tone reads as
484 // its flat amplitude, not a rectified wave (see @file).
485 const T detected = std::max(static_cast<T>(std::abs(detBand_[static_cast<size_t>(j)])),
486 static_cast<T>(detMag_[static_cast<size_t>(j)]));
487 const T levelDb = gainToDecibels(std::max(detected, kMinEnvelope));
488 if (levelDb > levels[j]) levels[j] = levelDb;
489 }
490 }
491 }
492
493 for (int j = 0; j < chunkLen; ++j)
494 {
495 const int i = chunkStart + j;
496 for (int b = 0; b < nb; ++b)
497 {
498 if (!states_[b].cfg.enabled) continue;
499
500 const T maxLevelDb = detLevelDb_[static_cast<size_t>(b)][static_cast<size_t>(j)];
501
502 // Gain Computer
503 T targetGainDb = computeTargetGain(states_[b].cfg, maxLevelDb);
504
505 // Gain Ballistics (Attack/Release applied to the Gain itself)
506 T& currentGain = currentGainDb_[b];
507 T diff = targetGainDb - currentGain;
508
509 T coeff;
510 if (maxLevelDb > states_[b].cfg.threshold) {
511 coeff = (std::abs(targetGainDb) > std::abs(currentGain))
512 ? states_[b].aboveAtkCoeff : states_[b].aboveRelCoeff;
513 } else {
514 coeff = (std::abs(targetGainDb) > std::abs(currentGain))
515 ? states_[b].belowAtkCoeff : states_[b].belowRelCoeff;
516 }
517
518 currentGain += coeff * diff;
519
520 // Refresh gain-filter coefficients every 16 samples - the gain
521 // envelope is slow enough that this granularity is inaudible.
522 // Bells use the precomputed freq/Q trig, so a refresh costs
523 // one pow() instead of a full sin/cos/pow redesign; shelves
524 // run their full design, which at 1/16th rate stays negligible.
525 // Stream-owner direct writes (setCoeffsNow): these values are
526 // computed HERE, on the audio thread, for this thread's own
527 // use, so they never touch the staged cross-thread channel.
528 if ((i & 15) == 0)
529 {
530 if (std::abs(currentGain) > T(0.01))
531 {
532 switch (states_[b].cfg.shape)
533 {
534 case BandShape::Bell:
535 updateDynamicPeakCoeffs(b, currentGain);
536 break;
538 bandFilter_[b].setCoeffsNow(BiquadCoeffs::makeLowShelf(
539 currentFs, static_cast<double>(states_[b].cfg.frequency),
540 static_cast<double>(currentGain)));
541 break;
543 bandFilter_[b].setCoeffsNow(BiquadCoeffs::makeHighShelf(
544 currentFs, static_cast<double>(states_[b].cfg.frequency),
545 static_cast<double>(currentGain)));
546 break;
547 }
548 }
549 else
550 bandFilter_[b].setCoeffsNow(BiquadCoeffs{}); // Bypass
551 }
552
553 if ((i & 63) == 0) // Sub-sample metering update
554 meterGainDb_[b].store(currentGain, std::memory_order_relaxed);
555 }
556
557 // Apply Filters
558 for (int ch = 0; ch < nCh; ++ch)
559 {
560 T audioSample = audio.getChannel(ch)[i];
561
562 if (laSamples > 0) {
563 lookaheadBuf_[ch].push(audioSample);
564 audioSample = lookaheadBuf_[ch].read(laSamples);
565 }
566
567 for (int b = 0; b < nb; ++b) {
568 if (states_[b].cfg.enabled) {
569 audioSample = bandFilter_[b].processSample(audioSample, ch);
570 }
571 }
572 audio.getChannel(ch)[i] = audioSample;
573 }
574 }
575 }
576 }
577
578 [[nodiscard]] T computeTargetGain(const BandConfig& cfg, T levelDb) const noexcept
579 {
580 T gainDb = T(0);
581
582 if (levelDb > cfg.threshold)
583 {
584 if (cfg.aboveRatio > T(1.001)) {
585 T overDb = levelDb - cfg.threshold;
586 T amount = std::min(overDb * (T(1) - T(1) / cfg.aboveRatio), cfg.aboveRangeDb);
587 gainDb += cfg.aboveBoost ? amount : -amount;
588 }
589 }
590 else
591 {
592 if (cfg.belowRatio > T(1.001)) {
593 T underDb = cfg.threshold - levelDb;
594 T amount = std::min(underDb * (T(1) - T(1) / cfg.belowRatio), cfg.belowRangeDb);
595 gainDb += cfg.belowBoost ? amount : -amount;
596 }
597 }
598 return gainDb;
599 }
600
601 void updateBandInternalState(int b, double fs) noexcept
602 {
603 // Bounded seqlock read of the published config into a thread-private
604 // plain copy. Every word crossing threads is std::atomic (see
605 // StagedBand): copying the struct itself here, even under a correct
606 // counter, was a plain concurrent read of words the control thread
607 // writes and so a data race by the C++ model, not merely a torn-value
608 // hazard. This runs on the audio thread, so the read is bounded: it
609 // never waits for the control thread to finish publishing.
610 BandConfig cfg;
611 if (!staged_[static_cast<size_t>(b)].tryRead(cfg))
612 {
613 // Gave up: leave states_[b] exactly as it is -- the band keeps
614 // running on its current config -- and re-arm so the publication is
615 // adopted on a later block.
616 paramsDirty_[b].store(true, std::memory_order_release);
617 return;
618 }
619
620 // A band re-enabled after being disabled would replay arbitrarily old
621 // filter history and gain state: start it clean.
622 const bool wasEnabled = states_[b].cfg.enabled;
623 if (cfg.enabled && !wasEnabled)
624 {
625 bandDetector_[b].reset();
626 bandFilter_[b].reset();
627 currentGainDb_[b] = T(0);
628 }
629 states_[b].cfg = cfg;
630
631 // Detector listens where the gain filter acts: bandpass for bells,
632 // the corresponding half of the spectrum for shelves.
633 // Direct writes (setCoeffsNow): this runs on the audio thread, which
634 // owns these Biquads (one-master rule, see the threading block), so
635 // even this cold reconfiguration must not self-publish through the
636 // staged channel.
637 switch (cfg.shape)
638 {
639 case BandShape::Bell:
640 bandDetector_[b].setCoeffsNow(BiquadCoeffs::makeBandPass(fs, cfg.frequency, cfg.q));
641 break;
643 bandDetector_[b].setCoeffsNow(BiquadCoeffs::makeLowPass(fs, cfg.frequency, T(0.707)));
644 break;
646 bandDetector_[b].setCoeffsNow(BiquadCoeffs::makeHighPass(fs, cfg.frequency, T(0.707)));
647 break;
648 }
649
650 // Precompute the freq/Q-dependent peak-EQ terms ONCE per parameter change
651 // (cos w0 and alpha), so the per-block dynamic update needs only a pow().
652 // Double, like the rest of the coefficient path: the design is only
653 // ever as good as the arithmetic that builds it.
654 const double w0 = 2.0 * 3.14159265358979323846 * static_cast<double>(cfg.frequency) / fs;
655 precomputedCos_[b] = std::cos(w0);
656 precomputedAlpha_[b] = std::sin(w0) / (2.0 * std::max(static_cast<double>(cfg.q), 0.001));
657
658 auto calcCoeff = [fs](T ms) -> T {
659 const double tauSec = std::max(static_cast<double>(ms), 0.01) / 1000.0;
660 return static_cast<T>(1.0 - std::exp(-1.0 / (fs * tauSec)));
661 };
662
663 states_[b].aboveAtkCoeff = calcCoeff(cfg.aboveAttackMs);
664 states_[b].aboveRelCoeff = calcCoeff(cfg.aboveReleaseMs);
665 states_[b].belowAtkCoeff = calcCoeff(cfg.belowAttackMs);
666 states_[b].belowRelCoeff = calcCoeff(cfg.belowReleaseMs);
667 }
668
670 void updateDynamicPeakCoeffs(int b, T gainDb) noexcept
671 {
672 const double A = std::pow(10.0, static_cast<double>(gainDb) / 40.0);
673 const double cosw = precomputedCos_[b];
674 const double alpha = precomputedAlpha_[b];
675 const double a0Inv = 1.0 / (1.0 + alpha / A);
676
677 BiquadCoeffs c;
678 c.b0 = (1.0 + alpha * A) * a0Inv;
679 c.b1 = (-2.0 * cosw) * a0Inv;
680 c.b2 = (1.0 - alpha * A) * a0Inv;
681 c.a1 = (-2.0 * cosw) * a0Inv;
682 c.a2 = (1.0 - alpha / A) * a0Inv;
683 // Stream-owner direct write: computed on the audio thread for its
684 // own use, so it bypasses the staged cross-thread channel.
685 bandFilter_[b].setCoeffsNow(c);
686 }
687
688 void updateLookahead() noexcept
689 {
690 if (sampleRate_ > 0) {
691 int samples = static_cast<int>(sampleRate_ * oversamplingFactor_ * lookaheadMs_ / T(1000));
692 lookaheadSamples_.store(samples, std::memory_order_relaxed);
693 }
694 }
695
696 // -- State & Mem ---------------------------------------------------------
697 std::atomic<bool> isPrepared_ { false };
698 AudioSpec spec_ {};
699 double sampleRate_ = 0;
700
701 std::atomic<int> numBands_ { 0 };
702
713 struct StagedBand
714 {
715 std::atomic<T> frequency { T(1000) };
716 std::atomic<T> q { T(1.0) };
717 std::atomic<T> threshold { T(-20) };
718 std::atomic<int> shape { static_cast<int>(BandShape::Bell) };
719 std::atomic<bool> enabled { true };
720
721 std::atomic<T> aboveRatio { T(1) };
722 std::atomic<T> aboveAttackMs { T(5) };
723 std::atomic<T> aboveReleaseMs { T(50) };
724 std::atomic<T> aboveRangeDb { T(12) };
725 std::atomic<bool> aboveBoost { false };
726
727 std::atomic<T> belowRatio { T(1) };
728 std::atomic<T> belowAttackMs { T(10) };
729 std::atomic<T> belowReleaseMs { T(100) };
730 std::atomic<T> belowRangeDb { T(12) };
731 std::atomic<bool> belowBoost { false };
732
733 std::atomic<unsigned> seq { 0 };
734
746 void publish(const BandConfig& c) noexcept
747 {
748 seq.fetch_add(1, std::memory_order_acq_rel); // -> odd
749 std::atomic_thread_fence(std::memory_order_release);
750 frequency.store(c.frequency, std::memory_order_relaxed);
751 q.store(c.q, std::memory_order_relaxed);
752 threshold.store(c.threshold, std::memory_order_relaxed);
753 shape.store(static_cast<int>(c.shape), std::memory_order_relaxed);
754 enabled.store(c.enabled, std::memory_order_relaxed);
755 aboveRatio.store(c.aboveRatio, std::memory_order_relaxed);
756 aboveAttackMs.store(c.aboveAttackMs, std::memory_order_relaxed);
757 aboveReleaseMs.store(c.aboveReleaseMs, std::memory_order_relaxed);
758 aboveRangeDb.store(c.aboveRangeDb, std::memory_order_relaxed);
759 aboveBoost.store(c.aboveBoost, std::memory_order_relaxed);
760 belowRatio.store(c.belowRatio, std::memory_order_relaxed);
761 belowAttackMs.store(c.belowAttackMs, std::memory_order_relaxed);
762 belowReleaseMs.store(c.belowReleaseMs, std::memory_order_relaxed);
763 belowRangeDb.store(c.belowRangeDb, std::memory_order_relaxed);
764 belowBoost.store(c.belowBoost, std::memory_order_relaxed);
765 seq.fetch_add(1, std::memory_order_release); // -> even
766 }
767
773 static constexpr int kSeqlockMaxAttempts = 3;
774
781 void loadWordsRelaxed(BandConfig& c) const noexcept
782 {
783 c.frequency = frequency.load(std::memory_order_relaxed);
784 c.q = q.load(std::memory_order_relaxed);
785 c.threshold = threshold.load(std::memory_order_relaxed);
786 c.shape = static_cast<BandShape>(shape.load(std::memory_order_relaxed));
787 c.enabled = enabled.load(std::memory_order_relaxed);
788 c.aboveRatio = aboveRatio.load(std::memory_order_relaxed);
789 c.aboveAttackMs = aboveAttackMs.load(std::memory_order_relaxed);
790 c.aboveReleaseMs = aboveReleaseMs.load(std::memory_order_relaxed);
791 c.aboveRangeDb = aboveRangeDb.load(std::memory_order_relaxed);
792 c.aboveBoost = aboveBoost.load(std::memory_order_relaxed);
793 c.belowRatio = belowRatio.load(std::memory_order_relaxed);
794 c.belowAttackMs = belowAttackMs.load(std::memory_order_relaxed);
795 c.belowReleaseMs = belowReleaseMs.load(std::memory_order_relaxed);
796 c.belowRangeDb = belowRangeDb.load(std::memory_order_relaxed);
797 c.belowBoost = belowBoost.load(std::memory_order_relaxed);
798 }
799
820 [[nodiscard]] bool tryRead(BandConfig& out) const noexcept
821 {
822 for (int attempt = 0; attempt < kSeqlockMaxAttempts; ++attempt)
823 {
824 const unsigned s0 = seq.load(std::memory_order_acquire);
825 if ((s0 & 1u) != 0u) continue; // writer mid-publish: do not copy
826 BandConfig c;
827 loadWordsRelaxed(c);
828 std::atomic_thread_fence(std::memory_order_acquire);
829 if (s0 == seq.load(std::memory_order_relaxed))
830 {
831 out = c; // commit only on validation
832 return true;
833 }
834 }
835 return false;
836 }
837
853 [[nodiscard]] BandConfig read() const noexcept
854 {
855 BandConfig c;
856 unsigned s0, s1;
857 do {
858 s0 = seq.load(std::memory_order_acquire);
859 loadWordsRelaxed(c);
860 std::atomic_thread_fence(std::memory_order_acquire);
861 s1 = seq.load(std::memory_order_relaxed);
862 } while ((s0 & 1u) != 0u || s0 != s1);
863 return c;
864 }
865 };
866
868 std::array<StagedBand, MaxBands> staged_ {};
869
874 std::array<BandConfig, MaxBands> masterConfigs_ {};
875 std::array<std::atomic<bool>, MaxBands> paramsDirty_ {};
876 std::array<BandState, MaxBands> states_ {};
877 // Precomputed freq/Q-dependent peak-EQ trig terms (per band) so the per-block
878 // dynamic gain update needs only a pow(), not cos/sin/pow every sample.
879 std::array<double, MaxBands> precomputedCos_ {};
880 std::array<double, MaxBands> precomputedAlpha_ {};
881
882 // Biquads must span the class's full channel capacity (kMaxChannels); the
883 // default Biquad<T> is only 8 channels, which would index its per-channel state
884 // out of bounds when processing 9..16-channel (surround/immersive) audio.
885 std::array<Biquad<T, kMaxChannels>, MaxBands> bandDetector_ {};
886 std::array<Biquad<T, kMaxChannels>, MaxBands> bandFilter_ {};
887 std::array<T, MaxBands> currentGainDb_ {};
888 std::array<std::atomic<T>, MaxBands> meterGainDb_ {};
889
890 int oversamplingFactor_ = 1;
891 std::unique_ptr<Oversampling<T>> oversampler_; // audio path
892 std::unique_ptr<Oversampling<T>> oversamplerSc_; // sidechain path
893
894 T lookaheadMs_ = T(0);
895 std::atomic<int> lookaheadSamples_ { 0 };
896 std::array<RingBuffer<T>, kMaxChannels> lookaheadBuf_ {};
897 std::vector<HilbertIIR<double>> analytic_;
898 static constexpr int kDetChunk = 64;
899 std::array<double, kDetChunk> detBand_ {};
900 std::array<double, kDetChunk> detMag_ {};
901 std::array<std::array<T, kDetChunk>, MaxBands> detLevelDb_ {};
902};
903
904} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Dynamic parametric EQ with dual above/below threshold per band.
Definition DynamicEQ.h:89
std::vector< uint8_t > getState() const
Serializes bands and modes (setup/UI threads; allocates).
Definition DynamicEQ.h:338
bool setState(const uint8_t *data, size_t size)
Restores bands from a blob (tolerant; rejects foreign ids).
Definition DynamicEQ.h:385
void reset() noexcept
Definition DynamicEQ.h:322
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio in-place using self-sidechain.
Definition DynamicEQ.h:179
T getBandGainDb(int band) const noexcept
Definition DynamicEQ.h:316
int getLatency() const noexcept
Total latency in samples at the base rate.
Definition DynamicEQ.h:308
void setOversampling(int factor) noexcept
Sets the internal oversampling factor.
Definition DynamicEQ.h:284
BandShape
Shape of the dynamic gain filter (and its detector region).
Definition DynamicEQ.h:93
@ LowShelf
Dynamic low shelf; detector hears below freq.
@ Bell
Parametric bell; detector is a bandpass at freq/Q.
@ HighShelf
Dynamic high shelf; detector hears above freq.
void setBand(int band, const BandConfig &config) noexcept
Thread-safe configuration update for a specific band.
Definition DynamicEQ.h:232
void prepare(const AudioSpec &spec)
Initializes the dynamic EQ, allocating ring buffers and oversamplers.
Definition DynamicEQ.h:141
void setLookahead(T ms) noexcept
Sets the lookahead (0..10 ms). Applied immediately (may click); non-finite values are ignored.
Definition DynamicEQ.h:293
void setNumBands(int n) noexcept
Definition DynamicEQ.h:260
void processBlock(AudioBufferView< T > audio, AudioBufferView< T > sidechain) noexcept
Processes audio with an external sidechain.
Definition DynamicEQ.h:189
Zero-latency analytic pair from two allpass chains (a 90-degree phase-difference network).
Definition Hilbert.h:235
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.
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
Definition DspMath.h:89
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
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
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 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
Full configuration for a single dynamic EQ band.
Definition DynamicEQ.h:104