DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
DynamicEQ.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
35#include "../Core/AudioBuffer.h"
36#include "../Core/AudioSpec.h"
37#include "../Core/Biquad.h"
38#include "../Core/DspMath.h"
39#include "../Core/Oversampling.h"
40#include "../Core/RingBuffer.h"
41#include "../Core/DenormalGuard.h"
42#include "../Core/StateBlob.h"
43
44#include <algorithm>
45#include <array>
46#include <atomic>
47#include <cmath>
48#include <cstddef>
49#include <cstdint>
50#include <cstdio>
51#include <memory>
52#include <type_traits>
53#include <vector>
54
55namespace dspark {
56
64template <FloatType T, int MaxBands = 8>
66{
67public:
69 enum class BandShape
70 {
71 Bell,
72 LowShelf,
74 };
75
81 {
82 T frequency = T(1000);
83 T q = T(1.0);
84 T threshold = T(-20);
86 bool enabled = true;
87
88 T aboveRatio = T(1);
89 T aboveAttackMs = T(5);
90 T aboveReleaseMs = T(50);
91 T aboveRangeDb = T(12);
92 bool aboveBoost = false;
93
94 T belowRatio = T(1);
95 T belowAttackMs = T(10);
96 T belowReleaseMs = T(100);
97 T belowRangeDb = T(12);
98 bool belowBoost = false;
99 };
100
101 static_assert(std::is_trivially_copyable_v<BandConfig>, "BandConfig must be trivially copyable for std::atomic");
102
104 {
105 for (int i = 0; i < MaxBands; ++i) {
106 configs_[i] = BandConfig{};
107 configSeq_[i].store(0, std::memory_order_relaxed);
108 paramsDirty_[i].store(true, std::memory_order_relaxed);
109 }
110 }
111
116 void prepare(const AudioSpec& spec)
117 {
118 if (!spec.isValid()) return; // invalid specs are ignored (state kept)
119 isPrepared_.store(false, std::memory_order_relaxed); // basic guarantee
120 spec_ = spec;
121 sampleRate_ = spec.sampleRate;
122
123 if (oversamplingFactor_ > 1)
124 {
125 // Two oversamplers: the audio-path one owns the buffer that we
126 // both process and downsample back; the sidechain one only acts
127 // as a level-detection upsampler so its buffer is read-only.
128 oversampler_ = std::make_unique<Oversampling<T>>(oversamplingFactor_);
129 oversamplerSc_ = std::make_unique<Oversampling<T>>(oversamplingFactor_);
130 oversampler_ ->prepare(spec);
131 oversamplerSc_->prepare(spec);
132 }
133 else
134 {
135 oversampler_.reset();
136 oversamplerSc_.reset();
137 }
138
139 int maxLaSamples = static_cast<int>(sampleRate_ * oversamplingFactor_ * 0.01) + 1;
140 for (int ch = 0; ch < kMaxChannels; ++ch)
141 lookaheadBuf_[ch].prepare(maxLaSamples);
142
143 updateLookahead();
144 reset();
145 isPrepared_.store(true, std::memory_order_relaxed);
146 }
147
152 void processBlock(AudioBufferView<T> buffer) noexcept
153 {
154 processBlock(buffer, buffer);
155 }
156
162 void processBlock(AudioBufferView<T> audio, AudioBufferView<T> sidechain) noexcept
163 {
164 if (!isPrepared_.load(std::memory_order_relaxed)) return;
165
166 // A sidechain shorter than the audio block would be read past its
167 // end; fall back to self-keying instead of over-reading the caller.
168 if (sidechain.getNumChannels() <= 0 ||
169 sidechain.getNumSamples() < audio.getNumSamples())
170 {
171 if (audio.getNumChannels() <= 0) return;
172 sidechain = audio;
173 }
174
175 DenormalGuard guard;
176
177 if (oversamplingFactor_ > 1 && oversampler_ && oversamplerSc_)
178 {
179 // Up-sample audio and sidechain through their dedicated oversamplers.
180 // Each Oversampling instance owns one internal high-rate buffer, so
181 // we cannot share one between the two streams; processing them
182 // separately keeps each stream's polyphase filter state consistent.
183 auto upAudio = oversampler_->upsample(audio);
184 auto upSc = oversamplerSc_->upsample(sidechain);
185
186 processCore(upAudio, upSc, sampleRate_ * oversamplingFactor_);
187
188 oversampler_->downsample(audio);
189 }
190 else
191 {
192 // Standard processing path
193 processCore(audio, sidechain, sampleRate_);
194 }
195 }
196
205 void setBand(int band, const BandConfig& config) noexcept
206 {
207 if (band < 0 || band >= MaxBands) return;
208
209 BandConfig c = config;
210 const BandConfig& prev = configs_[static_cast<size_t>(band)];
211 auto keep = [](T v, T fallback) { return std::isfinite(v) ? v : fallback; };
212 c.frequency = std::max(keep(c.frequency, prev.frequency), T(1));
213 c.q = keep(c.q, prev.q);
214 c.threshold = keep(c.threshold, prev.threshold);
215 c.shape = static_cast<BandShape>(std::clamp(static_cast<int>(c.shape), 0, 2));
216 c.aboveRatio = keep(c.aboveRatio, prev.aboveRatio);
217 c.aboveAttackMs = keep(c.aboveAttackMs, prev.aboveAttackMs);
219 c.aboveRangeDb = std::max(keep(c.aboveRangeDb, prev.aboveRangeDb), T(0));
220 c.belowRatio = keep(c.belowRatio, prev.belowRatio);
221 c.belowAttackMs = keep(c.belowAttackMs, prev.belowAttackMs);
223 c.belowRangeDb = std::max(keep(c.belowRangeDb, prev.belowRangeDb), T(0));
224
225 // Seqlock publish (lock-free, no atomic<BigStruct> mutex).
226 configSeq_[band].fetch_add(1, std::memory_order_acq_rel); // odd
227 configs_[static_cast<size_t>(band)] = c;
228 configSeq_[band].fetch_add(1, std::memory_order_release); // even
229 paramsDirty_[band].store(true, std::memory_order_release);
230 }
231
232 void setNumBands(int n) noexcept
233 {
234 numBands_.store(std::clamp(n, 1, MaxBands), std::memory_order_relaxed);
235 }
236
256 void setOversampling(int factor) noexcept
257 {
258 oversamplingFactor_ = std::clamp(factor, 1, 4);
259 if (oversamplingFactor_ == 3) oversamplingFactor_ = 4;
260 isPrepared_.store(false, std::memory_order_relaxed); // Forces user to call prepare()
261 }
262
265 void setLookahead(T ms) noexcept
266 {
267 if (!std::isfinite(ms)) return;
268 lookaheadMs_ = std::clamp(ms, T(0), T(10));
269 updateLookahead();
270 }
271
280 [[nodiscard]] int getLatency() const noexcept
281 {
282 const int factor = std::max(oversamplingFactor_, 1);
283 const int la = lookaheadSamples_.load(std::memory_order_relaxed) / factor;
284 const int os = oversampler_ ? oversampler_->getLatency() : 0;
285 return la + os;
286 }
287
288 [[nodiscard]] T getBandGainDb(int band) const noexcept
289 {
290 if (band < 0 || band >= MaxBands) return T(0);
291 return meterGainDb_[band].load(std::memory_order_relaxed);
292 }
293
294 void reset() noexcept
295 {
296 for (int b = 0; b < MaxBands; ++b)
297 {
298 bandDetector_[b].reset();
299 bandFilter_[b].reset();
300 currentGainDb_[b] = T(0);
301 meterGainDb_[b].store(T(0), std::memory_order_relaxed);
302 paramsDirty_[b].store(true, std::memory_order_relaxed);
303 }
304 for (int ch = 0; ch < kMaxChannels; ++ch)
305 lookaheadBuf_[ch].reset();
306 }
307
308
310 [[nodiscard]] std::vector<uint8_t> getState() const
311 {
312 StateWriter w(stateId("DYEQ"), 1);
313 const int n = numBands_.load(std::memory_order_relaxed);
314 w.write("numBands", n);
315 char key[28];
316 for (int i = 0; i < n; ++i)
317 {
318 const BandConfig& c = configs_[static_cast<size_t>(i)];
319 std::snprintf(key, sizeof(key), "b%d.freq", i);
320 w.write(key, static_cast<float>(c.frequency));
321 std::snprintf(key, sizeof(key), "b%d.q", i);
322 w.write(key, static_cast<float>(c.q));
323 std::snprintf(key, sizeof(key), "b%d.thresh", i);
324 w.write(key, static_cast<float>(c.threshold));
325 std::snprintf(key, sizeof(key), "b%d.shape", i);
326 w.write(key, static_cast<int>(c.shape));
327 std::snprintf(key, sizeof(key), "b%d.on", i);
328 w.write(key, c.enabled);
329 std::snprintf(key, sizeof(key), "b%d.aRatio", i);
330 w.write(key, static_cast<float>(c.aboveRatio));
331 std::snprintf(key, sizeof(key), "b%d.aAtk", i);
332 w.write(key, static_cast<float>(c.aboveAttackMs));
333 std::snprintf(key, sizeof(key), "b%d.aRel", i);
334 w.write(key, static_cast<float>(c.aboveReleaseMs));
335 std::snprintf(key, sizeof(key), "b%d.aRange", i);
336 w.write(key, static_cast<float>(c.aboveRangeDb));
337 std::snprintf(key, sizeof(key), "b%d.aBoost", i);
338 w.write(key, c.aboveBoost);
339 std::snprintf(key, sizeof(key), "b%d.bRatio", i);
340 w.write(key, static_cast<float>(c.belowRatio));
341 std::snprintf(key, sizeof(key), "b%d.bAtk", i);
342 w.write(key, static_cast<float>(c.belowAttackMs));
343 std::snprintf(key, sizeof(key), "b%d.bRel", i);
344 w.write(key, static_cast<float>(c.belowReleaseMs));
345 std::snprintf(key, sizeof(key), "b%d.bRange", i);
346 w.write(key, static_cast<float>(c.belowRangeDb));
347 std::snprintf(key, sizeof(key), "b%d.bBoost", i);
348 w.write(key, c.belowBoost);
349 }
350 return w.blob();
351 }
352
354 bool setState(const uint8_t* data, size_t size)
355 {
356 StateReader r(data, size);
357 if (!r.isValid() || r.processorId() != stateId("DYEQ")) return false;
358 const int n = std::clamp(r.read("numBands", 0), 0, MaxBands);
359 char key[28];
360 for (int i = 0; i < n; ++i)
361 {
362 BandConfig c;
363 std::snprintf(key, sizeof(key), "b%d.freq", i);
364 c.frequency = static_cast<T>(r.read(key, 1000.0f));
365 std::snprintf(key, sizeof(key), "b%d.q", i);
366 c.q = static_cast<T>(r.read(key, 1.0f));
367 std::snprintf(key, sizeof(key), "b%d.thresh", i);
368 c.threshold = static_cast<T>(r.read(key, -20.0f));
369 std::snprintf(key, sizeof(key), "b%d.shape", i);
370 c.shape = static_cast<BandShape>(std::clamp(r.read(key, 0), 0, 2));
371 std::snprintf(key, sizeof(key), "b%d.on", i);
372 c.enabled = r.read(key, true);
373 std::snprintf(key, sizeof(key), "b%d.aRatio", i);
374 c.aboveRatio = static_cast<T>(r.read(key, 1.0f));
375 std::snprintf(key, sizeof(key), "b%d.aAtk", i);
376 c.aboveAttackMs = static_cast<T>(r.read(key, 5.0f));
377 std::snprintf(key, sizeof(key), "b%d.aRel", i);
378 c.aboveReleaseMs = static_cast<T>(r.read(key, 50.0f));
379 std::snprintf(key, sizeof(key), "b%d.aRange", i);
380 c.aboveRangeDb = static_cast<T>(r.read(key, 12.0f));
381 std::snprintf(key, sizeof(key), "b%d.aBoost", i);
382 c.aboveBoost = r.read(key, false);
383 std::snprintf(key, sizeof(key), "b%d.bRatio", i);
384 c.belowRatio = static_cast<T>(r.read(key, 1.0f));
385 std::snprintf(key, sizeof(key), "b%d.bAtk", i);
386 c.belowAttackMs = static_cast<T>(r.read(key, 10.0f));
387 std::snprintf(key, sizeof(key), "b%d.bRel", i);
388 c.belowReleaseMs = static_cast<T>(r.read(key, 100.0f));
389 std::snprintf(key, sizeof(key), "b%d.bRange", i);
390 c.belowRangeDb = static_cast<T>(r.read(key, 12.0f));
391 std::snprintf(key, sizeof(key), "b%d.bBoost", i);
392 c.belowBoost = r.read(key, false);
393 setBand(i, c);
394 }
395 setNumBands(n);
396 return true;
397 }
398
399private:
400 static constexpr int kMaxChannels = 16;
401 static constexpr T kMinLevelDb = T(-100.0);
402 static constexpr T kMinEnvelope = T(1e-12); // Prevents NaN in log10
403
404 struct BandState
405 {
406 BandConfig cfg;
407 T aboveAtkCoeff, aboveRelCoeff;
408 T belowAtkCoeff, belowRelCoeff;
409 };
410
411 void processCore(AudioBufferView<T>& audio, AudioBufferView<T>& sidechain, double currentFs) noexcept
412 {
413 const int nCh = std::min(audio.getNumChannels(), kMaxChannels);
414 const int scCh = sidechain.getNumChannels();
415 const int nS = audio.getNumSamples();
416 if (scCh <= 0) return; // a 0-channel sidechain would index getChannel(-1)
417 const int nb = numBands_.load(std::memory_order_relaxed);
418 const int laSamples = lookaheadSamples_.load(std::memory_order_relaxed);
419
420 // 1. Thread-Safe State Update
421 for (int b = 0; b < nb; ++b)
422 {
423 if (paramsDirty_[b].exchange(false, std::memory_order_acquire))
424 updateBandInternalState(b, currentFs);
425 }
426
427 // 2. Audio Processing Loop
428 for (int i = 0; i < nS; ++i)
429 {
430 for (int b = 0; b < nb; ++b)
431 {
432 if (!states_[b].cfg.enabled) continue;
433
434 T maxLevelDb = kMinLevelDb;
435
436 // Sidechain Detection (Stereo Linked by Max Peak)
437 for (int ch = 0; ch < nCh; ++ch)
438 {
439 int sc = std::min(ch, scCh - 1);
440 T scSample = sidechain.getChannel(sc)[i];
441
442 T detected = std::abs(bandDetector_[b].processSample(scSample, ch));
443 T levelDb = gainToDecibels(std::max(detected, kMinEnvelope));
444
445 if (levelDb > maxLevelDb) maxLevelDb = levelDb;
446 }
447
448 // Gain Computer
449 T targetGainDb = computeTargetGain(states_[b].cfg, maxLevelDb);
450
451 // Gain Ballistics (Attack/Release applied to the Gain itself)
452 T& currentGain = currentGainDb_[b];
453 T diff = targetGainDb - currentGain;
454
455 T coeff;
456 if (maxLevelDb > states_[b].cfg.threshold) {
457 coeff = (std::abs(targetGainDb) > std::abs(currentGain))
458 ? states_[b].aboveAtkCoeff : states_[b].aboveRelCoeff;
459 } else {
460 coeff = (std::abs(targetGainDb) > std::abs(currentGain))
461 ? states_[b].belowAtkCoeff : states_[b].belowRelCoeff;
462 }
463
464 currentGain += coeff * diff;
465
466 // Refresh gain-filter coefficients every 16 samples - the gain
467 // envelope is slow enough that this granularity is inaudible.
468 // Bells use the precomputed freq/Q trig (a single pow() per
469 // refresh, F-059 performance fix); shelves run their full
470 // design, which at 1/16th rate stays negligible.
471 if ((i & 15) == 0)
472 {
473 if (std::abs(currentGain) > T(0.01))
474 {
475 switch (states_[b].cfg.shape)
476 {
477 case BandShape::Bell:
478 updateDynamicPeakCoeffs(b, currentGain);
479 break;
481 bandFilter_[b].setCoeffs(BiquadCoeffs::makeLowShelf(
482 currentFs, static_cast<double>(states_[b].cfg.frequency),
483 static_cast<double>(currentGain)));
484 break;
486 bandFilter_[b].setCoeffs(BiquadCoeffs::makeHighShelf(
487 currentFs, static_cast<double>(states_[b].cfg.frequency),
488 static_cast<double>(currentGain)));
489 break;
490 }
491 }
492 else
493 bandFilter_[b].setCoeffs(BiquadCoeffs{}); // Bypass
494 }
495
496 if ((i & 63) == 0) // Sub-sample metering update
497 meterGainDb_[b].store(currentGain, std::memory_order_relaxed);
498 }
499
500 // Apply Filters
501 for (int ch = 0; ch < nCh; ++ch)
502 {
503 T audioSample = audio.getChannel(ch)[i];
504
505 if (laSamples > 0) {
506 lookaheadBuf_[ch].push(audioSample);
507 audioSample = lookaheadBuf_[ch].read(laSamples);
508 }
509
510 for (int b = 0; b < nb; ++b) {
511 if (states_[b].cfg.enabled) {
512 audioSample = bandFilter_[b].processSample(audioSample, ch);
513 }
514 }
515 audio.getChannel(ch)[i] = audioSample;
516 }
517 }
518 }
519
520 [[nodiscard]] T computeTargetGain(const BandConfig& cfg, T levelDb) const noexcept
521 {
522 T gainDb = T(0);
523
524 if (levelDb > cfg.threshold)
525 {
526 if (cfg.aboveRatio > T(1.001)) {
527 T overDb = levelDb - cfg.threshold;
528 T amount = std::min(overDb * (T(1) - T(1) / cfg.aboveRatio), cfg.aboveRangeDb);
529 gainDb += cfg.aboveBoost ? amount : -amount;
530 }
531 }
532 else
533 {
534 if (cfg.belowRatio > T(1.001)) {
535 T underDb = cfg.threshold - levelDb;
536 T amount = std::min(underDb * (T(1) - T(1) / cfg.belowRatio), cfg.belowRangeDb);
537 gainDb += cfg.belowBoost ? amount : -amount;
538 }
539 }
540 return gainDb;
541 }
542
543 void updateBandInternalState(int b, double fs) noexcept
544 {
545 // Seqlock read of the published config (retry on a torn/in-progress
546 // write). The acquire fence between the copy and the relaxed re-read
547 // is required: an acquire LOAD only orders later accesses, so the
548 // copy could sink below the second read and a torn copy would pass
549 // the s0 == s1 check (same fix as Biquad's and FIRFilter's seqlocks).
550 BandConfig cfg;
551 unsigned s0, s1;
552 do {
553 s0 = configSeq_[b].load(std::memory_order_acquire);
554 cfg = configs_[static_cast<size_t>(b)];
555 std::atomic_thread_fence(std::memory_order_acquire);
556 s1 = configSeq_[b].load(std::memory_order_relaxed);
557 } while ((s0 & 1u) != 0u || s0 != s1);
558
559 // A band re-enabled after being disabled would replay arbitrarily old
560 // filter history and gain state: start it clean.
561 const bool wasEnabled = states_[b].cfg.enabled;
562 if (cfg.enabled && !wasEnabled)
563 {
564 bandDetector_[b].reset();
565 bandFilter_[b].reset();
566 currentGainDb_[b] = T(0);
567 }
568 states_[b].cfg = cfg;
569
570 // Detector listens where the gain filter acts: bandpass for bells,
571 // the corresponding half of the spectrum for shelves.
572 switch (cfg.shape)
573 {
574 case BandShape::Bell:
575 bandDetector_[b].setCoeffs(BiquadCoeffs::makeBandPass(fs, cfg.frequency, cfg.q));
576 break;
578 bandDetector_[b].setCoeffs(BiquadCoeffs::makeLowPass(fs, cfg.frequency, T(0.707)));
579 break;
581 bandDetector_[b].setCoeffs(BiquadCoeffs::makeHighPass(fs, cfg.frequency, T(0.707)));
582 break;
583 }
584
585 // Precompute the freq/Q-dependent peak-EQ terms ONCE per parameter change
586 // (cos w0 and alpha), so the per-block dynamic update needs only a pow().
587 // Double, like the rest of the coefficient path: the design is only
588 // ever as good as the arithmetic that builds it.
589 const double w0 = 2.0 * 3.14159265358979323846 * static_cast<double>(cfg.frequency) / fs;
590 precomputedCos_[b] = std::cos(w0);
591 precomputedAlpha_[b] = std::sin(w0) / (2.0 * std::max(static_cast<double>(cfg.q), 0.001));
592
593 auto calcCoeff = [fs](T ms) -> T {
594 const double tauSec = std::max(static_cast<double>(ms), 0.01) / 1000.0;
595 return static_cast<T>(1.0 - std::exp(-1.0 / (fs * tauSec)));
596 };
597
598 states_[b].aboveAtkCoeff = calcCoeff(cfg.aboveAttackMs);
599 states_[b].aboveRelCoeff = calcCoeff(cfg.aboveReleaseMs);
600 states_[b].belowAtkCoeff = calcCoeff(cfg.belowAttackMs);
601 states_[b].belowRelCoeff = calcCoeff(cfg.belowReleaseMs);
602 }
603
605 void updateDynamicPeakCoeffs(int b, T gainDb) noexcept
606 {
607 const double A = std::pow(10.0, static_cast<double>(gainDb) / 40.0);
608 const double cosw = precomputedCos_[b];
609 const double alpha = precomputedAlpha_[b];
610 const double a0Inv = 1.0 / (1.0 + alpha / A);
611
612 BiquadCoeffs c;
613 c.b0 = (1.0 + alpha * A) * a0Inv;
614 c.b1 = (-2.0 * cosw) * a0Inv;
615 c.b2 = (1.0 - alpha * A) * a0Inv;
616 c.a1 = (-2.0 * cosw) * a0Inv;
617 c.a2 = (1.0 - alpha / A) * a0Inv;
618 bandFilter_[b].setCoeffs(c);
619 }
620
621 void updateLookahead() noexcept
622 {
623 if (sampleRate_ > 0) {
624 int samples = static_cast<int>(sampleRate_ * oversamplingFactor_ * lookaheadMs_ / T(1000));
625 lookaheadSamples_.store(samples, std::memory_order_relaxed);
626 }
627 }
628
629 // -- State & Mem ---------------------------------------------------------
630 std::atomic<bool> isPrepared_ { false };
631 AudioSpec spec_ {};
632 double sampleRate_ = 0;
633
634 std::atomic<int> numBands_ { 0 };
635 // BandConfig (~50 bytes) is NOT lock-free as std::atomic, so publish it via a
636 // per-band seqlock instead (single producer = control thread, single consumer
637 // = audio thread). configSeq_ odd = write in progress.
638 std::array<BandConfig, MaxBands> configs_ {};
639 std::array<std::atomic<unsigned>, MaxBands> configSeq_ {};
640 std::array<std::atomic<bool>, MaxBands> paramsDirty_ {};
641 std::array<BandState, MaxBands> states_ {};
642 // Precomputed freq/Q-dependent peak-EQ trig terms (per band) so the per-block
643 // dynamic gain update needs only a pow(), not cos/sin/pow every sample.
644 std::array<double, MaxBands> precomputedCos_ {};
645 std::array<double, MaxBands> precomputedAlpha_ {};
646
647 // Biquads must span the class's full channel capacity (kMaxChannels); the
648 // default Biquad<T> is only 8 channels, which would index its per-channel state
649 // out of bounds when processing 9..16-channel (surround/immersive) audio.
650 std::array<Biquad<T, kMaxChannels>, MaxBands> bandDetector_ {};
651 std::array<Biquad<T, kMaxChannels>, MaxBands> bandFilter_ {};
652 std::array<T, MaxBands> currentGainDb_ {};
653 std::array<std::atomic<T>, MaxBands> meterGainDb_ {};
654
655 int oversamplingFactor_ = 1;
656 std::unique_ptr<Oversampling<T>> oversampler_; // audio path
657 std::unique_ptr<Oversampling<T>> oversamplerSc_; // sidechain path
658
659 T lookaheadMs_ = T(0);
660 std::atomic<int> lookaheadSamples_ { 0 };
661 std::array<RingBuffer<T>, kMaxChannels> lookaheadBuf_ {};
662};
663
664} // 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:66
std::vector< uint8_t > getState() const
Serializes bands and modes (setup/UI threads; allocates).
Definition DynamicEQ.h:310
bool setState(const uint8_t *data, size_t size)
Restores bands from a blob (tolerant; rejects foreign ids).
Definition DynamicEQ.h:354
void reset() noexcept
Definition DynamicEQ.h:294
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio in-place using self-sidechain.
Definition DynamicEQ.h:152
T getBandGainDb(int band) const noexcept
Definition DynamicEQ.h:288
int getLatency() const noexcept
Total latency in samples at the base rate.
Definition DynamicEQ.h:280
void setOversampling(int factor) noexcept
Sets the internal oversampling factor (RF-009 / ADR-011).
Definition DynamicEQ.h:256
BandShape
Shape of the dynamic gain filter (and its detector region).
Definition DynamicEQ.h:70
@ 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:205
void prepare(const AudioSpec &spec)
Initializes the dynamic EQ, allocating ring buffers and oversamplers.
Definition DynamicEQ.h:116
void setLookahead(T ms) noexcept
Sets the lookahead (0..10 ms). Applied immediately (may click); non-finite values are ignored.
Definition DynamicEQ.h:265
void setNumBands(int n) noexcept
Definition DynamicEQ.h:232
void processBlock(AudioBufferView< T > audio, AudioBufferView< T > sidechain) noexcept
Processes audio with an external sidechain.
Definition DynamicEQ.h:162
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.
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
Definition DspMath.h:78
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
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
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 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
Full configuration for a single dynamic EQ band.
Definition DynamicEQ.h:81