-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathSVF2.h
More file actions
312 lines (271 loc) · 9.24 KB
/
SVF2.h
File metadata and controls
312 lines (271 loc) · 9.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/*
* SVF2.h
*
* A State Variable Filter implementation.
* Higher quality but less efficient than SVF.h.
* Provides LPF, HPF, BPF, Notch and Allpass outputs.
*
* by Andrew R. Brown 2024
*
* Based on the Resonant Filter by Paul Kellett with SVF mods by Peter Schofhauzer from:
* https://www.musicdsp.org/en/latest/Filters/29-resonant-filter.html
*
* This file is part of the M16 audio library.
*
* M16 is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*/
#ifndef SVF2_H_
#define SVF2_H_
// ESP8266 warning: SVF2 uses 64-bit math which is slow without hardware support
#if IS_ESP8266()
#warning "SVF2.h uses 64-bit math and will be slow on ESP8266. Consider using SVF.h instead."
#endif
class SVF2 {
public:
/** Constructor */
SVF2() {
reset();
setRes(0.2);
}
/** Reset all filter state to zero */
void reset() {
buf0 = 0;
buf1 = 0;
low = 0;
band = 0;
high = 0;
notch = 0;
allpassPrevIn = 0;
allpassPrevOut = 0;
simplePrev = 0;
dcPrev = 0;
dcOut = 0;
}
/** Set filter resonance
* @param resonance 0.01 to 1.0
*/
inline void setRes(float resonance) {
float qFloat = min(0.96f, pow(max(0.0f, min(1.0f, resonance)), 0.3f));
qInt = (int32_t)(qFloat * 32768.0f);
// Resonance-dependent gain compensation to prevent clipping at high Q
// At res=0: gainComp=1.0, at res=1: gainComp≈0.6 (gentler curve)
float gainCompF = 1.0f / (1.0f + qFloat * 0.7f);
gainCompInt = (int32_t)(gainCompF * 32768.0f);
updateFeedback();
}
/** @return Current resonance value */
inline float getRes() {
return qInt * (1.0f / 32768.0f);
}
/** Set cutoff frequency in Hz
* @param freq_val 0 to ~10kHz
*/
inline void setFreq(int freq_val) {
freq = max(0, min((int)maxFreq, freq_val));
float fFloat = min(0.96f, 2.0f * sin(3.1415927f * freq * SAMPLE_RATE_INV));
fInt = (int32_t)(fFloat * 32768.0f);
updateFeedback();
}
/** @return Current cutoff frequency in Hz */
inline int32_t getFreq() {
return freq;
}
/** @return Internal f coefficient */
inline float getF() {
return fInt * (1.0f / 32768.0f);
}
/** Set cutoff as normalized value with non-linear mapping
* @param cutoff_val 0.0-1.0 maps to 0-10kHz
* Use setFreq() for absolute Hz values.
*/
inline void setNormalisedCutoff(float cutoff_val) {
_normalisedCutoff = max(0.0f, min(1.0f, cutoff_val));
// Minimum f of 0.001 (~20Hz) prevents filter from stalling at cutoff=0
float fFloat = max(0.001f, min(0.96f, pow(_normalisedCutoff, 2.2f)));
fInt = (int32_t)(fFloat * 32768.0f);
freq = (int32_t)(maxFreq * fFloat);
// fb = q + q * (1.0 + f) -> fbInt = qInt + (qInt * (32768 + fInt)) >> 15
fbInt = qInt + ((int64_t)qInt * (32768 + fInt) >> 15);
}
/** Alias for setNormalisedCutoff for backwards compatibility */
inline void setCutoff(float cutoff_val) { setNormalisedCutoff(cutoff_val); }
/** @return Current normalised cutoff frequency (0.0-1.0) */
inline float getNormalisedCutoff() {
return _normalisedCutoff;
}
/** Alias for getNormalisedCutoff for backwards compatibility */
inline float getCutoff() { return getNormalisedCutoff(); }
/** Calculate next lowpass sample
* @param input Audio sample, clipped to allow overdriven input
* @return Filtered sample
*/
inline int16_t nextLPF(int32_t input) {
input = clip16(input);
calcFilter(input);
return clip16(low);
}
/** Calculate next filter sample (alias for nextLPF)
* @param input Audio sample
* @return Filtered sample
*/
inline int16_t next(int32_t input) {
return nextLPF(input);
}
/** @return Current lowpass output without advancing filter */
inline int16_t currentLPF() {
return clip16(low);
}
/** Calculate next highpass sample
* @param input Audio sample
* @return Filtered sample
*/
inline int16_t nextHPF(int32_t input) {
input = clip16(input);
calcFilter(input);
return clip16(high);
}
/** @return Current highpass output without advancing filter */
inline int16_t currentHPF() {
return clip16(high);
}
/** Calculate next bandpass sample
* @param input Audio sample
* @return Filtered sample
*/
inline int16_t nextBPF(int32_t input) {
input = clip16(input);
calcFilter(input);
return clip16(band);
}
/** @return Current bandpass output without advancing filter */
inline int16_t nextBPF() {
return clip16(band);
}
/** Calculate next sample with crossfade between LPF, BPF, HPF
* @param input Audio sample
* @param mix 0.0=LPF, 0.5=BPF, 1.0=HPF
* @return Filtered sample
*/
inline int16_t nextFiltMix(int32_t input, float mix) {
input = clip16(input);
calcFilter(input);
int32_t lpfAmnt = 0;
int32_t bpfAmnt = 0;
int32_t hpfAmnt = 0;
if (mix < 0.5f) {
// LPF to BPF transition
float lpfMix = 1.0f - mix * 2.0f;
float bpfMix = mix * 2.0f;
lpfAmnt = (int32_t)(low * lpfMix);
bpfAmnt = (int32_t)(band * bpfMix);
} else {
// BPF to HPF transition
float bpfMix = 1.0f - (mix - 0.5f) * 2.0f;
float hpfMix = (mix - 0.5f) * 2.0f;
bpfAmnt = (int32_t)(band * bpfMix);
hpfAmnt = (int32_t)(high * hpfMix);
}
int32_t sum = lpfAmnt + bpfAmnt + hpfAmnt;
if (sum > MAX_16) return MAX_16;
if (sum < -MAX_16) return -MAX_16;
return (int16_t)sum;
}
/** Calculate next allpass sample with DC blocking
* @param input Audio sample
* @return Filtered sample
*/
inline int16_t nextAllpass(int32_t input) {
input = clip16(input);
int32_t output = input + allpassPrevIn - allpassPrevOut;
allpassPrevIn = input;
allpassPrevOut = output;
// DC blocking filter (integer version)
// dcOut = output - dcPrev + 0.995 * dcOut
// Using 15-bit fixed-point: 0.995 * 32768 = 32604
dcOut = output - dcPrev + ((dcOut * 32604) >> 15);
dcPrev = output;
// Clamp output
if (dcOut > MAX_16) return MAX_16;
if (dcOut < -MAX_16) return -MAX_16;
return (int16_t)dcOut;
}
/** Calculate next notch filter sample
* @param input Audio sample
* @return Filtered sample
*/
inline int16_t nextNotch(int32_t input) {
input = clip16(input);
calcFilter(input);
int32_t n = notch;
return max(-MAX_16, (int)min((int32_t)MAX_16, n));
}
private:
// volatile: ensure cross-core visibility on dual-core ESP32 (no CPU overhead)
volatile int32_t low = 0;
volatile int32_t band = 0;
volatile int32_t high = 0;
volatile int32_t notch = 0;
int32_t allpassPrevIn = 0;
int32_t allpassPrevOut = 0;
int32_t simplePrev = 0;
int32_t maxFreq = SAMPLE_RATE * 0.195f;
int32_t freq = 0;
float _normalisedCutoff = 0.0f; // Stored normalized cutoff (0.0-1.0)
// Fixed-point coefficients (15-bit, 32768 = 1.0)
int32_t fInt = 32768;
int32_t qInt = 0;
int32_t fbInt = 0;
int32_t gainCompInt = 32768; // Resonance-dependent gain compensation
// Filter state as 15-bit fixed-point (scaled by 32768)
// volatile: ensure cross-core visibility on dual-core ESP32 (no CPU overhead)
volatile int32_t buf0 = 0;
volatile int32_t buf1 = 0;
// DC blocker state (15-bit fixed-point)
int32_t dcPrev = 0;
int32_t dcOut = 0;
/** Update feedback coefficient when f or q changes */
inline void updateFeedback() {
// fb = q + q * (1.0 - f) -> fbInt = qInt + (qInt * (32768 - fInt)) >> 15
fbInt = qInt + ((int64_t)qInt * (32768 - fInt) >> 15);
}
/** Integer-only core filter calculation
* All arithmetic in 15-bit fixed-point (32768 = 1.0)
* Input expected in range -32767 to 32767, treated as -1.0 to 1.0
*/
inline void calcFilter(int32_t input) {
// Cache coefficients atomically to prevent race conditions with setFreq()/setRes()
// This ensures we use a consistent set of coefficients for the entire sample
int32_t cached_fInt, cached_fbInt, cached_gainCompInt;
#if defined(ESP32) || defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_RP2040)
cached_fInt = __atomic_load_n(&fInt, __ATOMIC_RELAXED);
cached_fbInt = __atomic_load_n(&fbInt, __ATOMIC_RELAXED);
cached_gainCompInt = __atomic_load_n(&gainCompInt, __ATOMIC_RELAXED);
#else
cached_fInt = fInt;
cached_fbInt = fbInt;
cached_gainCompInt = gainCompInt;
#endif
// Apply resonance-dependent gain compensation to prevent clipping at high Q
int32_t in = ((int64_t)input * cached_gainCompInt) >> 15;
// buf0 = buf0 + f * (in - buf0 + fb * (buf0 - buf1))
int32_t diff01 = buf0 - buf1;
int32_t fbTerm = ((int64_t)cached_fbInt * diff01) >> 15;
int32_t innerSum = in - buf0 + fbTerm;
buf0 += ((int64_t)cached_fInt * innerSum) >> 15;
// buf1 = buf1 + f * (buf0 - buf1)
buf1 += ((int64_t)cached_fInt * (buf0 - buf1)) >> 15;
// Prevent fixed-point overflow (equivalent to denormal flush)
// Clamp to reasonable range - increased headroom since input is gain-compensated
const int32_t LIMIT = 32767 * 8; // 8x headroom for resonance peaks
if (buf0 > LIMIT) buf0 = LIMIT;
else if (buf0 < -LIMIT) buf0 = -LIMIT;
if (buf1 > LIMIT) buf1 = LIMIT;
else if (buf1 < -LIMIT) buf1 = -LIMIT;
// Calculate outputs (already in audio range)
low = buf1;
high = in - buf0;
band = buf0 - buf1;
notch = in - buf0 + buf1;
}
};
#endif /* SVF2_H_ */