#include "plugin.hpp" /** Approximates 1/x, using the rcp() instruction and 1 Newton-Raphson refinement */ template inline T rcp_newton1(T x) { T r = simd::rcp(x); return r * (T(2) - x * r); } /** Approximates 1/sqrt(x), using the rsqrt() instruction and 1 Newton-Raphson refinement */ template inline T rsqrt_newton1(T x) { T y = simd::rsqrt(x); return y * (T(3) - x * y * y) * T(0.5); } /** Approximates tan(x) for x in [0, pi*0.5). Optimized coefficients for max relative error: 2.78e-05. */ template inline T tan_1_2(T x) { T x2 = x * x; T num = T(1) + x2 * T(-0.09776575533683811); T den = T(1) + x2 * (T(-0.43119539396382) + x2 * T(0.0105011966117302)); return x * num / den; } /** 1st-order ADAA for softclip: f(x) = x/sqrt(x²+1), F(x) = sqrt(x²+1). */ template struct SoftclipADAA1 { T xPrev = T(0); T fPrev = T(0); // f(0) = 0 T FPrev = T(1); // F(0) = sqrt(0+1) = 1 void reset() { xPrev = T(0); fPrev = T(0); FPrev = T(1); } T process(T x) { const T eps = T(1e-5); T diff = x - xPrev; // f = x/sqrt(x^2+1), F = sqrt(x^2+1) T x2p1 = x * x + T(1); T r = rsqrt_newton1(x2p1); T f = x * r; T F = x2p1 * r; T adaaResult = (F - FPrev) * rcp_newton1(diff); T fallbackResult = (f + fPrev) * T(0.5); T y = simd::ifelse(simd::abs(diff) > eps, adaaResult, fallbackResult); xPrev = x; fPrev = f; FPrev = F; return y; } }; /** 4-pole transistor ladder filter using TPT (Topology-Preserving Transform). Feedback uses previous sample (i.e. 1 sample delayed) to avoid iterative solving. For the equivalent state / trapezoidal integrator approach: Zavalishin, V. "The Art of VA Filter Design". 2018 https://www.native-instruments.com/fileadmin/ni_media/downloads/pdf/VAFilterDesign_2.1.2.pdf For Cytomic's formulation: https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf For antiderivative anti-aliasing (ADAA): Parker, J., Zavalishin, V., Le Bihan, E. "Reducing the Aliasing of Nonlinear Waveshaping Using Continuous-Time Convolution". DAFx 2016 https://dafx.de/paper-archive/2016/dafxpapers/20-DAFx-16_paper_41-PN.pdf */ template struct LadderFilter { T state[4]; SoftclipADAA1 adaa[5]; struct Frame { T input; /** Normalized cutoff frequency fc/fs in [0, 0.5) */ T cutoff; T resonance; /** 4-pole lowpass output. */ T lowpass4; /** 4-pole highpass output. */ T highpass4; }; LadderFilter() { reset(); } void reset() { for (int i = 0; i < 4; i++) { state[i] = T(0); } for (int i = 0; i < 5; i++) { adaa[i].reset(); } } void process(Frame& frame) { // Pre-warped frequency T g = tan_1_2(T(M_PI) * frame.cutoff); // Integrator gain T G = g / (T(1) + g); // Feedback path T feedback = adaa[4].process(frame.resonance * state[3]); T u = frame.input - feedback; // Stage 0 T sat0 = adaa[0].process(u); T v0 = G * (sat0 - state[0]); T y0 = v0 + state[0]; state[0] = y0 + v0; // Stage 1 T sat1 = adaa[1].process(y0); T v1 = G * (sat1 - state[1]); T y1 = v1 + state[1]; state[1] = y1 + v1; // Stage 2 T sat2 = adaa[2].process(y1); T v2 = G * (sat2 - state[2]); T y2 = v2 + state[2]; state[2] = y2 + v2; // Stage 3 T sat3 = adaa[3].process(y2); T v3 = G * (sat3 - state[3]); T y3 = v3 + state[3]; state[3] = y3 + v3; frame.lowpass4 = y3; frame.highpass4 = sat0 - T(4) * y0 + T(6) * y1 - T(4) * y2 + y3; } }; using simd::float_4; struct VCF : Module { enum ParamIds { FREQ_PARAM, FINE_PARAM, // removed in 2.0 RES_PARAM, FREQ_CV_PARAM, DRIVE_PARAM, // Added in 2.0 RES_CV_PARAM, DRIVE_CV_PARAM, NUM_PARAMS }; enum InputIds { FREQ_INPUT, RES_INPUT, DRIVE_INPUT, IN_INPUT, NUM_INPUTS }; enum OutputIds { LPF_OUTPUT, HPF_OUTPUT, NUM_OUTPUTS }; LadderFilter filters[4]; VCF() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS); // To preserve backward compatibility with <2.0, FREQ_PARAM follows // freq = C4 * 2^(10 * param - 5) // or // param = (log2(freq / C4) + 5) / 10 const float minFreq = (std::log2(8.f / dsp::FREQ_C4) + 5) / 10; const float maxFreq = (std::log2(22000.f / dsp::FREQ_C4) + 5) / 10; const float defaultFreq = (minFreq + maxFreq) / 2.f; configParam(FREQ_PARAM, minFreq, maxFreq, defaultFreq, "Cutoff frequency", " Hz", std::pow(2, 10.f), dsp::FREQ_C4 / std::pow(2, 5.f)); configParam(RES_PARAM, 0.f, 1.f, 0.f, "Resonance", "%", 0.f, 100.f); configParam(RES_CV_PARAM, -1.f, 1.f, 0.f, "Resonance CV", "%", 0.f, 100.f); configParam(FREQ_CV_PARAM, -1.f, 1.f, 0.f, "Cutoff frequency CV", "%", 0.f, 100.f); // gain(drive) = (1 + drive)^5 // gain(-1) = 0 // gain(0) = 1 // gain(1) = 32 configParam(DRIVE_PARAM, -1.f, 1.f, 0.f, "Drive", "%", 0, 100, 100); configParam(DRIVE_CV_PARAM, -1.f, 1.f, 0.f, "Drive CV", "%", 0, 100); configInput(FREQ_INPUT, "Frequency"); configInput(RES_INPUT, "Resonance"); configInput(DRIVE_INPUT, "Drive"); configInput(IN_INPUT, "Audio"); configOutput(LPF_OUTPUT, "Lowpass filter"); configOutput(HPF_OUTPUT, "Highpass filter"); configBypass(IN_INPUT, LPF_OUTPUT); configBypass(IN_INPUT, HPF_OUTPUT); } void onReset() override { for (int i = 0; i < 4; i++) { filters[i].reset(); } } void process(const ProcessArgs& args) override { if (!outputs[LPF_OUTPUT].isConnected() && !outputs[HPF_OUTPUT].isConnected()) { return; } float driveParam = params[DRIVE_PARAM].getValue(); float driveCvParam = params[DRIVE_CV_PARAM].getValue(); float resParam = params[RES_PARAM].getValue(); float resCvParam = params[RES_CV_PARAM].getValue(); float freqParam = params[FREQ_PARAM].getValue(); // Rescale for backward compatibility freqParam = freqParam * 10.f - 5.f; float freqCvParam = params[FREQ_CV_PARAM].getValue(); int channels = std::max(1, inputs[IN_INPUT].getChannels()); for (int c = 0; c < channels; c += 4) { LadderFilter::Frame frame; // Input float_4 input = inputs[IN_INPUT].getVoltageSimd(c) / 5.f; // Drive float_4 drive = driveParam + inputs[DRIVE_INPUT].getPolyVoltageSimd(c) / 10.f * driveCvParam; drive = simd::clamp(drive, -1.f, 1.f); float_4 gain = simd::pow(1.f + drive, 5); input *= gain; // Add -120dB noise to bootstrap self-oscillation input += 1e-6f * (2.f * random::uniform() - 1.f); frame.input = input; // Resonance float_4 resonance = resParam + inputs[RES_INPUT].getPolyVoltageSimd(c) / 10.f * resCvParam; resonance = simd::clamp(resonance, 0.f, 1.f); frame.resonance = simd::pow(resonance, 2) * 10.f; // Cutoff frequency float_4 pitch = freqParam + inputs[FREQ_INPUT].getPolyVoltageSimd(c) * freqCvParam; float_4 cutoff = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch); frame.cutoff = simd::clamp(cutoff * args.sampleTime, 0.f, 0.499f); // Process filters[c / 4].process(frame); // Outputs outputs[LPF_OUTPUT].setVoltageSimd(frame.lowpass4 * 5.f, c); outputs[HPF_OUTPUT].setVoltageSimd(frame.highpass4 * 5.f, c); } outputs[LPF_OUTPUT].setChannels(channels); outputs[HPF_OUTPUT].setChannels(channels); } void paramsFromJson(json_t* rootJ) override { // These attenuators didn't exist in version <2, so set to 1 in case they are not overwritten. params[RES_CV_PARAM].setValue(1.f); params[DRIVE_CV_PARAM].setValue(1.f); Module::paramsFromJson(rootJ); } }; struct VCFWidget : ModuleWidget { VCFWidget(VCF* module) { setModule(module); setPanel(createPanel(asset::plugin(pluginInstance, "res/VCF.svg"), asset::plugin(pluginInstance, "res/VCF-dark.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParamCentered(mm2px(Vec(17.587, 29.808)), module, VCF::FREQ_PARAM)); addParam(createParamCentered(mm2px(Vec(8.895, 56.388)), module, VCF::RES_PARAM)); addParam(createParamCentered(mm2px(Vec(26.665, 56.388)), module, VCF::DRIVE_PARAM)); addParam(createParamCentered(mm2px(Vec(6.996, 80.603)), module, VCF::FREQ_CV_PARAM)); addParam(createParamCentered(mm2px(Vec(17.833, 80.603)), module, VCF::RES_CV_PARAM)); addParam(createParamCentered(mm2px(Vec(28.67, 80.603)), module, VCF::DRIVE_CV_PARAM)); addInput(createInputCentered(mm2px(Vec(6.996, 96.813)), module, VCF::FREQ_INPUT)); addInput(createInputCentered(mm2px(Vec(17.833, 96.813)), module, VCF::RES_INPUT)); addInput(createInputCentered(mm2px(Vec(28.67, 96.813)), module, VCF::DRIVE_INPUT)); addInput(createInputCentered(mm2px(Vec(6.996, 113.115)), module, VCF::IN_INPUT)); addOutput(createOutputCentered(mm2px(Vec(17.833, 113.115)), module, VCF::LPF_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(28.67, 113.115)), module, VCF::HPF_OUTPUT)); } }; Model* modelVCF = createModel("VCF");