DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
StateVariableFilter.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
60#include "DspMath.h"
61#include "AudioSpec.h"
62#include "AudioBuffer.h"
63#include "DenormalGuard.h"
64
65#include <algorithm>
66#include <array>
67#include <cmath>
68
69namespace dspark {
70
90template <FloatType T>
92{
93public:
95 enum class Mode
96 {
97 LowPass,
98 HighPass,
99 BandPass,
100 Notch,
101 AllPass,
102 Bell,
103 LowShelf,
104 HighShelf
105 };
106
114 {
118 };
119
121
122 // -- Lifecycle --------------------------------------------------------------
123
132 void prepare(const AudioSpec& spec) noexcept
133 {
134 if (!(spec.sampleRate > 0.0)) return;
135 spec_ = spec;
136 updateCoefficients();
137 reset();
138 }
139
144 void processBlock(AudioBufferView<T> buffer) noexcept
145 {
146 DenormalGuard guard;
147 const int nCh = std::min(buffer.getNumChannels(), kMaxChannels);
148 const int nS = buffer.getNumSamples();
149
150 // Channel-outer / sample-inner: keeps one channel's state and buffer hot
151 // in cache for the whole inner loop (cache-friendly, matches LadderFilter).
152 for (int ch = 0; ch < nCh; ++ch)
153 {
154 T* data = buffer.getChannel(ch);
155 for (int i = 0; i < nS; ++i)
156 data[i] = processOne(data[i], ch);
157 }
158 }
159
176 [[nodiscard]] T processSample(T input, int channel) noexcept
177 {
178 if (channel < 0 || channel >= kMaxChannels) return input;
179 return processOne(input, channel);
180 }
181
194 [[nodiscard]] MultiOutput processMultiOutput(T input, int channel) noexcept
195 {
196 if (channel < 0 || channel >= kMaxChannels) return { input, T(0), T(0) };
197 return processCore(input, channel);
198 }
199
201 void reset() noexcept
202 {
203 for (auto& s : state_)
204 {
205 s.ic1eq = T(0);
206 s.ic2eq = T(0);
207 }
208 }
209
210 // -- Parameters (Level 1) ---------------------------------------------------
211
223 void setCutoff(T hz) noexcept
224 {
225 if (!std::isfinite(hz)) return;
226 cutoff_ = std::max(hz, T(0));
227 updateCoefficients();
228 }
229
237 void setResonance(T resonance) noexcept
238 {
239 if (!std::isfinite(resonance)) return; // NaN would poison R_
240 resonance_ = std::clamp(resonance, T(0), T(1));
241 // Map 0-1 to Q: 0.5 (wide) to 50 (near self-osc). R = 1/(2*Q).
242 const T Q = T(0.5) + resonance_ * T(49.5);
243 R_ = T(1) / (T(2) * Q);
244 updateBellR();
245 updateDerivedCoeffs();
246 }
247
257 void setQ(T q) noexcept
258 {
259 if (!std::isfinite(q)) return; // NaN would poison R_
260 q = std::max(q, T(0.01));
261 R_ = T(1) / (T(2) * q);
262 resonance_ = std::clamp((q - T(0.5)) / T(49.5), T(0), T(1));
263 updateBellR();
264 updateDerivedCoeffs();
265 }
266
268 void setMode(Mode mode) noexcept
269 {
270 mode_ = mode;
271 updateDerivedCoeffs();
272 }
273
278 void setGain(T dB) noexcept
279 {
280 if (!std::isfinite(dB)) return; // NaN/Inf would poison A_
281 gainDb_ = dB;
282 A_ = std::pow(T(10), std::abs(dB) / T(40)); // sqrt(linear gain)
283 updateBellR();
284 updateDerivedCoeffs();
285 }
286
287 // -- Getters ----------------------------------------------------------------
288
289 [[nodiscard]] T getCutoff() const noexcept { return cutoff_; }
290 [[nodiscard]] T getResonance() const noexcept { return resonance_; }
291 [[nodiscard]] T getQ() const noexcept { return T(1) / (T(2) * R_); }
292 [[nodiscard]] T getGain() const noexcept { return gainDb_; }
293 [[nodiscard]] Mode getMode() const noexcept { return mode_; }
294
295protected:
296 static constexpr int kMaxChannels = 16;
297
299 {
300 T ic1eq = T(0);
301 T ic2eq = T(0);
302 };
303
304 std::array<ChannelState, kMaxChannels> state_ {};
306 T cutoff_ = T(1000);
307 T resonance_ = T(0);
308 T gainDb_ = T(0);
309 T g_ = T(0); // tan(pi * fc / fs) - TPT coefficient
310 T R_ = T(1); // 1/(2*Q) - damping
311 T Rbell_ = T(1); // Mode-specific R for Bell (Zavalishin gain-dependent damping)
312 T A_ = T(1); // sqrt(gain) for shelving/bell
314
315 // Cached derived coefficients (recomputed by updateDerivedCoeffs()).
316 T effG_ = T(0); // possibly shelf-prewarped g
317 T effR_ = T(1); // mode-effective damping (R_ or Rbell_)
318 T a1_ = T(1), a2_ = T(0), a3_ = T(0);
319
320private:
328 void updateCoefficients() noexcept
329 {
330 if (spec_.sampleRate > 0)
331 {
332 cutoff_ = std::clamp(cutoff_, T(20),
333 static_cast<T>(spec_.sampleRate) * T(0.499));
334 g_ = static_cast<T>(std::tan(pi<double> * static_cast<double>(cutoff_)
335 / spec_.sampleRate));
336 }
337 updateBellR();
338 updateDerivedCoeffs();
339 }
340
342 void updateBellR() noexcept
343 {
344 // Zavalishin Bell EQ topology: modify damping by gain factor A
345 // (A_ >= 1 always - setGain uses |dB| and rejects non-finite input).
346 // Boost: decrease R (increase Q) => narrower peak compensates for gain spread
347 // Cut: increase R (decrease Q) => wider notch compensates for gain spread
348 if (gainDb_ >= T(0))
349 Rbell_ = R_ / A_;
350 else
351 Rbell_ = R_ * A_;
352 }
353
365 void updateDerivedCoeffs() noexcept
366 {
367 effR_ = (mode_ == Mode::Bell) ? Rbell_ : R_;
368
369 T gEff = g_;
371 {
372 const T sqrtAs = std::sqrt((gainDb_ >= T(0)) ? A_ : T(1) / A_);
373 gEff = (mode_ == Mode::LowShelf) ? g_ / sqrtAs : g_ * sqrtAs;
374 }
375
376 effG_ = gEff;
377 a1_ = T(1) / (T(1) + T(2) * effR_ * gEff + gEff * gEff);
378 a2_ = gEff * a1_;
379 a3_ = gEff * a2_;
380 }
381
383 [[nodiscard]] T processOne(T input, int channel) noexcept
384 {
385 const MultiOutput m = processCore(input, channel);
386 return selectOutput(input, m.lowpass, m.highpass, m.bandpass);
387 }
388
402 [[nodiscard]] MultiOutput processCore(T input, int channel) noexcept
403 {
404 auto& s = state_[channel];
405
406 const T v3 = input - s.ic2eq;
407 const T v1 = a1_ * s.ic1eq + a2_ * v3;
408 const T v2 = s.ic2eq + a2_ * s.ic1eq + a3_ * v3;
409
410 s.ic1eq = T(2) * v1 - s.ic1eq;
411 s.ic2eq = T(2) * v2 - s.ic2eq;
412
413 return { v2, input - T(2) * effR_ * v1 - v2, v1 };
414 }
415
416 [[nodiscard]] T selectOutput(T input, T lp, T hp, T bp) const noexcept
417 {
418 switch (mode_)
419 {
420 case Mode::LowPass: return lp;
421 case Mode::HighPass: return hp;
422 case Mode::BandPass: return bp;
423 case Mode::Notch: return lp + hp; // LP + HP = Notch
424 case Mode::AllPass: return lp + hp - T(2) * R_ * bp; // HP - 2R*BP + LP
425 case Mode::Bell:
426 {
427 // Bell EQ (Zavalishin ch. 4): bandwidth-correct topology.
428 // processCore already used Rbell_ for the SVF damping,
429 // so the BP bandwidth is correct. Apply gain via mixing:
430 // boost: output = input + (A^2 - 1) * 2*Rbell * BP
431 // cut: output = input + (1/A^2 - 1) * 2*Rbell * BP
432 // At the centre frequency 2*Rbell*bp == input, so the mix factor
433 // must be (A^2 - 1) to reach gain A^2 (boost) and (1/A^2 - 1) to
434 // reach gain 1/A^2 (cut). The cut term must be NEGATIVE - the old
435 // (1 - 1/A^2) was positive and boosted instead of cutting.
436 const T k = T(2) * Rbell_;
437 return (gainDb_ >= T(0))
438 ? input + (A_ * A_ - T(1)) * k * bp
439 : input + (T(1) / (A_ * A_) - T(1)) * k * bp;
440 }
441 case Mode::LowShelf:
442 {
443 // Canonical Simper low shelf. With As = A_ (boost) or 1/A_
444 // (cut), where A_ = 10^(|dB|/40) = sqrt(linear gain), and the
445 // pre-warped g from updateDerivedCoeffs():
446 // out = As^2 * LP + 2R * As * BP + HP
447 // DC gain = As^2 (= linear gain), Nyquist gain = 1, and the
448 // half-gain point sits exactly on the nominal frequency.
449 const T As = (gainDb_ >= T(0)) ? A_ : T(1) / A_;
450 const T twoR = T(2) * R_;
451 return As * As * lp + As * twoR * bp + hp;
452 }
453 case Mode::HighShelf:
454 {
455 // Canonical Simper high shelf (mirror of the low shelf):
456 // out = LP + 2R * As * BP + As^2 * HP
457 const T As = (gainDb_ >= T(0)) ? A_ : T(1) / A_;
458 const T twoR = T(2) * R_;
459 return lp + As * twoR * bp + As * As * hp;
460 }
461 }
462 return lp;
463 }
464};
465
466} // namespace dspark
Owning audio buffer and non-owning view for real-time DSP processing.
Describes the audio processing environment (sample rate, block size, channels).
RAII scope guard that disables denormal (subnormal) float arithmetic.
Core mathematical utilities for digital signal processing.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
TPT State Variable Filter with simultaneous multi-output.
void setResonance(T resonance) noexcept
Sets the resonance amount.
void setCutoff(T hz) noexcept
Sets the cutoff/center frequency.
std::array< ChannelState, kMaxChannels > state_
void setQ(T q) noexcept
Sets the Q factor directly.
void reset() noexcept
Resets internal state.
void setGain(T dB) noexcept
Sets gain for Bell/Shelf modes (in dB).
static constexpr int kMaxChannels
void setMode(Mode mode) noexcept
Sets the output mode.
MultiOutput processMultiOutput(T input, int channel) noexcept
Processes a single sample returning LP, HP, BP simultaneously.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place using the selected Mode.
Mode
Filter output mode for processBlock/processSample.
@ LowShelf
Low shelf (boost/cut below frequency).
@ BandPass
Bandpass (constant skirt gain).
@ AllPass
Allpass (phase shift, unity gain).
@ LowPass
2nd-order lowpass (12 dB/oct).
@ Bell
Parametric bell (boost/cut at frequency).
@ Notch
Band-reject (notch).
@ HighShelf
High shelf (boost/cut above frequency).
@ HighPass
2nd-order highpass (12 dB/oct).
T processSample(T input, int channel) noexcept
Processes a single sample (selected Mode output).
void prepare(const AudioSpec &spec) noexcept
Prepares the filter.
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
Result struct for simultaneous multi-output processing.