Sound Byte Libs 29c5ff3
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/widgets/proc/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. Fixed-point variant for M0+ is future work.
13//
14// Resonance compensation: optional input gain reduction as resonance
15// increases, preventing output clipping during filter sweeps. This is
16// the "volume drop" characteristic of classic analog filters (SH-101,
17// MS-20). Disabled by default (k=0) for backward compatibility.
18//
19// process_modulated() provides per-sample exponential cutoff modulation
20// from a normalized signal [-1, 1]. This eliminates the manual per-sample
21// coefficient loop that every LFO→filter instrument must write. Uses
22// sbl::signal::exp_mod() internally for perceptually correct scaling.
23// See FDP-031 for architecture rationale, RPT-014 for the triggering bug.
24//
25// Usage:
26// sbl::widgets::proc::Svf filter;
27// filter.set_sample_rate(48000);
28// filter.set_cutoff(1000.0f); // 1 kHz
29// filter.set_resonance(0.75f); // moderate resonance (0.0 = flat, 1.0 = max)
30// filter.set_resonance_compensation(3.0f); // analog-style volume drop
31// filter.set_mode(FilterMode::LowPass);
32// filter.process(buf, frames);
33//
34// // Per-sample modulated filtering (LFO → cutoff):
35// lfo.process(lfo_buf, frames);
36// filter.process_modulated(buf, frames, lfo_buf, 1000.0f, 2.0f); // ±2 oct
37
38#pragma once
39
40#include <cstdint>
41
42#include <sbl/dsp/fast_math.hpp>
44
45namespace sbl::widgets::proc {
46
47enum class FilterMode : uint8_t {
48 LowPass,
51 Notch,
52};
53
54class Svf {
55public:
56 /// @note All public methods are ISR-safe — bounded computation, no I/O.
57
58 void set_sample_rate(uint32_t sr) { sample_rate_ = sr; }
59
60 /**
61 * @brief Set cutoff frequency in Hz
62 * @param freq_hz Cutoff in Hz (e.g., 1000.0f)
63 */
64 void set_cutoff(float freq_hz) {
65 float f = freq_hz / static_cast<float>(sample_rate_);
66 if (f > 0.497f) f = 0.497f;
67 g_ = dsp::fast_tan_pif(f);
68 h_ = 1.0f / (1.0f + r_ * g_ + g_ * g_);
69 }
70
71 /**
72 * @brief Set resonance
73 * @param q 0.0 = no resonance (flat), 1.0 = maximum resonance
74 */
75 void set_resonance(float q) {
76 // Map float to damping: 0.0 → 2.0 (flat), 1.0 → 0.0 (self-oscillation)
77 q_ = q;
78 r_ = (1.0f - q) * 2.0f;
79 h_ = 1.0f / (1.0f + r_ * g_ + g_ * g_);
80 update_comp_gain();
81 }
82
83 /**
84 * @brief Set resonance compensation strength
85 *
86 * Controls how much the input signal is attenuated as resonance
87 * increases: gain = 1 / (1 + q * k). This prevents the resonant
88 * peak from clipping during filter sweeps.
89 *
90 * k = 0 No compensation (default, backward compatible)
91 * k = 2 Gentle drop (~33% volume at full resonance)
92 * k = 4 Strong drop (~20% volume at full resonance)
93 *
94 * @param k Compensation amount (0.0 = off)
95 */
97 comp_k_ = k;
98 update_comp_gain();
99 }
100
101 /** @brief Set filter output mode */
102 void set_mode(FilterMode mode) { mode_ = mode; }
103
104 /**
105 * @brief Process a block of audio samples in-place
106 *
107 * ZDF SVF — unconditionally stable, no oversampling needed.
108 *
109 * @param buf Float audio buffer (modified in-place, [-1.0, 1.0])
110 * @param frames Number of samples
111 */
112 void process(float* buf, uint16_t frames) {
113 float g = g_, r = r_, h = h_;
114 float comp = comp_gain_;
115 float s1 = state_1_, s2 = state_2_;
116
117 for (uint16_t i = 0; i < frames; ++i) {
118 float x = buf[i] * comp;
119 float hp = (x - r * s1 - g * s1 - s2) * h;
120 float bp = g * hp + s1;
121 s1 = g * hp + bp;
122 float lp = g * bp + s2;
123 s2 = g * bp + lp;
124
125 switch (mode_) {
126 case FilterMode::LowPass: buf[i] = lp; break;
127 case FilterMode::HighPass: buf[i] = hp; break;
128 case FilterMode::BandPass: buf[i] = bp; break;
129 case FilterMode::Notch: buf[i] = lp + hp; break;
130 default: buf[i] = lp; break;
131 }
132 }
133 state_1_ = s1;
134 state_2_ = s2;
135 }
136
137 /**
138 * @brief Process with per-sample exponential cutoff modulation
139 *
140 * Applies exponential modulation to the cutoff frequency per sample,
141 * recomputing ZDF coefficients each sample. The modulation signal is
142 * normalized [-1, 1] and depth is in octaves — perceptually uniform
143 * regardless of the center frequency.
144 *
145 * This replaces the manual per-sample loop pattern:
146 * for (i = 0; i < n; ++i) {
147 * filter.set_cutoff(cutoff + lfo[i]); // WRONG: linear, broken
148 * filter.process(&buf[i], 1);
149 * }
150 *
151 * With:
152 * filter.process_modulated(buf, n, lfo, cutoff, 2.0f); // ±2 octaves
153 *
154 * @param buf Float audio buffer (modified in-place)
155 * @param frames Number of samples
156 * @param mod Modulation signal buffer (normalized [-1.0, 1.0])
157 * @param center Center cutoff frequency in Hz
158 * @param depth Modulation depth in octaves (e.g., 2.0 = ±2 octaves)
159 * @param lo Minimum cutoff in Hz (default 20)
160 * @param hi Maximum cutoff in Hz (default 20000)
161 */
162 void process_modulated(float* buf, uint16_t frames,
163 const float* mod, float center, float depth,
164 float lo = 20.0f, float hi = 20000.0f) {
165 float r = r_;
166 float comp = comp_gain_;
167 float s1 = state_1_, s2 = state_2_;
168 float sr = static_cast<float>(sample_rate_);
169
170 for (uint16_t i = 0; i < frames; ++i) {
171 // Exponential modulation: center * 2^(mod * depth)
172 float freq = signal::exp_mod(center, mod[i], depth, lo, hi);
173
174 // Recompute ZDF coefficients for this sample
175 float f = freq / sr;
176 if (f > 0.497f) f = 0.497f;
177 float g = dsp::fast_tan_pif(f);
178 float h = 1.0f / (1.0f + r * g + g * g);
179
180 // ZDF SVF tick
181 float x = buf[i] * comp;
182 float hp = (x - r * s1 - g * s1 - s2) * h;
183 float bp = g * hp + s1;
184 s1 = g * hp + bp;
185 float lp = g * bp + s2;
186 s2 = g * bp + lp;
187
188 switch (mode_) {
189 case FilterMode::LowPass: buf[i] = lp; break;
190 case FilterMode::HighPass: buf[i] = hp; break;
191 case FilterMode::BandPass: buf[i] = bp; break;
192 case FilterMode::Notch: buf[i] = lp + hp; break;
193 default: buf[i] = lp; break;
194 }
195 }
196 state_1_ = s1;
197 state_2_ = s2;
198
199 // Update cached coefficients to reflect last sample's frequency
200 // so that a subsequent process() call uses a reasonable state
201 float f_last = signal::exp_mod(center, mod[frames - 1], depth, lo, hi) / sr;
202 if (f_last > 0.497f) f_last = 0.497f;
203 g_ = dsp::fast_tan_pif(f_last);
204 h_ = 1.0f / (1.0f + r_ * g_ + g_ * g_);
205 }
206
207 /** @brief Reset filter state */
208 void reset() {
209 state_1_ = 0.0f;
210 state_2_ = 0.0f;
211 }
212
213private:
214 void update_comp_gain() {
215 comp_gain_ = (comp_k_ > 0.0f)
216 ? 1.0f / (1.0f + q_ * comp_k_)
217 : 1.0f;
218 }
219
220 float g_ = 0.0f; // tan(π·f) — frequency coefficient
221 float r_ = 1.0f; // Damping (2.0 = flat, 0.0 = self-oscillation)
222 float h_ = 0.0f; // 1 / (1 + r·g + g²)
223 float q_ = 0.0f; // Resonance (0.0–1.0), cached for compensation
224 float comp_k_ = 0.0f; // Compensation strength (0 = off)
225 float comp_gain_ = 1.0f; // Precomputed: 1 / (1 + q * k)
226 float state_1_ = 0.0f;
227 float state_2_ = 0.0f;
229 uint32_t sample_rate_ = 48000;
230};
231
232} // namespace sbl::widgets::proc
void set_resonance(float q)
Set resonance.
Definition svf.hpp:75
void process(float *buf, uint16_t frames)
Process a block of audio samples in-place.
Definition svf.hpp:112
void set_mode(FilterMode mode)
Set filter output mode.
Definition svf.hpp:102
void reset()
Reset filter state.
Definition svf.hpp:208
void set_cutoff(float freq_hz)
Set cutoff frequency in Hz.
Definition svf.hpp:64
void process_modulated(float *buf, uint16_t frames, const float *mod, float center, float depth, float lo=20.0f, float hi=20000.0f)
Process with per-sample exponential cutoff modulation.
Definition svf.hpp:162
void set_resonance_compensation(float k)
Set resonance compensation strength.
Definition svf.hpp:96
void set_sample_rate(uint32_t sr)
Definition svf.hpp:58
float fast_tan_pif(float f)
Definition fast_math.hpp:68
float exp_mod(float base, float mod, float depth, float lo=20.0f, float hi=20000.0f)
Definition exp_mod.hpp:49
Signal processing widgets.
Definition delay.hpp:31