DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
StateVariableFilter.h
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#include "detail/TptSvf.h"
65
66#include <algorithm>
67#include <array>
68#include <cmath>
69
70namespace dspark {
71
91template <FloatType T>
93{
94public:
96 enum class Mode
97 {
98 LowPass,
99 HighPass,
100 BandPass,
101 Notch,
102 AllPass,
103 Bell,
104 LowShelf,
105 HighShelf
106 };
107
115 {
119 };
120
122
123 // -- Lifecycle --------------------------------------------------------------
124
133 void prepare(const AudioSpec& spec) noexcept
134 {
135 if (!(spec.sampleRate > 0.0)) return;
136 spec_ = spec;
137 updateCoefficients();
138 reset();
139 }
140
145 void processBlock(AudioBufferView<T> buffer) noexcept
146 {
147 DenormalGuard guard;
148 const int nCh = std::min(buffer.getNumChannels(), kMaxChannels);
149 const int nS = buffer.getNumSamples();
150
151 // Channel-outer / sample-inner: keeps one channel's state and buffer hot
152 // in cache for the whole inner loop (cache-friendly, matches LadderFilter).
153 for (int ch = 0; ch < nCh; ++ch)
154 {
155 T* data = buffer.getChannel(ch);
156 for (int i = 0; i < nS; ++i)
157 data[i] = processOne(data[i], ch);
158 }
159 }
160
177 [[nodiscard]] T processSample(T input, int channel) noexcept
178 {
179 if (channel < 0 || channel >= kMaxChannels) return input;
180 return processOne(input, channel);
181 }
182
195 [[nodiscard]] MultiOutput processMultiOutput(T input, int channel) noexcept
196 {
197 if (channel < 0 || channel >= kMaxChannels) return { input, T(0), T(0) };
198 return processCore(input, channel);
199 }
200
202 void reset() noexcept
203 {
204 for (auto& s : state_)
205 {
206 s.ic1eq = T(0);
207 s.ic2eq = T(0);
208 }
209 }
210
211 // -- Parameters (Level 1) ---------------------------------------------------
212
224 void setCutoff(T hz) noexcept
225 {
226 if (!std::isfinite(hz)) return;
227 cutoff_ = std::max(hz, T(0));
228 updateCoefficients();
229 }
230
238 void setResonance(T resonance) noexcept
239 {
240 if (!std::isfinite(resonance)) return; // NaN would poison R_
241 resonance_ = std::clamp(resonance, T(0), T(1));
242 // Map 0-1 to Q: 0.5 (wide) to 50 (near self-osc). R = 1/(2*Q).
243 const T Q = T(0.5) + resonance_ * T(49.5);
244 R_ = T(1) / (T(2) * Q);
245 updateBellR();
246 updateDerivedCoeffs();
247 }
248
258 void setQ(T q) noexcept
259 {
260 if (!std::isfinite(q)) return; // NaN would poison R_
261 q = std::max(q, T(0.01));
262 R_ = T(1) / (T(2) * q);
263 resonance_ = std::clamp((q - T(0.5)) / T(49.5), T(0), T(1));
264 updateBellR();
265 updateDerivedCoeffs();
266 }
267
269 void setMode(Mode mode) noexcept
270 {
271 mode_ = mode;
272 updateDerivedCoeffs();
273 }
274
279 void setGain(T dB) noexcept
280 {
281 if (!std::isfinite(dB)) return; // NaN/Inf would poison A_
282 gainDb_ = dB;
283 A_ = std::pow(T(10), std::abs(dB) / T(40)); // sqrt(linear gain)
284 updateBellR();
285 updateDerivedCoeffs();
286 }
287
288 // -- Getters ----------------------------------------------------------------
289
290 [[nodiscard]] T getCutoff() const noexcept { return cutoff_; }
291 [[nodiscard]] T getResonance() const noexcept { return resonance_; }
292 [[nodiscard]] T getQ() const noexcept { return T(1) / (T(2) * R_); }
293 [[nodiscard]] T getGain() const noexcept { return gainDb_; }
294 [[nodiscard]] Mode getMode() const noexcept { return mode_; }
295
296protected:
297 static constexpr int kMaxChannels = 16;
298
300
301 std::array<ChannelState, kMaxChannels> state_ {};
303 T cutoff_ = T(1000);
304 T resonance_ = T(0);
305 T gainDb_ = T(0);
306 T g_ = T(0); // tan(pi * fc / fs) - TPT coefficient
307 T R_ = T(1); // 1/(2*Q) - damping
308 T Rbell_ = T(1); // Mode-specific R for Bell (Zavalishin gain-dependent damping)
309 T A_ = T(1); // sqrt(gain) for shelving/bell
311
312 // Cached derived coefficients (recomputed by updateDerivedCoeffs()).
313 T effG_ = T(0); // possibly shelf-prewarped g
314 T effR_ = T(1); // mode-effective damping (R_ or Rbell_)
315 T a1_ = T(1), a2_ = T(0), a3_ = T(0);
316
317private:
325 void updateCoefficients() noexcept
326 {
327 if (spec_.sampleRate > 0)
328 {
329 cutoff_ = std::clamp(cutoff_, T(20),
330 static_cast<T>(spec_.sampleRate) * T(0.499));
331 g_ = static_cast<T>(std::tan(pi<double> * static_cast<double>(cutoff_)
332 / spec_.sampleRate));
333 }
334 updateBellR();
335 updateDerivedCoeffs();
336 }
337
339 void updateBellR() noexcept
340 {
341 // Zavalishin Bell EQ topology: modify damping by gain factor A
342 // (A_ >= 1 always - setGain uses |dB| and rejects non-finite input).
343 // Boost: decrease R (increase Q) => narrower peak compensates for gain spread
344 // Cut: increase R (decrease Q) => wider notch compensates for gain spread
345 if (gainDb_ >= T(0))
346 Rbell_ = R_ / A_;
347 else
348 Rbell_ = R_ * A_;
349 }
350
362 void updateDerivedCoeffs() noexcept
363 {
364 effR_ = (mode_ == Mode::Bell) ? Rbell_ : R_;
365
366 T gEff = g_;
368 {
369 const T sqrtAs = std::sqrt((gainDb_ >= T(0)) ? A_ : T(1) / A_);
370 gEff = (mode_ == Mode::LowShelf) ? g_ / sqrtAs : g_ * sqrtAs;
371 }
372
373 effG_ = gEff;
374 a1_ = T(1) / (T(1) + T(2) * effR_ * gEff + gEff * gEff);
375 a2_ = gEff * a1_;
376 a3_ = gEff * a2_;
377 }
378
380 [[nodiscard]] T processOne(T input, int channel) noexcept
381 {
382 const MultiOutput m = processCore(input, channel);
383 return selectOutput(input, m.lowpass, m.highpass, m.bandpass);
384 }
385
399 [[nodiscard]] MultiOutput processCore(T input, int channel) noexcept
400 {
401 const auto output = detail::tptSvfStep(input, state_[channel], a1_, a2_, a3_);
402 return { output.lowpass, input - T(2) * effR_ * output.bandpass - output.lowpass,
403 output.bandpass };
404 }
405
406 [[nodiscard]] T selectOutput(T input, T lp, T hp, T bp) const noexcept
407 {
408 switch (mode_)
409 {
410 case Mode::LowPass: return lp;
411 case Mode::HighPass: return hp;
412 case Mode::BandPass: return bp;
413 case Mode::Notch: return lp + hp; // LP + HP = Notch
414 case Mode::AllPass: return lp + hp - T(2) * R_ * bp; // HP - 2R*BP + LP
415 case Mode::Bell:
416 {
417 // Bell EQ (Zavalishin ch. 4): bandwidth-correct topology.
418 // processCore already used Rbell_ for the SVF damping,
419 // so the BP bandwidth is correct. Apply gain via mixing:
420 // boost: output = input + (A^2 - 1) * 2*Rbell * BP
421 // cut: output = input + (1/A^2 - 1) * 2*Rbell * BP
422 // At the centre frequency 2*Rbell*bp == input, so the mix factor
423 // must be (A^2 - 1) to reach gain A^2 (boost) and (1/A^2 - 1) to
424 // reach gain 1/A^2 (cut). The cut term must be NEGATIVE - the old
425 // (1 - 1/A^2) was positive and boosted instead of cutting.
426 const T k = T(2) * Rbell_;
427 return (gainDb_ >= T(0))
428 ? input + (A_ * A_ - T(1)) * k * bp
429 : input + (T(1) / (A_ * A_) - T(1)) * k * bp;
430 }
431 case Mode::LowShelf:
432 {
433 // Canonical Simper low shelf. With As = A_ (boost) or 1/A_
434 // (cut), where A_ = 10^(|dB|/40) = sqrt(linear gain), and the
435 // pre-warped g from updateDerivedCoeffs():
436 // out = As^2 * LP + 2R * As * BP + HP
437 // DC gain = As^2 (= linear gain), Nyquist gain = 1, and the
438 // half-gain point sits exactly on the nominal frequency.
439 const T As = (gainDb_ >= T(0)) ? A_ : T(1) / A_;
440 const T twoR = T(2) * R_;
441 return As * As * lp + As * twoR * bp + hp;
442 }
443 case Mode::HighShelf:
444 {
445 // Canonical Simper high shelf (mirror of the low shelf):
446 // out = LP + 2R * As * BP + As^2 * HP
447 const T As = (gainDb_ >= T(0)) ? A_ : T(1) / A_;
448 const T twoR = T(2) * R_;
449 return lp + As * twoR * bp + As * As * hp;
450 }
451 }
452 return lp;
453 }
454};
455
456} // namespace dspark
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.
TptSvfOutput< T > tptSvfStep(T input, TptSvfState< T > &state, T a1, T a2, T a3) noexcept
Advances the expanded Simper TPT recurrence by one sample.
Definition TptSvf.h:43
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
Result struct for simultaneous multi-output processing.
Equivalent-current memories of the two trapezoidal integrators.
Definition TptSvf.h:20