Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
svf.hpp
Go to the documentation of this file.
1// sbl/dsp/widgets/svf.hpp — State Variable Filter (Audio Stack — Widgets)
2//
3// Zero-delay feedback (ZDF) SVF based on Andrew Simper's topology, as
4// documented in Vadim Zavalishin's "The Art of VA Filter Design."
5// Reference implementation: Mutable Instruments stmlib/dsp/filter.h (MIT).
6//
7// Unconditionally stable at all cutoff and resonance settings — no clamping
8// needed, no oversampling required. This replaces the previous Chamberlin
9// SVF which was unstable at high cutoff + resonance near Nyquist.
10//
11// Uses float state variables — STM32H7 (Cortex-M7) has hardware FPU with
12// single-cycle float ops.
13//
14// Signal Unit API (FDP-052):
15// Widgets expose normalized [0,1] inputs via _su() methods. The widget
16// owns internal mapping (exp curve for cutoff, linear for resonance) and
17// parameter conditioning (CriticalDamp smoothing — no overshoot, zero-
18// velocity arrival). Applications pass normalized pot/CV values directly.
19//
20// Usage:
21// sbl::dsp::widgets::Svf filter;
22// filter.set_cutoff_su(0.5f); // [0,1] → 20–20000 Hz (exp mapped)
23// filter.set_resonance_su(0.7f); // [0,1] → internally mapped
24// filter.set_mode(FilterMode::LowPass);
25// filter.process(buf, frames); // per-sample smoothed coefficients
26//
27// process() with optional modulation parameters provides per-sample exponential
28// cutoff modulation from a normalized signal [-1, 1]. Uses exp_mod() internally.
29// See FDP-031 for architecture rationale.
30
31#ifndef SBL_DSP_WIDGETS_SVF_HPP_
32#define SBL_DSP_WIDGETS_SVF_HPP_
33
34#include <cstdint>
35
41
42namespace sbl::dsp::widgets {
43
44enum class FilterMode : uint8_t {
45 LowPass,
48 Notch,
49};
50
51class Svf {
52public:
53 /// @note All public methods are ISR-safe — bounded computation, no I/O.
54
55 // === Frequency range constants (for test helpers) ===
56 static constexpr float kMinFreqHz = 20.0f;
57 static constexpr float kMaxFreqHz = 20000.0f;
58 static constexpr float kCutoffOctaves = 9.965784f; // log2(20000/20)
59
60 // === Signal Unit API (FDP-052) ===
61
62 /**
63 * @brief Set cutoff frequency from normalized [0,1] signal unit
64 * @param su Unipolar [0,1] — exponentially mapped to 20–20000 Hz
65 *
66 * First call auto-initializes (no ramp from 0). Subsequent calls
67 * smooth via CriticalDamp (30ms settling, no overshoot).
68 */
69 void set_cutoff_su(float su) {
70 su = math::clamp01(su);
72 target_freq_hz_ = hz;
73 ensure_smoothers();
74 if (!cutoff_init_) {
75 cd_cutoff_.reset(hz);
76 apply_cutoff(hz);
77 cutoff_init_ = true;
78 }
79 }
80
81 /**
82 * @brief Set resonance from normalized [0,1] signal unit
83 * @param su Unipolar [0,1] — 0 = flat, 1 = self-oscillation
84 *
85 * First call auto-initializes. Subsequent calls smooth via
86 * CriticalDamp (20ms settling, no overshoot).
87 */
88 void set_resonance_su(float su) {
89 su = math::clamp01(su);
90 target_q_ = su;
91 ensure_smoothers();
92 if (!reso_init_) {
93 cd_resonance_.reset(su);
94 apply_resonance(su);
95 reso_init_ = true;
96 }
97 }
98
99 /**
100 * @brief Set resonance compensation strength
101 *
102 * Controls how much the input signal is attenuated as resonance
103 * increases: gain = 1 / (1 + q * k). This prevents the resonant
104 * peak from clipping during filter sweeps.
105 *
106 * k = 0 No compensation (default)
107 * k = 2 Gentle drop (~33% volume at full resonance)
108 * k = 4 Strong drop (~20% volume at full resonance)
109 *
110 * @param k Compensation amount (0.0 = off)
111 */
113 comp_k_ = k;
114 update_comp_gain();
115 }
116
117 /** @brief Set filter output mode */
118 void set_mode(FilterMode mode) { mode_ = mode; }
119
120 /**
121 * @brief Process a block of audio samples in-place
122 *
123 * ZDF SVF with per-sample CriticalDamp coefficient smoothing.
124 * When cutoff_mod is provided, exponential modulation is applied
125 * around the smoothed cutoff center per-sample.
126 *
127 * @param buf Float audio buffer (modified in-place)
128 * @param frames Number of samples
129 * @param cutoff_mod Cutoff modulation signal [-1,1] (nullptr = no modulation)
130 * @param mod_depth Modulation depth in octaves (e.g., 2.0 = ±2 octaves)
131 */
132 void process(float* buf, uint16_t frames,
133 const float* cutoff_mod = nullptr, float mod_depth = 0.0f) {
134 ensure_smoothers();
135 float freq_target = target_freq_hz_;
136 float q_target = target_q_;
137 float sr = types::SAMPLE_RATE_F;
138 float ck = comp_k_;
139 float s1 = state_1_, s2 = state_2_;
140
141 for (uint16_t i = 0; i < frames; ++i) {
142 // Advance coefficient smoothers
143 float center = cd_cutoff_.process(freq_target);
144 float q = cd_resonance_.process(q_target);
145
146 // Apply modulation if provided
147 float freq = (cutoff_mod != nullptr)
148 ? math::exp_mod(center, cutoff_mod[i], mod_depth,
150 : center;
151
152 // Compute ZDF coefficients
153 float f = freq / sr;
154 if (f > 0.497f) f = 0.497f;
155 float g = math::fast_tan_pif(f);
156 float r = (1.0f - q) * 2.0f;
157 float h = 1.0f / (1.0f + r * g + g * g);
158
159 // Resonance compensation
160 float comp = (ck > 0.0f) ? 1.0f / (1.0f + q * ck) : 1.0f;
161
162 // ZDF SVF tick
163 float x = buf[i] * comp;
164 float hp = (x - r * s1 - g * s1 - s2) * h;
165 float bp = g * hp + s1;
166 s1 = g * hp + bp;
167 float lp = g * bp + s2;
168 s2 = g * bp + lp;
169
170 switch (mode_) {
171 case FilterMode::LowPass: buf[i] = lp; break;
172 case FilterMode::HighPass: buf[i] = hp; break;
173 case FilterMode::BandPass: buf[i] = bp; break;
174 case FilterMode::Notch: buf[i] = lp + hp; break;
175 default: buf[i] = lp; break;
176 }
177 }
178 state_1_ = s1;
179 state_2_ = s2;
180
181 // Sync cached coefficients with final smoothed state
182 sync_cached_coefficients();
183 }
184
185 /** @brief Reset filter state (audio state only, not parameter smoothers) */
186 void reset() {
187 state_1_ = 0.0f;
188 state_2_ = 0.0f;
189 }
190
191private:
192 // Test-only access to engineering-unit setters
193 friend struct SvfTestAccess;
194
195 /** @brief Set cutoff in Hz — immediate, no smoothing (private) */
196 void set_cutoff(float freq_hz) {
197 target_freq_hz_ = freq_hz;
198 cd_cutoff_.reset(freq_hz);
199 apply_cutoff(freq_hz);
200 cutoff_init_ = true;
201 }
202
203 /** @brief Set resonance 0–1 — immediate, no smoothing (private) */
204 void set_resonance(float q) {
205 target_q_ = q;
206 cd_resonance_.reset(q);
207 apply_resonance(q);
208 reso_init_ = true;
209 }
210
211 void apply_cutoff(float freq_hz) {
212 float f = freq_hz / types::SAMPLE_RATE_F;
213 if (f > 0.497f) f = 0.497f;
214 g_ = math::fast_tan_pif(f);
215 h_ = 1.0f / (1.0f + r_ * g_ + g_ * g_);
216 }
217
218 void apply_resonance(float q) {
219 q_ = q;
220 r_ = (1.0f - q) * 2.0f;
221 h_ = 1.0f / (1.0f + r_ * g_ + g_ * g_);
222 update_comp_gain();
223 }
224
225 void update_comp_gain() {
226 comp_gain_ = (comp_k_ > 0.0f)
227 ? 1.0f / (1.0f + q_ * comp_k_)
228 : 1.0f;
229 }
230
231 void ensure_smoothers() {
232 if (!smoothers_configured_) {
233 cd_cutoff_.set_time_ms(30.0f, types::SAMPLE_RATE_F);
234 cd_resonance_.set_time_ms(20.0f, types::SAMPLE_RATE_F);
235 cd_cutoff_.reset(target_freq_hz_);
236 cd_resonance_.reset(target_q_);
237 smoothers_configured_ = true;
238 }
239 }
240
241 void sync_cached_coefficients() {
242 q_ = cd_resonance_.value();
243 r_ = (1.0f - q_) * 2.0f;
244 float f = cd_cutoff_.value() / types::SAMPLE_RATE_F;
245 if (f > 0.497f) f = 0.497f;
246 g_ = math::fast_tan_pif(f);
247 h_ = 1.0f / (1.0f + r_ * g_ + g_ * g_);
248 comp_gain_ = (comp_k_ > 0.0f) ? 1.0f / (1.0f + q_ * comp_k_) : 1.0f;
249 }
250
251 // ZDF coefficients (cached for modulated process fallback)
252 float g_ = 0.0f; // tan(π·f) — frequency coefficient
253 float r_ = 1.0f; // Damping (2.0 = flat, 0.0 = self-oscillation)
254 float h_ = 0.0f; // 1 / (1 + r·g + g²)
255 float q_ = 0.0f; // Resonance (0.0–1.0), cached for compensation
256 float comp_k_ = 0.0f; // Compensation strength (0 = off)
257 float comp_gain_ = 1.0f; // Precomputed: 1 / (1 + q * k)
258 float state_1_ = 0.0f;
259 float state_2_ = 0.0f;
261
262 // CriticalDamp smoothers for _su() inputs (FDP-052)
263 primitives::CriticalDamp cd_cutoff_;
264 primitives::CriticalDamp cd_resonance_;
265 float target_freq_hz_ = 0.0f;
266 float target_q_ = 0.0f;
267 bool cutoff_init_ = false;
268 bool reso_init_ = false;
269 bool smoothers_configured_ = false;
270};
271
272} // namespace sbl::dsp::widgets
273
274#endif // SBL_DSP_WIDGETS_SVF_HPP_
Clamping (Cross-cutting — Math)
float value() const
Current output value.
void set_time_ms(float ms, float rate_hz)
Set settling time in milliseconds.
float process(float target)
Process one sample toward target.
static constexpr float kMinFreqHz
Definition svf.hpp:56
static constexpr float kMaxFreqHz
Definition svf.hpp:57
static constexpr float kCutoffOctaves
Definition svf.hpp:58
void set_mode(FilterMode mode)
Set filter output mode.
Definition svf.hpp:118
void set_resonance_su(float su)
Set resonance from normalized [0,1] signal unit.
Definition svf.hpp:88
void set_resonance_compensation(float k)
Set resonance compensation strength.
Definition svf.hpp:112
void reset()
Reset filter state (audio state only, not parameter smoothers)
Definition svf.hpp:186
friend struct SvfTestAccess
Definition svf.hpp:193
void set_cutoff_su(float su)
Set cutoff frequency from normalized [0,1] signal unit.
Definition svf.hpp:69
void process(float *buf, uint16_t frames, const float *cutoff_mod=nullptr, float mod_depth=0.0f)
Process a block of audio samples in-place.
Definition svf.hpp:132
Critically damped 2nd-order parameter smoother.
Exponential modulation converter (Signal layer)
Fast analytical approximations (Audio Stack — Atoms)
Fixed-point constants and audio sample types.
constexpr float clamp01(float x)
x held to [0, 1]; NaN → 0.
Definition clamp.hpp:18
float exp_mod(float base, float mod, float depth, float lo=20.0f, float hi=20000.0f)
Definition exp_mod.hpp:50
float fast_tan_pif(float f)
Definition fast_math.hpp:71
float su_to_hz(float su, float min_hz, float octaves)
float SAMPLE_RATE_F
Definition fixed.hpp:17
Black boxes with _su ports.
Definition channel.hpp:49