Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
ota_cascade_filter.hpp
Go to the documentation of this file.
1#pragma once
2
3// Four-pole OTA/ladder-family cascade with a zero-delay nonlinear feedback
4// junction.
5//
6// WHY THIS EXISTS
7// Classic IR3109, SSM/CEM-style and transistor-ladder low-pass filters share a
8// useful DSP skeleton: four one-pole stages inside one global feedback loop.
9// Their audible differences mostly come from control laws and headroom, so this
10// header keeps the topology voicing-neutral and leaves measured panel behavior
11// to AnalogVcfT.
12//
13// The feedback junction is implicit because it uses the current sample's
14// fourth-stage output. Three fixed Newton iterations solve that loop without an
15// audio-thread convergence branch. All state and coefficient math is double,
16// including when SampleType is float; low TPT coefficients lose useful
17// resolution in float at small cutoff/sample-rate ratios.
18//
19// Oversampling is a fixed-storage cascade of 2x, 65-tap Kaiser-windowed-sinc
20// half-band stages. Configuration and processing allocate no memory. The
21// nonlinear junction, pole cascade, tap mix, cross-modulation and output gains
22// all run at the top rate so the downsampler removes their images together.
23
25
26#include <algorithm>
27#include <array>
28#include <cmath>
29#include <cstddef>
30#include <cstdint>
31
32namespace pulp::signal {
33
41template <typename SampleType = float>
43public:
55
62 OtaCascadeFilterT() noexcept {
63 update_smoothing_coefficient();
64 update_g_target();
65 snap_coefficients();
66 }
67
73 void set_sample_rate(double sample_rate) noexcept {
74 // Keep the mandated 20 Hz pole floor below the 0.45 * sample-rate
75 // ceiling even for defensive/invalid host preparation values.
76 sample_rate_ = std::max(64.0, sample_rate);
77 update_internal_rate();
78 update_smoothing_coefficient();
79 update_g_target();
80 reset();
81 }
82
88 void set_oversampling(int factor) noexcept {
89 if (!valid_oversampling_factor(factor))
90 factor = 2;
91 if (factor_ == factor)
92 return;
93 factor_ = factor;
94 update_internal_rate();
95 update_g_target();
96 reset();
97 }
98
103 void set_pole_frequency(double pole_hz) noexcept {
104 pole_hz_ = std::clamp(pole_hz, 20.0, 0.45 * sample_rate_);
105 update_g_target();
106 }
107
112 void set_resonance(double normalized) noexcept {
113 resonance_ = std::clamp(normalized, 0.0, 1.0);
114 update_k_target();
115 }
116
121 void set_k_max(double k_max) noexcept {
122 k_max_ = std::max(0.0, k_max);
123 update_k_target();
124 }
125
129 void set_drive_db(double drive_db) noexcept {
130 drive_target_ = std::pow(10.0, std::clamp(drive_db, -96.0, 72.0) / 20.0);
131 }
132
136 void set_saturation_headroom(double headroom) noexcept {
137 saturation_target_ = std::clamp(headroom, 0.25, 16.0);
138 }
139
143 void set_bias(double bias) noexcept {
144 bias_target_ = std::clamp(bias, -2.0, 2.0);
145 }
146
151 void set_compensation(double amount) noexcept {
152 compensation_ = std::clamp(amount, 0.0, 1.0);
153 update_compensation_target();
154 }
155
159 void set_output_gain(double gain) noexcept {
160 output_gain_target_ = std::max(0.0, gain);
161 }
162
168 void set_cross_modulation(double depth, double drive = 2.0) noexcept {
169 cross_mod_depth_target_ = std::clamp(depth, -1.0, 1.0);
170 cross_mod_drive_target_ = std::clamp(drive, 0.1, 16.0);
171 }
172
177 void set_mode(Mode mode) noexcept {
178 mode_ = mode;
179 update_compensation_target();
180 }
181
187 void set_smoothing_time_ms(double milliseconds) noexcept {
188 smoothing_ms_ = std::max(0.0, milliseconds);
189 update_smoothing_coefficient();
190 }
191
200 void set_drift(double cutoff_cents, double resonance_fraction, double cutoff_rate_hz = 1.5,
201 double resonance_rate_hz = 0.0) noexcept {
202 drift_cutoff_cents_ = std::clamp(cutoff_cents, 0.0, 100.0);
203 drift_resonance_fraction_ = std::clamp(resonance_fraction, 0.0, 0.1);
204 drift_cutoff_rate_hz_ = std::clamp(cutoff_rate_hz, 0.05, 20.0);
205 drift_resonance_rate_hz_ =
206 resonance_rate_hz <= 0.0 ? 0.0 : std::clamp(resonance_rate_hz, 0.05, 40.0);
207 if (drift_cutoff_cents_ == 0.0 && drift_resonance_fraction_ == 0.0) {
208 drift_g_multiplier_ = 1.0;
209 drift_k_multiplier_ = 1.0;
210 }
211 }
212
218 SampleType process(SampleType input) noexcept {
219 step_base_rate_controls();
220
221 std::array<double, 16> values{};
222 std::array<double, 16> expanded{};
223 values[0] = static_cast<double>(input);
224 std::size_t count = 1;
225 const int stages = stage_count();
226
227 for (int stage = 0; stage < stages; ++stage) {
228 for (std::size_t i = 0; i < count; ++i) {
229 expanded[2 * i] = 2.0 * upsamplers_[stage].process(values[i]);
230 expanded[2 * i + 1] = 2.0 * upsamplers_[stage].process(0.0);
231 }
232 count *= 2;
233 std::copy_n(expanded.begin(), count, values.begin());
234 }
235
236 for (std::size_t i = 0; i < count; ++i)
237 values[i] = process_internal(values[i]);
238
239 for (int stage = stages; stage-- > 0;) {
240 count /= 2;
241 for (std::size_t i = 0; i < count; ++i) {
242 // Emit on the even phase. For a 65-tap round trip this places
243 // the symmetric impulse peak exactly at the reported delay.
244 const double output = downsamplers_[stage].process(values[2 * i]);
245 static_cast<void>(downsamplers_[stage].process(values[2 * i + 1]));
246 values[i] = output;
247 }
248 }
249
250 return static_cast<SampleType>(values[0]);
251 }
252
258 void process(SampleType* buffer, int num_samples) noexcept {
259 if (buffer == nullptr || num_samples <= 0)
260 return;
261 for (int i = 0; i < num_samples; ++i)
262 buffer[i] = process(buffer[i]);
263 }
264
270 void reset() noexcept {
271 states_.fill(0.0);
272 taps_.fill(0.0);
273 for (auto& stage : upsamplers_)
274 stage.reset();
275 for (auto& stage : downsamplers_)
276 stage.reset();
277
278 drift_rng_ = kDriftSeed;
279 drift_cutoff_state_ = 0.0;
280 drift_resonance_state_ = 0.0;
281 drift_g_multiplier_ = 1.0;
282 drift_k_multiplier_ = 1.0;
283 drift_countdown_ = 0;
284 snap_coefficients();
285 }
286
291 static constexpr int latency_samples_for_oversampling(int factor) noexcept {
292 if (factor != 1 && factor != 2 && factor != 4 && factor != 8 &&
293 factor != 16)
294 return -1;
295 int latency = 0;
296 for (int level = 0; factor > 1; ++level, factor >>= 1)
297 latency += 32 >> level;
298 return latency;
299 }
300
302 int latency_samples() const noexcept {
303 return latency_samples_for_oversampling(factor_);
304 }
305
307 int oversampling() const noexcept {
308 return factor_;
309 }
310
312 double sample_rate() const noexcept {
313 return sample_rate_;
314 }
315
316private:
317 static constexpr std::uint64_t kDriftSeed = 0xD41F7ull;
318 static constexpr double kPi = 3.141592653589793238462643383279502884;
319
320 static bool valid_oversampling_factor(int factor) noexcept {
321 return factor == 1 || factor == 2 || factor == 4 || factor == 8 || factor == 16;
322 }
323
324 int stage_count() const noexcept {
325 int result = 0;
326 for (int factor = factor_; factor > 1; factor >>= 1)
327 ++result;
328 return result;
329 }
330
331 static double flush(double value) noexcept {
332 return std::abs(value) <= 1.0e-18 ? 0.0 : value;
333 }
334
335 static double smooth(double current, double target, double coefficient) noexcept {
336 return current + coefficient * (target - current);
337 }
338
339 void update_internal_rate() noexcept {
340 internal_rate_ = sample_rate_ * static_cast<double>(factor_);
341 }
342
343 void update_smoothing_coefficient() noexcept {
344 smoothing_coefficient_ =
345 smoothing_ms_ <= 0.0 ? 1.0
346 : 1.0 - std::exp(-1.0 / (smoothing_ms_ * 0.001 * sample_rate_));
347 }
348
349 void update_g_target() noexcept {
350 const double clamped = std::clamp(pole_hz_, 20.0, 0.45 * sample_rate_);
351 const double g = std::tan(kPi * clamped / internal_rate_);
352 g_target_ = g / (1.0 + g);
353 }
354
355 void update_k_target() noexcept {
356 k_target_ = resonance_ * k_max_;
357 update_compensation_target();
358 }
359
360 void update_compensation_target() noexcept {
361 compensation_gain_target_ =
362 mode_ == Mode::highpass4 ? 1.0 : 1.0 + compensation_ * k_target_;
363 }
364
365 void snap_coefficients() noexcept {
366 g_ = g_target_;
367 k_ = k_target_;
368 drive_ = drive_target_;
369 saturation_ = saturation_target_;
370 bias_ = bias_target_;
371 compensation_gain_ = compensation_gain_target_;
372 output_gain_ = output_gain_target_;
373 cross_mod_depth_ = cross_mod_depth_target_;
374 cross_mod_drive_ = cross_mod_drive_target_;
375 }
376
377 std::uint64_t random_u64() noexcept {
378 std::uint64_t z = (drift_rng_ += 0x9E3779B97F4A7C15ull);
379 z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
380 z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
381 return z ^ (z >> 31);
382 }
383
384 double uniform_open01() noexcept {
385 constexpr double kScale = 1.0 / 9007199254740992.0;
386 return (static_cast<double>(random_u64() >> 11) + 0.5) * kScale;
387 }
388
389 double gaussian() noexcept {
390 const double u1 = uniform_open01();
391 const double u2 = uniform_open01();
392 return std::sqrt(-2.0 * std::log(u1)) * std::cos(2.0 * kPi * u2);
393 }
394
395 void step_drift() noexcept {
396 if (drift_cutoff_cents_ == 0.0 && drift_resonance_fraction_ == 0.0) {
397 drift_g_multiplier_ = 1.0;
398 drift_k_multiplier_ = 1.0;
399 return;
400 }
401
402 const double dt = 32.0 / sample_rate_;
403 const double a1 = std::clamp(2.0 * kPi * drift_cutoff_rate_hz_ * dt, 0.0, 0.5);
404 const double resonance_rate =
405 drift_resonance_rate_hz_ == 0.0 ? drift_cutoff_rate_hz_ : drift_resonance_rate_hz_;
406 const double a2 = std::clamp(2.0 * kPi * resonance_rate * dt, 0.0, 0.5);
407
408 drift_cutoff_state_ += a1 * -drift_cutoff_state_ + std::sqrt(2.0 * a1) * 0.5 * gaussian();
409 drift_resonance_state_ +=
410 a2 * -drift_resonance_state_ + std::sqrt(2.0 * a2) * 0.5 * gaussian();
411 drift_cutoff_state_ = std::clamp(drift_cutoff_state_, -1.5, 1.5);
412 drift_resonance_state_ = std::clamp(drift_resonance_state_, -1.5, 1.5);
413
414 drift_g_multiplier_ = std::exp2(drift_cutoff_cents_ * drift_cutoff_state_ / 1200.0);
415 drift_k_multiplier_ = 1.0 + drift_resonance_fraction_ * drift_resonance_state_;
416 }
417
418 void step_base_rate_controls() noexcept {
419 if (drift_countdown_ <= 0) {
420 step_drift();
421 drift_countdown_ = 32;
422 }
423 --drift_countdown_;
424
425 const double coefficient = smoothing_coefficient_;
426 g_ = smooth(g_, std::clamp(g_target_ * drift_g_multiplier_, 0.0, 0.999999), coefficient);
427 k_ = smooth(k_, std::max(0.0, k_target_ * drift_k_multiplier_), coefficient);
428 drive_ = smooth(drive_, drive_target_, coefficient);
429 saturation_ = smooth(saturation_, saturation_target_, coefficient);
430 bias_ = smooth(bias_, bias_target_, coefficient);
431 compensation_gain_ = smooth(compensation_gain_, compensation_gain_target_, coefficient);
432 output_gain_ = smooth(output_gain_, output_gain_target_, coefficient);
433 cross_mod_depth_ = smooth(cross_mod_depth_, cross_mod_depth_target_, coefficient);
434 cross_mod_drive_ = smooth(cross_mod_drive_, cross_mod_drive_target_, coefficient);
435 }
436
437 double process_internal(double input) noexcept {
438 std::array<double, 4> precomputed{};
439 for (std::size_t i = 0; i < states_.size(); ++i)
440 precomputed[i] = (1.0 - g_) * states_[i];
441
442 const double g2 = g_ * g_;
443 const double g3 = g2 * g_;
444 const double a = g2 * g2;
445 const double b =
446 g3 * precomputed[0] + g2 * precomputed[1] + g_ * precomputed[2] + precomputed[3];
447 const double driven = drive_ * input;
448 const double tanh_bias = std::tanh(bias_);
449
450 double y4 = (a * driven + b) / (1.0 + k_ * a);
451 for (int iteration = 0; iteration < 3; ++iteration) {
452 const double w = driven - k_ * y4;
453 const double th = std::tanh(w / saturation_ + bias_);
454 const double f = y4 - a * saturation_ * (th - tanh_bias) - b;
455 const double derivative = 1.0 + a * k_ * (1.0 - th * th);
456 y4 -= f / derivative;
457 }
458
459 const double junction =
460 saturation_ * (std::tanh((driven - k_ * y4) / saturation_ + bias_) - tanh_bias);
461
462 double previous = junction;
463 for (std::size_t i = 0; i < states_.size(); ++i) {
464 const double output = g_ * previous + precomputed[i];
465 states_[i] = flush(2.0 * output - states_[i]);
466 taps_[i] = output;
467 previous = output;
468 }
469
470 double output = taps_[3];
471 switch (mode_) {
472 case Mode::lowpass24:
473 output = taps_[3];
474 break;
475 case Mode::lowpass12:
476 output = taps_[1];
477 break;
478 case Mode::highpass4:
479 output = junction - 4.0 * taps_[0] + 6.0 * taps_[1] - 4.0 * taps_[2] + taps_[3];
480 break;
481 case Mode::bandpass4:
482 output = 4.0 * (taps_[1] - 2.0 * taps_[2] + taps_[3]);
483 break;
484 }
485
486 output *= 1.0 + cross_mod_depth_ * std::tanh(cross_mod_drive_ * driven);
487 return output * compensation_gain_ * output_gain_;
488 }
489
490 double sample_rate_ = 44100.0;
491 double internal_rate_ = 88200.0;
492 int factor_ = 2;
493 double pole_hz_ = 1000.0;
494 double resonance_ = 0.0;
495 double k_max_ = 4.3;
496 Mode mode_ = Mode::lowpass24;
497
498 double smoothing_ms_ = 0.0;
499 double smoothing_coefficient_ = 1.0;
500
501 double g_target_ = 0.0;
502 double k_target_ = 0.0;
503 double drive_target_ = 1.0;
504 double saturation_target_ = 1.0;
505 double bias_target_ = 0.0;
506 double compensation_ = 0.0;
507 double compensation_gain_target_ = 1.0;
508 double output_gain_target_ = 1.0;
509 double cross_mod_depth_target_ = 0.0;
510 double cross_mod_drive_target_ = 2.0;
511
512 double g_ = 0.0;
513 double k_ = 0.0;
514 double drive_ = 1.0;
515 double saturation_ = 1.0;
516 double bias_ = 0.0;
517 double compensation_gain_ = 1.0;
518 double output_gain_ = 1.0;
519 double cross_mod_depth_ = 0.0;
520 double cross_mod_drive_ = 2.0;
521
522 double drift_cutoff_cents_ = 0.0;
523 double drift_resonance_fraction_ = 0.0;
524 double drift_cutoff_rate_hz_ = 1.5;
525 double drift_resonance_rate_hz_ = 0.0;
526 double drift_cutoff_state_ = 0.0;
527 double drift_resonance_state_ = 0.0;
528 double drift_g_multiplier_ = 1.0;
529 double drift_k_multiplier_ = 1.0;
530 std::uint64_t drift_rng_ = kDriftSeed;
531 int drift_countdown_ = 0;
532
533 std::array<double, 4> states_{};
534 std::array<double, 4> taps_{};
535 std::array<detail::FixedHalfBandFir65, 4> upsamplers_{};
536 std::array<detail::FixedHalfBandFir65, 4> downsamplers_{};
537};
538
543
544} // namespace pulp::signal
Voicing-neutral four-pole nonlinear cascade with fixed-storage oversampling.
Definition ota_cascade_filter.hpp:42
void process(SampleType *buffer, int num_samples) noexcept
Processes num_samples base-rate samples in place.
Definition ota_cascade_filter.hpp:258
void set_resonance(double normalized) noexcept
Sets normalized resonance in [0, 1].
Definition ota_cascade_filter.hpp:112
void set_mode(Mode mode) noexcept
Selects the output tap combination without resetting cascade history.
Definition ota_cascade_filter.hpp:177
void set_k_max(double k_max) noexcept
Sets the non-negative maximum feedback coefficient.
Definition ota_cascade_filter.hpp:121
void set_drift(double cutoff_cents, double resonance_fraction, double cutoff_rate_hz=1.5, double resonance_rate_hz=0.0) noexcept
Configures deterministic low-rate cutoff and resonance drift.
Definition ota_cascade_filter.hpp:200
void set_drive_db(double drive_db) noexcept
Sets input drive in decibels, clamped to [-96, 72] dB.
Definition ota_cascade_filter.hpp:129
void reset() noexcept
Clears cascade and oversampler history while preserving configuration.
Definition ota_cascade_filter.hpp:270
int latency_samples() const noexcept
Returns the current round-trip oversampling latency in base-rate samples.
Definition ota_cascade_filter.hpp:302
void set_oversampling(int factor) noexcept
Sets the oversampling factor to 1, 2, 4, 8, or 16.
Definition ota_cascade_filter.hpp:88
void set_output_gain(double gain) noexcept
Sets a non-negative linear output-gain multiplier.
Definition ota_cascade_filter.hpp:159
void set_smoothing_time_ms(double milliseconds) noexcept
Sets the base-rate coefficient-smoothing time in milliseconds.
Definition ota_cascade_filter.hpp:187
OtaCascadeFilterT() noexcept
Constructs a 44.1 kHz, 2x, 24 dB/octave low-pass filter.
Definition ota_cascade_filter.hpp:62
Mode
Output tap combination selected from the shared four-pole cascade.
Definition ota_cascade_filter.hpp:45
@ lowpass12
Two-pole low-pass response (nominal 12 dB/octave).
Definition ota_cascade_filter.hpp:49
@ lowpass24
Four-pole low-pass response (nominal 24 dB/octave).
Definition ota_cascade_filter.hpp:47
@ bandpass4
Four-pole band-pass response.
Definition ota_cascade_filter.hpp:53
@ highpass4
Four-pole high-pass response.
Definition ota_cascade_filter.hpp:51
double sample_rate() const noexcept
Definition ota_cascade_filter.hpp:312
void set_sample_rate(double sample_rate) noexcept
Sets the base sample rate in Hz and resets all DSP history.
Definition ota_cascade_filter.hpp:73
void set_pole_frequency(double pole_hz) noexcept
Sets the one-pole corner frequency in Hz.
Definition ota_cascade_filter.hpp:103
void set_bias(double bias) noexcept
Sets dimensionless asymmetric saturation bias, clamped to [-2, 2].
Definition ota_cascade_filter.hpp:143
SampleType process(SampleType input) noexcept
Processes one base-rate sample and returns one base-rate sample.
Definition ota_cascade_filter.hpp:218
void set_saturation_headroom(double headroom) noexcept
Sets nonlinear-junction headroom, clamped to [0.25, 16].
Definition ota_cascade_filter.hpp:136
void set_compensation(double amount) noexcept
Sets resonance-dependent passband compensation in [0, 1].
Definition ota_cascade_filter.hpp:151
void set_cross_modulation(double depth, double drive=2.0) noexcept
Sets nonlinear output cross-modulation.
Definition ota_cascade_filter.hpp:168
static constexpr int latency_samples_for_oversampling(int factor) noexcept
Returns round-trip FIR latency for an oversampling factor.
Definition ota_cascade_filter.hpp:291
int oversampling() const noexcept
Returns the active oversampling factor: 1, 2, 4, 8, or 16.
Definition ota_cascade_filter.hpp:307
@ kScale
Definition forge_pitch_catalog.hpp:207
@ f
Definition pitch.hpp:25
@ b
Definition pitch.hpp:31
@ g
Definition pitch.hpp:27
Definition additive_bank.hpp:218
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
OtaCascadeFilterT< float > OtaCascadeFilter
Single-precision sample interface for OtaCascadeFilterT<float>.
Definition ota_cascade_filter.hpp:540
OtaCascadeFilterT< double > OtaCascadeFilter64
Double-precision sample interface for OtaCascadeFilterT<double>.
Definition ota_cascade_filter.hpp:542
@ w
Definition input_events.hpp:41
@ z
Definition input_events.hpp:42
Linear-phase 2x FIR stage used by the realtime oversampler.