#include "Filter.h" namespace serum { namespace { constexpr double kPi = 3.14159265358979323846; inline float clampF (float v, float lo, float hi) noexcept { return v < lo ? lo : (v > hi ? hi : v); } inline double clampD (double v, double lo, double hi) noexcept { return v < lo ? lo : (v > hi ? hi : v); } } void Filter::prepare (double sampleRate, int maxBlockSize) { sr = sampleRate; combLine.assign ((size_t) sampleRate, 0.0f); // 1 second of delay combWrite = 0; combDamp = 0.0; reset(); (void) maxBlockSize; } void Filter::reset() { ic1eq = ic2eq = 0.0; lastG = lastK = 0.0; a1 = a2 = a3 = 0.0; stage.fill (0.0); for (auto& s : formantState) s.fill (0.0); std::fill (combLine.begin(), combLine.end(), 0.0f); combWrite = 0; combDamp = 0.0; } void Filter::updateSvf (double g, double k) noexcept { if (g == lastG && k == lastK) return; lastG = g; lastK = k; a1 = 1.0 / (1.0 + g * (g + k)); a2 = g * a1; a3 = g * a2; } double Filter::svfLow (double in, double g, double k) noexcept { updateSvf (g, k); const double v3 = in - ic2eq; const double v1 = a1 * ic1eq + a2 * v3; const double v2 = ic2eq + a2 * ic1eq + a3 * v3; ic1eq = 2.0 * v1 - ic1eq; ic2eq = 2.0 * v2 - ic2eq; return v2; } double Filter::svfBand (double in, double g, double k) noexcept { updateSvf (g, k); const double v3 = in - ic2eq; const double v1 = a1 * ic1eq + a2 * v3; const double v2 = ic2eq + a2 * ic1eq + a3 * v3; ic1eq = 2.0 * v1 - ic1eq; ic2eq = 2.0 * v2 - ic2eq; return v1; } double Filter::svfHigh (double in, double g, double k) noexcept { updateSvf (g, k); const double v3 = in - ic2eq; const double v1 = a1 * ic1eq + a2 * v3; const double v2 = ic2eq + a2 * ic1eq + a3 * v3; ic1eq = 2.0 * v1 - ic1eq; ic2eq = 2.0 * v2 - ic2eq; return in - k * v1 - v2; } double Filter::ladder (double in, double g, double res, double drive, int stages, bool diode) noexcept { stages = juce::jlimit (1, 4, stages); res = clampD (res, 0.0, 0.97); double x; if (diode) { // Diode ladder: softer, asymmetric feedback and diode clipping. x = in - 3.0 * res * (stage[(size_t) (stages - 1)] - in * 0.5); x = x / (1.0 + std::abs (x)); // diode curve x = x * (1.0 + drive * 3.0); } else { x = in - 4.0 * res * stage[(size_t) (stages - 1)]; x = std::tanh (x * (1.0 + drive * 6.0)); // Moog-ish input saturation } for (int i = 0; i < stages; ++i) { stage[(size_t) i] += g * (x - stage[(size_t) i]); x = stage[(size_t) i]; } x = clampD (x, -8.0, 8.0); return x; } double Filter::comb (double in, double freqHz, double res, double drive) noexcept { const int len = (int) combLine.size(); if (len < 4) return in; const double freq = clampD (freqHz, 20.0, sr * 0.45); double delay = sr / freq; delay = clampD (delay, 2.0, (double) len - 2.0); int readPos = combWrite - (int) delay; if (readPos < 0) readPos += len; const int readPos2 = (readPos + 1) % len; const float frac = (float) (delay - std::floor (delay)); const float y0 = combLine[(size_t) readPos]; const float y1 = combLine[(size_t) readPos2]; float y = y0 + (y1 - y0) * frac; // Damping lowpass in the feedback path (drive controls damping). const double dampCoef = 1.0 - clampD (drive, 0.0, 0.99); combDamp = y * (1.0 - dampCoef) + combDamp * dampCoef; const float feedback = clampF ((float) res * 0.9f, 0.0f, 0.98f); combLine[(size_t) combWrite] = (float) in + (float) combDamp * feedback; combWrite = (combWrite + 1) % len; return (double) y; } double Filter::formant (double in, double morph, double res) 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 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; formantState[(size_t) i][0] = 2.0 * v1 - formantState[(size_t) i][0]; formantState[(size_t) i][1] = 2.0 * v2 - formantState[(size_t) i][1]; out += v1 * gains[i]; } return clampD (out * 0.5, -8.0, 8.0); } double Filter::screamer (double in, double cutoffHz, double res, 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 driven = std::tanh (band * (1.0 + drive * 12.0)); return driven * (1.0 - drive * 0.4); } float Filter::processSample (float in, float cutoffHz, float res, float drive, int type, int slope) noexcept { 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); double out = (double) in; switch ((FilterModel) type) { case FilterModel::LadderLP: { const int stages = (slope == 0) ? 1 : (slope == 1) ? 2 : 4; out = ladder (in, g, res, drive, stages, false); break; } case FilterModel::LadderHP: { const int stages = (slope == 0) ? 1 : (slope == 1) ? 2 : 4; out = (double) in - ladder (in, g, res, drive, stages, false); break; } case FilterModel::LadderBP: { int stages = (slope == 0) ? 2 : (slope == 1) ? 2 : 4; out = ladder (in, g, res, drive, stages, false); out = stage[0] - stage[(size_t) (stages - 1)]; break; } case FilterModel::Diode: { const int stages = (slope == 0) ? 1 : (slope == 1) ? 2 : 4; out = ladder (in, g, res, drive, stages, true); break; } case FilterModel::Comb: out = comb (in, cutoffHz, res, drive); break; case FilterModel::Formant: out = formant (in, maps::hzToCutoff (cutoffHz), res); break; case FilterModel::Screamer: out = screamer (in, cutoffHz, res, drive); break; default: break; } return (float) clampD (out, -8.0, 8.0); } 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); for (int i = 0; i < numSamples; ++i) samples[i] = processSample (samples[i], cutoffHz, res, drive, type, slope); } } // namespace serum