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.
This commit is contained in:
+76
-29
@@ -5,7 +5,7 @@ namespace serum
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr double kPi = 3.14159265358979323846;
|
constexpr double kPi = juce::MathConstants<double>::pi;
|
||||||
|
|
||||||
inline float clampF (float v, float lo, float hi) noexcept
|
inline float clampF (float v, float lo, float hi) noexcept
|
||||||
{
|
{
|
||||||
@@ -30,6 +30,9 @@ void Filter::prepare (double sampleRate, int maxBlockSize)
|
|||||||
|
|
||||||
void Filter::reset()
|
void Filter::reset()
|
||||||
{
|
{
|
||||||
|
lastCutoffHz = lastRes = -1.0f;
|
||||||
|
lastType = -1;
|
||||||
|
ladderG = 0.0;
|
||||||
ic1eq = ic2eq = 0.0;
|
ic1eq = ic2eq = 0.0;
|
||||||
lastG = lastK = 0.0;
|
lastG = lastK = 0.0;
|
||||||
a1 = a2 = a3 = 0.0;
|
a1 = a2 = a3 = 0.0;
|
||||||
@@ -40,6 +43,57 @@ void Filter::reset()
|
|||||||
combDamp = 0.0;
|
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
|
void Filter::updateSvf (double g, double k) noexcept
|
||||||
{
|
{
|
||||||
if (g == lastG && k == lastK)
|
if (g == lastG && k == lastK)
|
||||||
@@ -143,22 +197,16 @@ double Filter::comb (double in, double freqHz, double res, double drive) noexcep
|
|||||||
return (double) y;
|
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;
|
double out = 0.0;
|
||||||
const double gains[3] = { 1.0, 0.8, 0.5 };
|
const double gains[3] = { 1.0, 0.8, 0.5 };
|
||||||
for (int i = 0; i < 3; ++i)
|
for (int i = 0; i < 3; ++i)
|
||||||
{
|
{
|
||||||
const double fc = base[i] * (0.7 + 1.6 * m) * (i == 2 ? 0.9 : 1.0);
|
const auto& c = formantCoefficients[(size_t) i];
|
||||||
const double g = std::tan (kPi * clampD (fc, 30.0, sr * 0.45) / sr);
|
const double a1 = c[0];
|
||||||
const double a1 = 1.0 / (1.0 + g * (g + k));
|
const double a2 = c[1];
|
||||||
const double a2 = g * a1;
|
const double a3 = c[2];
|
||||||
const double a3 = g * a2;
|
|
||||||
const double v3 = in - formantState[(size_t) i][1];
|
const double v3 = in - formantState[(size_t) i][1];
|
||||||
const double v1 = a1 * formantState[(size_t) i][0] + a2 * v3;
|
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;
|
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);
|
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 band = svfBand (in, lastG, lastK);
|
||||||
const double k = clampD (2.0 * (1.0 - res), 0.05, 2.0);
|
|
||||||
const double band = svfBand (in, g, k);
|
|
||||||
const double driven = std::tanh (band * (1.0 + drive * 12.0));
|
const double driven = std::tanh (band * (1.0 + drive * 12.0));
|
||||||
return driven * (1.0 - drive * 0.4);
|
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);
|
res = clampF (res, 0.0f, 0.98f);
|
||||||
drive = clampF (drive, 0.0f, 1.0f);
|
drive = clampF (drive, 0.0f, 1.0f);
|
||||||
|
|
||||||
// Ladder stages are cascaded one-poles, which need an exponential coefficient
|
updateCoefficients (cutoffHz, res, type);
|
||||||
// (always in (0,1]) for unconditional stability. The TPT SVF (formant/screamer)
|
const double g = ladderG;
|
||||||
// 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);
|
|
||||||
double out = (double) in;
|
double out = (double) in;
|
||||||
|
|
||||||
switch ((FilterModel) type)
|
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);
|
out = comb (in, cutoffHz, res, drive);
|
||||||
break;
|
break;
|
||||||
case FilterModel::Formant:
|
case FilterModel::Formant:
|
||||||
out = formant (in, maps::hzToCutoff (cutoffHz), res);
|
out = formant (in);
|
||||||
break;
|
break;
|
||||||
case FilterModel::Screamer:
|
case FilterModel::Screamer:
|
||||||
out = screamer (in, cutoffHz, res, drive);
|
out = screamer (in, drive);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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);
|
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,
|
void Filter::process (float* samples, int numSamples, float cutoffNorm, float res,
|
||||||
float drive, float keytrack, float noteHz, int type, int slope) noexcept
|
float drive, float keytrack, float noteHz, int type, int slope) noexcept
|
||||||
{
|
{
|
||||||
if (samples == nullptr || numSamples <= 0)
|
if (samples == nullptr || numSamples <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Keytrack shifts the cutoff with note pitch.
|
const float cutoffHz = getCutoffHz (cutoffNorm, keytrack, noteHz);
|
||||||
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);
|
|
||||||
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
samples[i] = processSample (samples[i], cutoffHz, res, drive, type, slope);
|
samples[i] = processSample (samples[i], cutoffHz, res, drive, type, slope);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -23,9 +23,13 @@ public:
|
|||||||
|
|
||||||
// Single-sample version (used by the comb/naive paths where convenient).
|
// 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;
|
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:
|
private:
|
||||||
double sr = 44100.0;
|
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).
|
// TPT SVF state (also reused by formant/screamer).
|
||||||
double ic1eq = 0.0, ic2eq = 0.0;
|
double ic1eq = 0.0, ic2eq = 0.0;
|
||||||
@@ -42,15 +46,17 @@ private:
|
|||||||
|
|
||||||
// Formant: three parallel bandpass SVFs (state pairs).
|
// Formant: three parallel bandpass SVFs (state pairs).
|
||||||
std::array<std::array<double, 2>, 3> formantState { { { { 0.0, 0.0 } }, { { 0.0, 0.0 } }, { { 0.0, 0.0 } } } };
|
std::array<std::array<double, 2>, 3> formantState { { { { 0.0, 0.0 } }, { { 0.0, 0.0 } }, { { 0.0, 0.0 } } } };
|
||||||
|
std::array<std::array<double, 3>, 3> formantCoefficients {};
|
||||||
|
|
||||||
|
void updateCoefficients (float cutoffHz, float res, int type) noexcept;
|
||||||
void updateSvf (double g, double k) noexcept;
|
void updateSvf (double g, double k) noexcept;
|
||||||
double svfLow (double in, double g, double k) noexcept;
|
double svfLow (double in, double g, double k) noexcept;
|
||||||
double svfBand (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 svfHigh (double in, double g, double k) noexcept;
|
||||||
double ladder (double in, double g, double res, double drive, int stages, bool diode) 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 comb (double in, double freqHz, double res, double drive) noexcept;
|
||||||
double formant (double in, double morph, double res) noexcept;
|
double formant (double in) noexcept;
|
||||||
double screamer (double in, double cutoffHz, double res, double drive) noexcept;
|
double screamer (double in, double drive) noexcept;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace serum
|
} // namespace serum
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ void FilterBank::process (float* l, float* r, int numSamples, const FilterBankPa
|
|||||||
else if (p.route == (int) FilterRoute::Parallel)
|
else if (p.route == (int) FilterRoute::Parallel)
|
||||||
{
|
{
|
||||||
// Run both filters on copies and crossfade.
|
// 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;
|
float f1l = 0.0f, f1r = 0.0f, f2l = 0.0f, f2r = 0.0f;
|
||||||
for (int i = 0; i < numSamples; ++i)
|
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];
|
f2l = l[i]; f2r = r[i];
|
||||||
if (p.f1On)
|
if (p.f1On)
|
||||||
{
|
{
|
||||||
f1l = f1L.processSample (f1l, 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, maps::cutoffToHz (p.f1Cutoff), p.f1Res, p.f1Drive, p.f1Type, p.f1Slope);
|
f1r = f1R.processSample (f1r, cutoff1, p.f1Res, p.f1Drive, p.f1Type, p.f1Slope);
|
||||||
}
|
}
|
||||||
if (p.f2On)
|
if (p.f2On)
|
||||||
{
|
{
|
||||||
f2l = f2L.processSample (f2l, 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, maps::cutoffToHz (p.f2Cutoff), 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;
|
const float m = p.mix;
|
||||||
l[i] = f1l * (1.0f - m) + f2l * m;
|
l[i] = f1l * (1.0f - m) + f2l * m;
|
||||||
|
|||||||
Reference in New Issue
Block a user