From dc6fd88ed10fb55d9928fe109f3fd4e9c9a5c930 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Wed, 9 Sep 2026 14:32:49 +0200 Subject: [PATCH] refactor(filter): cache coefficients and extract cutoff calculation Add updateCoefficients() that short-circuits when cutoff, resonance, and type are unchanged. Cache ladderG and formantCoefficients so processSample() avoids redundant exp()/tan() calls per sample. Extract static getCutoffHz() from Filter::process() so FilterBank can compute the keytracked cutoff once per parallel branch instead of per sample. Simplify formant() and screamer() signatures to use cached state. --- Source/Filter.cpp | 105 ++++++++++++++++++++++++++++++------------ Source/Filter.h | 10 +++- Source/FilterBank.cpp | 10 ++-- 3 files changed, 90 insertions(+), 35 deletions(-) diff --git a/Source/Filter.cpp b/Source/Filter.cpp index 484e7bf..724421f 100644 --- a/Source/Filter.cpp +++ b/Source/Filter.cpp @@ -5,7 +5,7 @@ namespace serum namespace { - constexpr double kPi = 3.14159265358979323846; + constexpr double kPi = juce::MathConstants::pi; inline float clampF (float v, float lo, float hi) noexcept { @@ -30,6 +30,9 @@ void Filter::prepare (double sampleRate, int maxBlockSize) void Filter::reset() { + lastCutoffHz = lastRes = -1.0f; + lastType = -1; + ladderG = 0.0; ic1eq = ic2eq = 0.0; lastG = lastK = 0.0; a1 = a2 = a3 = 0.0; @@ -40,6 +43,57 @@ void Filter::reset() combDamp = 0.0; } +void Filter::updateCoefficients (float cutoffHz, float res, int type) noexcept +{ + if (cutoffHz == lastCutoffHz && res == lastRes && type == lastType) + return; + lastCutoffHz = cutoffHz; + lastRes = res; + lastType = type; + + // Ladder stages are cascaded one-poles, which need an exponential coefficient + // (always in (0,1]) for unconditional stability. The TPT SVF (formant/screamer) + // uses cached tan()-based coefficients instead. + switch ((FilterModel) type) + { + case FilterModel::LadderLP: + case FilterModel::LadderHP: + case FilterModel::LadderBP: + case FilterModel::Diode: + { + const double fc = clampD (cutoffHz, 20.0, sr * 0.45); + ladderG = 1.0 - std::exp (-2.0 * kPi * fc / sr); + break; + } + case FilterModel::Formant: + { + // morph (0..1) sweeps the three bandpass centres to produce vowel-like spectra. + const double m = clampD (maps::hzToCutoff (cutoffHz), 0.0, 1.0); + const double base[3] = { 400.0, 1200.0, 2600.0 }; + const double k = clampD (2.0 * (1.0 - (double) res), 0.05, 2.0); + for (int i = 0; i < 3; ++i) + { + const double fc = base[i] * (0.7 + 1.6 * m) * (i == 2 ? 0.9 : 1.0); + const double g = std::tan (kPi * clampD (fc, 30.0, sr * 0.45) / sr); + auto& c = formantCoefficients[(size_t) i]; + c[0] = 1.0 / (1.0 + g * (g + k)); + c[1] = g * c[0]; + c[2] = g * c[1]; + } + break; + } + case FilterModel::Screamer: + { + const double g = std::tan (kPi * clampD (cutoffHz, 30.0, sr * 0.45) / sr); + const double k = clampD (2.0 * (1.0 - (double) res), 0.05, 2.0); + updateSvf (g, k); + break; + } + default: + break; + } +} + void Filter::updateSvf (double g, double k) noexcept { if (g == lastG && k == lastK) @@ -143,22 +197,16 @@ double Filter::comb (double in, double freqHz, double res, double drive) noexcep return (double) y; } -double Filter::formant (double in, double morph, double res) noexcept +double Filter::formant (double in) noexcept { - // morph (0..1) sweeps the three bandpass centres to produce vowel-like spectra. - const double m = clampD (morph, 0.0, 1.0); - const double base[3] = { 400.0, 1200.0, 2600.0 }; - const double k = clampD (2.0 * (1.0 - res), 0.05, 2.0); - double out = 0.0; const double gains[3] = { 1.0, 0.8, 0.5 }; for (int i = 0; i < 3; ++i) { - const double fc = base[i] * (0.7 + 1.6 * m) * (i == 2 ? 0.9 : 1.0); - const double g = std::tan (kPi * clampD (fc, 30.0, sr * 0.45) / sr); - const double a1 = 1.0 / (1.0 + g * (g + k)); - const double a2 = g * a1; - const double a3 = g * a2; + const auto& c = formantCoefficients[(size_t) i]; + const double a1 = c[0]; + const double a2 = c[1]; + const double a3 = c[2]; const double v3 = in - formantState[(size_t) i][1]; const double v1 = a1 * formantState[(size_t) i][0] + a2 * v3; const double v2 = formantState[(size_t) i][1] + a2 * formantState[(size_t) i][0] + a3 * v3; @@ -169,11 +217,9 @@ double Filter::formant (double in, double morph, double res) noexcept return clampD (out * 0.5, -8.0, 8.0); } -double Filter::screamer (double in, double cutoffHz, double res, double drive) noexcept +double Filter::screamer (double in, double drive) noexcept { - const double g = std::tan (kPi * clampD (cutoffHz, 30.0, sr * 0.45) / sr); - const double k = clampD (2.0 * (1.0 - res), 0.05, 2.0); - const double band = svfBand (in, g, k); + const double band = svfBand (in, lastG, lastK); const double driven = std::tanh (band * (1.0 + drive * 12.0)); return driven * (1.0 - drive * 0.4); } @@ -183,11 +229,8 @@ float Filter::processSample (float in, float cutoffHz, float res, float drive, i res = clampF (res, 0.0f, 0.98f); drive = clampF (drive, 0.0f, 1.0f); - // Ladder stages are cascaded one-poles, which need an exponential coefficient - // (always in (0,1]) for unconditional stability. The TPT SVF (formant/screamer) - // computes its own tan()-based g internally. - const double fc = clampD (cutoffHz, 20.0, sr * 0.45); - const double g = 1.0 - std::exp (-2.0 * kPi * fc / sr); + updateCoefficients (cutoffHz, res, type); + const double g = ladderG; double out = (double) in; switch ((FilterModel) type) @@ -221,10 +264,10 @@ float Filter::processSample (float in, float cutoffHz, float res, float drive, i out = comb (in, cutoffHz, res, drive); break; case FilterModel::Formant: - out = formant (in, maps::hzToCutoff (cutoffHz), res); + out = formant (in); break; case FilterModel::Screamer: - out = screamer (in, cutoffHz, res, drive); + out = screamer (in, drive); break; default: break; @@ -233,18 +276,22 @@ float Filter::processSample (float in, float cutoffHz, float res, float drive, i return (float) clampD (out, -8.0, 8.0); } +float Filter::getCutoffHz (float cutoffNorm, float keytrack, float noteHz) noexcept +{ + // Keytrack shifts the cutoff with note pitch. + const float noteNumber = (noteHz > 0.0f) ? (69.0f + 12.0f * std::log2f (noteHz / 440.0f)) : 60.0f; + const float baseHz = maps::cutoffToHz (cutoffNorm); + const float keyFactor = std::pow (2.0f, keytrack * (noteNumber - 60.0f) / 12.0f); + return clampF (baseHz * keyFactor, 20.0f, 18000.0f); +} + void Filter::process (float* samples, int numSamples, float cutoffNorm, float res, float drive, float keytrack, float noteHz, int type, int slope) noexcept { if (samples == nullptr || numSamples <= 0) return; - // Keytrack shifts the cutoff with note pitch. - const float noteNumber = (noteHz > 0.0f) ? (69.0f + 12.0f * std::log2f (noteHz / 440.0f)) : 60.0f; - const float baseHz = maps::cutoffToHz (cutoffNorm); - const float keyFactor = std::pow (2.0f, keytrack * (noteNumber - 60.0f) / 12.0f); - const float cutoffHz = clampF (baseHz * keyFactor, 20.0f, 18000.0f); - + const float cutoffHz = getCutoffHz (cutoffNorm, keytrack, noteHz); for (int i = 0; i < numSamples; ++i) samples[i] = processSample (samples[i], cutoffHz, res, drive, type, slope); } diff --git a/Source/Filter.h b/Source/Filter.h index 7043b5e..9c1279e 100644 --- a/Source/Filter.h +++ b/Source/Filter.h @@ -23,9 +23,13 @@ public: // Single-sample version (used by the comb/naive paths where convenient). float processSample (float in, float cutoffHz, float res, float drive, int type, int slope) noexcept; + static float getCutoffHz (float cutoffNorm, float keytrack, float noteHz) noexcept; private: double sr = 44100.0; + float lastCutoffHz = -1.0f, lastRes = -1.0f; + int lastType = -1; + double ladderG = 0.0; // TPT SVF state (also reused by formant/screamer). double ic1eq = 0.0, ic2eq = 0.0; @@ -42,15 +46,17 @@ private: // Formant: three parallel bandpass SVFs (state pairs). std::array, 3> formantState { { { { 0.0, 0.0 } }, { { 0.0, 0.0 } }, { { 0.0, 0.0 } } } }; + std::array, 3> formantCoefficients {}; + void updateCoefficients (float cutoffHz, float res, int type) noexcept; void updateSvf (double g, double k) noexcept; double svfLow (double in, double g, double k) noexcept; double svfBand (double in, double g, double k) noexcept; double svfHigh (double in, double g, double k) noexcept; double ladder (double in, double g, double res, double drive, int stages, bool diode) noexcept; double comb (double in, double freqHz, double res, double drive) noexcept; - double formant (double in, double morph, double res) noexcept; - double screamer (double in, double cutoffHz, double res, double drive) noexcept; + double formant (double in) noexcept; + double screamer (double in, double drive) noexcept; }; } // namespace serum diff --git a/Source/FilterBank.cpp b/Source/FilterBank.cpp index dce04a8..ba39163 100644 --- a/Source/FilterBank.cpp +++ b/Source/FilterBank.cpp @@ -39,6 +39,8 @@ void FilterBank::process (float* l, float* r, int numSamples, const FilterBankPa else if (p.route == (int) FilterRoute::Parallel) { // Run both filters on copies and crossfade. + const float cutoff1 = p.f1On ? Filter::getCutoffHz (p.f1Cutoff, p.f1Key, noteHz) : 0.0f; + const float cutoff2 = p.f2On ? Filter::getCutoffHz (p.f2Cutoff, p.f2Key, noteHz) : 0.0f; float f1l = 0.0f, f1r = 0.0f, f2l = 0.0f, f2r = 0.0f; for (int i = 0; i < numSamples; ++i) { @@ -46,13 +48,13 @@ void FilterBank::process (float* l, float* r, int numSamples, const FilterBankPa f2l = l[i]; f2r = r[i]; if (p.f1On) { - f1l = f1L.processSample (f1l, maps::cutoffToHz (p.f1Cutoff), p.f1Res, p.f1Drive, p.f1Type, p.f1Slope); - f1r = f1R.processSample (f1r, maps::cutoffToHz (p.f1Cutoff), p.f1Res, p.f1Drive, p.f1Type, p.f1Slope); + f1l = f1L.processSample (f1l, cutoff1, p.f1Res, p.f1Drive, p.f1Type, p.f1Slope); + f1r = f1R.processSample (f1r, cutoff1, p.f1Res, p.f1Drive, p.f1Type, p.f1Slope); } if (p.f2On) { - f2l = f2L.processSample (f2l, maps::cutoffToHz (p.f2Cutoff), p.f2Res, p.f2Drive, p.f2Type, p.f2Slope); - f2r = f2R.processSample (f2r, maps::cutoffToHz (p.f2Cutoff), p.f2Res, p.f2Drive, p.f2Type, p.f2Slope); + f2l = f2L.processSample (f2l, cutoff2, p.f2Res, p.f2Drive, p.f2Type, p.f2Slope); + f2r = f2R.processSample (f2r, cutoff2, p.f2Res, p.f2Drive, p.f2Type, p.f2Slope); } const float m = p.mix; l[i] = f1l * (1.0f - m) + f2l * m;