diff --git a/src/VCF.cpp b/src/VCF.cpp index 195d8b4..7b51679 100644 --- a/src/VCF.cpp +++ b/src/VCF.cpp @@ -1,88 +1,58 @@ -/* -The filter DSP code has been derived from -Miller Puckette's code hosted at -https://github.com/ddiakopoulos/MoogLadders/blob/master/src/RKSimulationModel.h -which is licensed for use under the following terms (MIT license): - - -Copyright (c) 2015, Miller Puckette. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - - #include "Fundamental.hpp" +#include "dsp/functions.hpp" +#include "dsp/resampler.hpp" +#include "dsp/ode.hpp" -// The clipping function of a transistor pair is approximately tanh(x) -// TODO: Put this in a lookup table. 5th order approx doesn't seem to cut it inline float clip(float x) { return tanhf(x); } struct LadderFilter { - float cutoff = 1000.0f; + float omega0; float resonance = 1.0f; - float state[4] = {}; - - void calculateDerivatives(float input, float *dstate, const float *state) { - float cutoff2Pi = 2*M_PI * cutoff; - - float satstate0 = clip(state[0]); - float satstate1 = clip(state[1]); - float satstate2 = clip(state[2]); - - dstate[0] = cutoff2Pi * (clip(input - resonance * state[3]) - satstate0); - dstate[1] = cutoff2Pi * (satstate0 - satstate1); - dstate[2] = cutoff2Pi * (satstate1 - satstate2); - dstate[3] = cutoff2Pi * (satstate2 - clip(state[3])); + float state[4]; + float input; + float lowpass; + float highpass; + + LadderFilter() { + reset(); + setCutoff(0.f); } - void process(float input, float dt) { - float deriv1[4], deriv2[4], deriv3[4], deriv4[4], tempState[4]; - - calculateDerivatives(input, deriv1, state); - for (int i = 0; i < 4; i++) - tempState[i] = state[i] + 0.5f * dt * deriv1[i]; - - calculateDerivatives(input, deriv2, tempState); - for (int i = 0; i < 4; i++) - tempState[i] = state[i] + 0.5f * dt * deriv2[i]; - - calculateDerivatives(input, deriv3, tempState); - for (int i = 0; i < 4; i++) - tempState[i] = state[i] + dt * deriv3[i]; - - calculateDerivatives(input, deriv4, tempState); - for (int i = 0; i < 4; i++) - state[i] += (1.0f / 6.0f) * dt * (deriv1[i] + 2.0f * deriv2[i] + 2.0f * deriv3[i] + deriv4[i]); - } void reset() { for (int i = 0; i < 4; i++) { - state[i] = 0.0f; + state[i] = 0.f; } } + + void setCutoff(float cutoff) { + omega0 = 2.f*M_PI * cutoff; + } + + void process(float input, float dt) { + stepRK4(0.f, dt, state, 4, [&](float t, const float y[], float dydt[]) { + float inputc = clip(input - resonance * y[3]); + float yc0 = clip(y[0]); + float yc1 = clip(y[1]); + float yc2 = clip(y[2]); + float yc3 = clip(y[3]); + + dydt[0] = omega0 * (inputc - yc0); + dydt[1] = omega0 * (yc0 - yc1); + dydt[2] = omega0 * (yc1 - yc2); + dydt[3] = omega0 * (yc2 - yc3); + }); + + lowpass = state[3]; + highpass = clip((input - resonance*state[3]) - 4 * state[0] + 6*state[1] - 4*state[2] + state[3]); + } }; +static const int UPSAMPLE = 2; + struct VCF : Module { enum ParamIds { FREQ_PARAM, @@ -106,70 +76,99 @@ struct VCF : Module { }; LadderFilter filter; + // Upsampler inputUpsampler; + // Decimator lowpassDecimator; + // Decimator highpassDecimator; VCF() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {} - void step() override; + void onReset() override { filter.reset(); } -}; + void step() override { + if (!outputs[LPF_OUTPUT].active && !outputs[HPF_OUTPUT].active) { + outputs[LPF_OUTPUT].value = 0.f; + outputs[HPF_OUTPUT].value = 0.f; + return; + } -void VCF::step() { - float input = inputs[IN_INPUT].value / 5.0f; - float drive = params[DRIVE_PARAM].value + inputs[DRIVE_INPUT].value / 10.0f; - float gain = powf(100.0f, drive); - input *= gain; - // Add -60dB noise to bootstrap self-oscillation - input += 1e-6f * (2.0f*randomUniform() - 1.0f); - - // Set resonance - float res = params[RES_PARAM].value + inputs[RES_INPUT].value / 5.0f; - res = 5.5f * clamp(res, 0.0f, 1.0f); - filter.resonance = res; - - // Set cutoff frequency - float cutoffExp = params[FREQ_PARAM].value + params[FREQ_CV_PARAM].value * inputs[FREQ_INPUT].value / 5.0f; - cutoffExp = clamp(cutoffExp, 0.0f, 1.0f); - const float minCutoff = 15.0f; - const float maxCutoff = 8400.0f; - filter.cutoff = minCutoff * powf(maxCutoff / minCutoff, cutoffExp); - - // Push a sample to the state filter - filter.process(input, 1.0f/engineGetSampleRate()); - - // Set outputs - outputs[LPF_OUTPUT].value = 5.0f * filter.state[3]; - outputs[HPF_OUTPUT].value = 5.0f * (input - filter.state[3]); -} - + float input = inputs[IN_INPUT].value / 5.f; + float drive = clamp(params[DRIVE_PARAM].value + inputs[DRIVE_INPUT].value / 10.f, 0.f, 1.f); + float gain = powf(1.f + drive, 5); + input *= gain; + + // Add -60dB noise to bootstrap self-oscillation + input += 1e-6f * (2.f * randomUniform() - 1.f); + + // Set resonance + float res = clamp(params[RES_PARAM].value + inputs[RES_INPUT].value / 10.f, 0.f, 1.f); + filter.resonance = powf(res, 2) * 10.f; + + // Set cutoff frequency + float pitch = 0.f; + if (inputs[FREQ_INPUT].active) + pitch += inputs[FREQ_INPUT].value * quadraticBipolar(params[FREQ_CV_PARAM].value); + pitch += params[FREQ_PARAM].value * 10.f - 5.f; + pitch += quadraticBipolar(params[FINE_PARAM].value * 2.f - 1.f) * 7.f / 12.f; + float cutoff = 261.626f * powf(2.f, pitch); + cutoff = clamp(cutoff, 1.f, 8000.f); + filter.setCutoff(cutoff); + + /* + // Process sample + float dt = engineGetSampleTime() / UPSAMPLE; + float inputBuf[UPSAMPLE]; + float lowpassBuf[UPSAMPLE]; + float highpassBuf[UPSAMPLE]; + inputUpsampler.process(input, inputBuf); + for (int i = 0; i < UPSAMPLE; i++) { + // Step the filter + filter.process(inputBuf[i], dt); + lowpassBuf[i] = filter.lowpass; + highpassBuf[i] = filter.highpass; + } -struct VCFWidget : ModuleWidget { - VCFWidget(VCF *module); + // Set outputs + if (outputs[LPF_OUTPUT].active) { + outputs[LPF_OUTPUT].value = 5.f * lowpassDecimator.process(lowpassBuf); + } + if (outputs[HPF_OUTPUT].active) { + outputs[HPF_OUTPUT].value = 5.f * highpassDecimator.process(highpassBuf); + } + */ + filter.process(input, engineGetSampleTime()); + outputs[LPF_OUTPUT].value = 5.f * filter.lowpass; + outputs[HPF_OUTPUT].value = 5.f * filter.highpass; + } }; -VCFWidget::VCFWidget(VCF *module) : ModuleWidget(module) { - setPanel(SVG::load(assetPlugin(plugin, "res/VCF.svg"))); - - addChild(Widget::create(Vec(15, 0))); - addChild(Widget::create(Vec(box.size.x-30, 0))); - addChild(Widget::create(Vec(15, 365))); - addChild(Widget::create(Vec(box.size.x-30, 365))); - - addParam(ParamWidget::create(Vec(33, 61), module, VCF::FREQ_PARAM, 0.0f, 1.0f, 0.5f)); - addParam(ParamWidget::create(Vec(12, 143), module, VCF::FINE_PARAM, 0.0f, 1.0f, 0.5f)); - addParam(ParamWidget::create(Vec(71, 143), module, VCF::RES_PARAM, 0.0f, 1.0f, 0.0f)); - addParam(ParamWidget::create(Vec(12, 208), module, VCF::FREQ_CV_PARAM, -1.0f, 1.0f, 0.0f)); - addParam(ParamWidget::create(Vec(71, 208), module, VCF::DRIVE_PARAM, 0.0f, 1.0f, 0.0f)); - addInput(Port::create(Vec(10, 276), Port::INPUT, module, VCF::FREQ_INPUT)); - addInput(Port::create(Vec(48, 276), Port::INPUT, module, VCF::RES_INPUT)); - addInput(Port::create(Vec(85, 276), Port::INPUT, module, VCF::DRIVE_INPUT)); - addInput(Port::create(Vec(10, 320), Port::INPUT, module, VCF::IN_INPUT)); +struct VCFWidget : ModuleWidget { + VCFWidget(VCF *module) : ModuleWidget(module) { + setPanel(SVG::load(assetPlugin(plugin, "res/VCF.svg"))); + + addChild(Widget::create(Vec(15, 0))); + addChild(Widget::create(Vec(box.size.x - 30, 0))); + addChild(Widget::create(Vec(15, 365))); + addChild(Widget::create(Vec(box.size.x - 30, 365))); + + addParam(ParamWidget::create(Vec(33, 61), module, VCF::FREQ_PARAM, 0.f, 1.f, 0.5f)); + addParam(ParamWidget::create(Vec(12, 143), module, VCF::FINE_PARAM, 0.f, 1.f, 0.5f)); + addParam(ParamWidget::create(Vec(71, 143), module, VCF::RES_PARAM, 0.f, 1.f, 0.f)); + addParam(ParamWidget::create(Vec(12, 208), module, VCF::FREQ_CV_PARAM, -1.f, 1.f, 0.f)); + addParam(ParamWidget::create(Vec(71, 208), module, VCF::DRIVE_PARAM, 0.f, 1.f, 0.f)); + + addInput(Port::create(Vec(10, 276), Port::INPUT, module, VCF::FREQ_INPUT)); + addInput(Port::create(Vec(48, 276), Port::INPUT, module, VCF::RES_INPUT)); + addInput(Port::create(Vec(85, 276), Port::INPUT, module, VCF::DRIVE_INPUT)); + addInput(Port::create(Vec(10, 320), Port::INPUT, module, VCF::IN_INPUT)); + + addOutput(Port::create(Vec(48, 320), Port::OUTPUT, module, VCF::LPF_OUTPUT)); + addOutput(Port::create(Vec(85, 320), Port::OUTPUT, module, VCF::HPF_OUTPUT)); + } +}; - addOutput(Port::create(Vec(48, 320), Port::OUTPUT, module, VCF::LPF_OUTPUT)); - addOutput(Port::create(Vec(85, 320), Port::OUTPUT, module, VCF::HPF_OUTPUT)); -} Model *modelVCF = Model::create("Fundamental", "VCF", "VCF", FILTER_TAG);