diff --git a/plugin.json b/plugin.json index 8daa779..6ecf88f 100644 --- a/plugin.json +++ b/plugin.json @@ -342,6 +342,18 @@ "Hardware clone", "VCO" ] + }, + { + "slug": "PonyVCF", + "name": "PonyVCF", + "description": "Space-conscious lowpass filter and volume processor.", + "manualUrl": "https://www.befaco.org/pony-vcf/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-pony-vcf-", + "tags": [ + "Hardware clone", + "Mixer", + "Filter" + ] } ] } \ No newline at end of file diff --git a/res/panels/PonyVCF.svg b/res/panels/PonyVCF.svg new file mode 100644 index 0000000..00edd01 --- /dev/null +++ b/res/panels/PonyVCF.svg @@ -0,0 +1,1147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VCA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + + + + + + + + + diff --git a/src/PonyVCF.cpp b/src/PonyVCF.cpp new file mode 100644 index 0000000..1ed763a --- /dev/null +++ b/src/PonyVCF.cpp @@ -0,0 +1,224 @@ +#include "plugin.hpp" + +using simd::float_4; + +// filter engine is just Fundemental VCF from https://github.com/VCVRack/Fundamental/blob/v2/src/VCF.cpp for now +// GPL-v3 + + +template +static T clip(T x) { + // return std::tanh(x); + // Pade approximant of tanh + x = simd::clamp(x, -3.f, 3.f); + return x * (27 + x * x) / (27 + 9 * x * x); +} + + +template +struct LadderFilter { + T omega0; + T resonance = 1; + T state[4]; + T input; + + LadderFilter() { + reset(); + setCutoff(0); + } + + void reset() { + for (int i = 0; i < 4; i++) { + state[i] = 0; + } + } + + void setCutoff(T cutoff) { + omega0 = 2 * T(M_PI) * cutoff; + } + + void process(T input, T dt) { + dsp::stepRK4(T(0), dt, state, 4, [&](T t, const T x[], T dxdt[]) { + T inputt = crossfade(this->input, input, t / dt); + T inputc = clip(inputt - resonance * x[3]); + T yc0 = clip(x[0]); + T yc1 = clip(x[1]); + T yc2 = clip(x[2]); + T yc3 = clip(x[3]); + + dxdt[0] = omega0 * (inputc - yc0); + dxdt[1] = omega0 * (yc0 - yc1); + dxdt[2] = omega0 * (yc1 - yc2); + dxdt[3] = omega0 * (yc2 - yc3); + }); + + this->input = input; + } + + T lowpass() { + return state[3]; + } + T highpass() { + return clip((input - resonance * state[3]) - 4 * state[0] + 6 * state[1] - 4 * state[2] + state[3]); + } +}; + + +struct PonyVCF : Module { + enum ParamId { + CV1_PARAM, + RES_PARAM, + FREQ_PARAM, + GAIN1_PARAM, + GAIN2_PARAM, + GAIN3_PARAM, + ROUTING_PARAM, + PARAMS_LEN + }; + enum InputId { + IN1_INPUT, + RES_INPUT, + VCA_INPUT, + IN2_INPUT, + CV1_INPUT, + IN3_INPUT, + CV2_INPUT, + INPUTS_LEN + }; + enum OutputId { + OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + IN2_LIGHT, + IN1_LIGHT, + LIGHTS_LEN + }; + + LadderFilter filters[4]; + + PonyVCF() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configParam(CV1_PARAM, 0.f, 1.f, 0.f, "CV1 Attenuator"); + configParam(RES_PARAM, 0.f, 1.f, 0.f, "Resonance"); + configParam(FREQ_PARAM, 0.f, 1.f, 0.f, "Frequency"); + configParam(GAIN1_PARAM, 0.f, 1.2f, 1.f, "Gain Channel 1"); + configParam(GAIN2_PARAM, 0.f, 1.2f, 1.f, "Gain Channel 2"); + configParam(GAIN3_PARAM, 0.f, 1.2f, 1.f, "Gain Channel 3"); + configParam(ROUTING_PARAM, 0.f, 1.f, 0.f, "VCA routing"); + + configInput(IN1_INPUT, "Channel 1"); + configInput(RES_INPUT, "Resonance CV"); + configInput(VCA_INPUT, "VCA"); + configInput(IN2_INPUT, "Channel 2"); + configInput(CV1_INPUT, "Frequency (CV1)"); + configInput(IN3_INPUT, "Channel 3"); + configInput(CV2_INPUT, "Frequency (CV2)"); + + configOutput(OUTPUT, "Main"); + + onReset(); + } + + + void onReset() override { + for (int i = 0; i < 4; i++) + filters[i].reset(); + } + + float_4 prevOut[4] = {}; + + void process(const ProcessArgs& args) override { + if (!outputs[OUTPUT].isConnected()) { + return; + } + + float resParam = params[RES_PARAM].getValue(); + float freqParam = params[FREQ_PARAM].getValue(); + float freqCvParam = params[CV1_PARAM].getValue(); + + + int channels = std::max({1, inputs[IN1_INPUT].getChannels(), inputs[IN2_INPUT].getChannels(), inputs[IN3_INPUT].getChannels()}); + + for (int c = 0; c < channels; c += 4) { + auto& filter = filters[c / 4]; + + float_4 input = inputs[IN1_INPUT].getVoltageSimd(c) * params[GAIN1_PARAM].getValue(); + input += inputs[IN2_INPUT].getVoltageSimd(c) * params[GAIN2_PARAM].getValue(); + input += inputs[IN3_INPUT].getNormalVoltageSimd(prevOut[c / 4], c) * params[GAIN3_PARAM].getValue(); + + + // input = Saturator::process(input / 5.0f) * 1.1f; + input = clip(input / 5.0f) * 1.1f; + + // Add -120dB noise to bootstrap self-oscillation + input += 1e-6f * (2.f * random::uniform() - 1.f); + + // Set resonance + float_4 resonance = resParam + inputs[RES_INPUT].getPolyVoltageSimd(c) / 10.f; + resonance = clamp(resonance, 0.f, 1.f); + filter.resonance = simd::pow(resonance, 2) * 10.f; + + // Get pitch + float_4 pitch = 5 * freqParam + inputs[CV1_INPUT].getPolyVoltageSimd(c) * freqCvParam + inputs[CV2_INPUT].getPolyVoltageSimd(c); + // Set cutoff + float_4 cutoff = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch); + // Without oversampling, we must limit to 8000 Hz or so @ 44100 Hz + cutoff = clamp(cutoff, 1.f, args.sampleRate / 2.f); + + // Without oversampling, we must limit to 8000 Hz or so @ 44100 Hz + cutoff = clamp(cutoff, 1.f, args.sampleRate * 0.18f); + filter.setCutoff(cutoff); + + // Set outputs + filter.process(input, args.sampleTime); + if (outputs[OUTPUT].isConnected()) { + float_4 resGain = 1.0f / (0.05 + 0.9 * dsp::exp2_taylor5(-8.f * simd::pow(resonance, 2.0))); + // float_4 resGain = (1.0f + 8.f * resonance); // 1st order empirical fit + float_4 out = 5.f * filter.lowpass() * resGain; + outputs[OUTPUT].setVoltageSimd(out, c); + + prevOut[c / 4] = out; + } + + // DEBUG("channel %d %g", channels, input[0]); + } + + outputs[OUTPUT].setChannels(channels); + } +}; + + +struct PonyVCFWidget : ModuleWidget { + PonyVCFWidget(PonyVCF* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/PonyVCF.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(7.62, 14.5)), module, PonyVCF::CV1_PARAM)); + addParam(createParamCentered(mm2px(Vec(22.38, 14.5)), module, PonyVCF::RES_PARAM)); + addParam(createParamCentered(mm2px(Vec(15.0, 35.001)), module, PonyVCF::FREQ_PARAM)); + addParam(createParam(mm2px(Vec(3.217, 48.584)), module, PonyVCF::GAIN1_PARAM)); + addParam(createParam(mm2px(Vec(13.271, 48.584)), module, PonyVCF::GAIN2_PARAM)); + addParam(createParam(mm2px(Vec(23.316, 48.584)), module, PonyVCF::GAIN3_PARAM)); + addParam(createParam(mm2px(Vec(23.498, 96.784)), module, PonyVCF::ROUTING_PARAM)); + + addInput(createInputCentered(mm2px(Vec(5.0, 86.5)), module, PonyVCF::IN1_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.0, 86.5)), module, PonyVCF::RES_INPUT)); + addInput(createInputCentered(mm2px(Vec(25.0, 86.5)), module, PonyVCF::VCA_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.0, 100.0)), module, PonyVCF::IN2_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.0, 100.0)), module, PonyVCF::CV1_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.0, 113.5)), module, PonyVCF::IN3_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.0, 113.5)), module, PonyVCF::CV2_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(25.0, 113.5)), module, PonyVCF::OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(2.578, 23.492)), module, PonyVCF::IN2_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(2.578, 27.159)), module, PonyVCF::IN1_LIGHT)); + } +}; + + +Model* modelPonyVCF = createModel("PonyVCF"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index 3a9f56d..fa8f4bc 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -32,4 +32,5 @@ void init(rack::Plugin *p) { p->addModel(modelMidiThing); p->addModel(modelVoltio); p->addModel(modelOctaves); + p->addModel(modelPonyVCF); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 7be2f1b..57eb131 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -33,6 +33,7 @@ extern Model* modelBurst; extern Model* modelMidiThing; extern Model* modelVoltio; extern Model* modelOctaves; +extern Model* modelPonyVCF; struct Knurlie : SvgScrew { Knurlie() {