| @@ -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 "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) { | inline float clip(float x) { | ||||
| return tanhf(x); | return tanhf(x); | ||||
| } | } | ||||
| struct LadderFilter { | struct LadderFilter { | ||||
| float cutoff = 1000.0f; | |||||
| float omega0; | |||||
| float resonance = 1.0f; | 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() { | void reset() { | ||||
| for (int i = 0; i < 4; i++) { | 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 { | struct VCF : Module { | ||||
| enum ParamIds { | enum ParamIds { | ||||
| FREQ_PARAM, | FREQ_PARAM, | ||||
| @@ -106,70 +76,99 @@ struct VCF : Module { | |||||
| }; | }; | ||||
| LadderFilter filter; | LadderFilter filter; | ||||
| // Upsampler<UPSAMPLE, 8> inputUpsampler; | |||||
| // Decimator<UPSAMPLE, 8> lowpassDecimator; | |||||
| // Decimator<UPSAMPLE, 8> highpassDecimator; | |||||
| VCF() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {} | VCF() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {} | ||||
| void step() override; | |||||
| void onReset() override { | void onReset() override { | ||||
| filter.reset(); | 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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(Widget::create<ScrewSilver>(Vec(box.size.x-30, 0))); | |||||
| addChild(Widget::create<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(Widget::create<ScrewSilver>(Vec(box.size.x-30, 365))); | |||||
| addParam(ParamWidget::create<RoundHugeBlackKnob>(Vec(33, 61), module, VCF::FREQ_PARAM, 0.0f, 1.0f, 0.5f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(12, 143), module, VCF::FINE_PARAM, 0.0f, 1.0f, 0.5f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(71, 143), module, VCF::RES_PARAM, 0.0f, 1.0f, 0.0f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(12, 208), module, VCF::FREQ_CV_PARAM, -1.0f, 1.0f, 0.0f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(71, 208), module, VCF::DRIVE_PARAM, 0.0f, 1.0f, 0.0f)); | |||||
| addInput(Port::create<PJ301MPort>(Vec(10, 276), Port::INPUT, module, VCF::FREQ_INPUT)); | |||||
| addInput(Port::create<PJ301MPort>(Vec(48, 276), Port::INPUT, module, VCF::RES_INPUT)); | |||||
| addInput(Port::create<PJ301MPort>(Vec(85, 276), Port::INPUT, module, VCF::DRIVE_INPUT)); | |||||
| addInput(Port::create<PJ301MPort>(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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||||
| addChild(Widget::create<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 30, 365))); | |||||
| addParam(ParamWidget::create<RoundHugeBlackKnob>(Vec(33, 61), module, VCF::FREQ_PARAM, 0.f, 1.f, 0.5f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(12, 143), module, VCF::FINE_PARAM, 0.f, 1.f, 0.5f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(71, 143), module, VCF::RES_PARAM, 0.f, 1.f, 0.f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(12, 208), module, VCF::FREQ_CV_PARAM, -1.f, 1.f, 0.f)); | |||||
| addParam(ParamWidget::create<RoundLargeBlackKnob>(Vec(71, 208), module, VCF::DRIVE_PARAM, 0.f, 1.f, 0.f)); | |||||
| addInput(Port::create<PJ301MPort>(Vec(10, 276), Port::INPUT, module, VCF::FREQ_INPUT)); | |||||
| addInput(Port::create<PJ301MPort>(Vec(48, 276), Port::INPUT, module, VCF::RES_INPUT)); | |||||
| addInput(Port::create<PJ301MPort>(Vec(85, 276), Port::INPUT, module, VCF::DRIVE_INPUT)); | |||||
| addInput(Port::create<PJ301MPort>(Vec(10, 320), Port::INPUT, module, VCF::IN_INPUT)); | |||||
| addOutput(Port::create<PJ301MPort>(Vec(48, 320), Port::OUTPUT, module, VCF::LPF_OUTPUT)); | |||||
| addOutput(Port::create<PJ301MPort>(Vec(85, 320), Port::OUTPUT, module, VCF::HPF_OUTPUT)); | |||||
| } | |||||
| }; | |||||
| addOutput(Port::create<PJ301MPort>(Vec(48, 320), Port::OUTPUT, module, VCF::LPF_OUTPUT)); | |||||
| addOutput(Port::create<PJ301MPort>(Vec(85, 320), Port::OUTPUT, module, VCF::HPF_OUTPUT)); | |||||
| } | |||||
| Model *modelVCF = Model::create<VCF, VCFWidget>("Fundamental", "VCF", "VCF", FILTER_TAG); | Model *modelVCF = Model::create<VCF, VCFWidget>("Fundamental", "VCF", "VCF", FILTER_TAG); | ||||