From 9864bbcac2d87fe62dd224dc25f624d78c1dd1a3 Mon Sep 17 00:00:00 2001 From: hemmer <915048+hemmer@users.noreply.github.com> Date: Sun, 15 Sep 2024 11:53:41 +0100 Subject: [PATCH] Possible workaround for SlewLFO staircasing If LFO peak is reached part way through a an engine sample, work out how much is left of the sample and recalculate the remain trajectory (now opposite rise/fall direction). --- src/SlewLFO.cpp | 203 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 158 insertions(+), 45 deletions(-) diff --git a/src/SlewLFO.cpp b/src/SlewLFO.cpp index 6a98253..3a4f92c 100644 --- a/src/SlewLFO.cpp +++ b/src/SlewLFO.cpp @@ -1,4 +1,5 @@ #include "plugin.hpp" +#include "ChowDSP.hpp" using namespace simd; @@ -40,6 +41,10 @@ struct SlewLFO : Module { LFO, SLEW }; + enum LFOState { + RISING, + FALLING + }; SlewLFO() { config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); @@ -54,19 +59,33 @@ struct SlewLFO : Module { configInput(FALL_INPUT, "Fall CV"); configInput(IN_INPUT, "In"); configOutput(OUT_OUTPUT, "Out"); + + // calculate up/downsampling rates + onSampleRateChange(); + } + + void onSampleRateChange() override { + float sampleRate = APP->engine->getSampleRate(); + for (int c = 0; c < PORT_MAX_CHANNELS; c++) { + oversampler[c].setOversamplingIndex(oversamplingIndex); + oversampler[c].reset(sampleRate); + } } inline double crossfade(double a, double b, double p) { return a + (b - a) * p; } + // oversampling + chowdsp::VariableOversampling<6, double> oversampler[PORT_MAX_CHANNELS]; // uses a 2*6=12th order Butterworth filter + int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling + bool removeDCAtAudioRates = true; + double out[PORT_MAX_CHANNELS] = {}; double phase[PORT_MAX_CHANNELS] = {}; bool state[PORT_MAX_CHANNELS] = {}; // false = rise, true = fall - std::pair getMinMaxSlewRates() { - const CapacitorModifier capacitor = static_cast(params[CAPACITOR_PARAM].getValue()); - const RateMode rate = static_cast(params[RATE_PARAM].getValue()); + std::pair getMinMaxSlewRates(RateMode rate, CapacitorModifier capacitor) { double slowestTime, fastestTime; switch (capacitor) { @@ -99,70 +118,52 @@ struct SlewLFO : Module { void process(const ProcessArgs& args) override { - // minimum and maximum slopes in volts per second - const auto [slewMin, slewMax] = getMinMaxSlewRates(); + const RateMode rate = static_cast(params[RATE_PARAM].getValue()); + const CapacitorModifier capacitor = static_cast(params[CAPACITOR_PARAM].getValue()); + + const auto [slewMin, slewMax] = getMinMaxSlewRates(rate, capacitor); // Amount of extra slew per voltage difference const double shapeScale = 1 / 10.; const double shape = (1 - params[CURVE_PARAM].getValue()) * 0.998; + const double param_rise = params[RISE_PARAM].getValue() * 10.; const double param_fall = params[FALL_PARAM].getValue() * 10.; // this is the number of active polyphony engines, defined by the input const int numPolyphonyEngines = std::max({1, inputs[IN_INPUT].getChannels(), inputs[RISE_INPUT].getChannels(), inputs[FALL_INPUT].getChannels()}); + outputs[OUT_OUTPUT].setChannels(numPolyphonyEngines); + + // oversampling parts + const int oversamplingRatio = oversampler[0].getOversamplingRatio(); + const bool oversampleOutput = (rate == FAST) && (oversamplingRatio > 1); + const int oversampleRatioMain = (rate == FAST) ? oversamplingRatio : 1; + const double sampleTimeOversample = args.sampleTime / oversampleRatioMain; const SlewLFOMode mode = static_cast(params[MODE_PARAM].getValue()); - outputs[OUT_OUTPUT].setChannels(numPolyphonyEngines); for (int c = 0; c < numPolyphonyEngines; c++) { - double in; - switch (mode) { - case SLEW: { - in = inputs[IN_INPUT].getPolyVoltage(c); - break; - } - case LFO: { - state[c] = out[c] >= 10. ? true : state[c]; - state[c] = out[c] <= 0. ? false : state[c]; - in = state[c] ? 0. : 10.; - break; - } - } + double* outBuffer = oversampler[c].getOSBuffer(); + for (int i = 0; i < oversampleRatioMain; i++) { + double remainder = processForChannel(c, mode, slewMin, slewMax, shapeScale, shape, param_rise, param_fall, sampleTimeOversample); - double riseCV = 0.0, fallCV = 0.0; - if (inputs[RISE_INPUT].isConnected()) { - riseCV = inputs[RISE_INPUT].getPolyVoltage(c); - } - if (inputs[FALL_INPUT].isConnected()) { - fallCV = inputs[FALL_INPUT].getPolyVoltage(c); + if (remainder > 0) { + processForChannel(c, mode, slewMin, slewMax, shapeScale, shape, param_rise, param_fall, remainder); + } + outBuffer[i] = out[c]; } - riseCV += param_rise; - fallCV += param_fall; - - double delta = in - out[c]; - double rateCV = 0.0; - if (delta > 0.0) { - rateCV = riseCV; + const double outDownsampled = oversampleOutput ? oversampler[c].downsample() : out[c]; + if (removeDCAtAudioRates && rate == FAST && mode == LFO) { + outputs[OUT_OUTPUT].setVoltage(outDownsampled - 5.f, c); } - else if (delta < 0.0) { - rateCV = fallCV; + else { + outputs[OUT_OUTPUT].setVoltage(outDownsampled, c); } - rateCV *= 0.1; - - double pm_one = (delta > 0) - (delta < 0); - double slew = slewMax * std::pow(slewMin / slewMax, rateCV); - - double diff = slew * crossfade(pm_one, shapeScale * delta, shape) * args.sampleTime; - out[c] += diff; - out[c] = (delta > 0 && (out[c] > in)) ? in : out[c]; - out[c] = (delta < 0 && (out[c] < in)) ? in : out[c]; - - outputs[OUT_OUTPUT].setVoltage(out[c], c); } if (inputs[IN_INPUT].isConnected() && mode == SLEW) { @@ -180,8 +181,100 @@ struct SlewLFO : Module { lights[firstLightId + 1].setBrightnessSmooth(value > 0 ? +value / 10.f : 0.f, deltaTime); // green lights[firstLightId + 2].setBrightness(0.f); // blue } + + double processForChannel(int c, SlewLFOMode mode, double slewMin, double slewMax, double shapeScale, double shape, double param_rise, double param_fall, double sampleTime) { + + double in; + switch (mode) { + case SLEW: { + in = inputs[IN_INPUT].getPolyVoltage(c); + break; + } + case LFO: { + in = state[c] ? 0. : 10.; + break; + } + } + + double riseCV = 0.0, fallCV = 0.0; + if (inputs[RISE_INPUT].isConnected()) { + riseCV = clamp(inputs[RISE_INPUT].getPolyVoltage(c), -5., 10.f); + } + if (inputs[FALL_INPUT].isConnected()) { + fallCV = clamp(inputs[FALL_INPUT].getPolyVoltage(c), -5., 10.f); + } + + riseCV += param_rise; + fallCV += param_fall; + + double delta = in - out[c]; + double rateCV = 0.0; + if (delta > 0.0) { + rateCV = riseCV; + } + else if (delta < 0.0) { + rateCV = fallCV; + } + rateCV *= 0.1; + + double pm_one = (delta > 0) - (delta < 0); + double slew = slewMax * std::pow(slewMin / slewMax, rateCV); + + double diff = slew * crossfade(pm_one, shapeScale * delta, shape) * sampleTime; + + out[c] += diff; + + if (mode == SLEW) { + out[c] = (delta > 0 && (out[c] > in)) ? in : out[c]; + out[c] = (delta < 0 && (out[c] < in)) ? in : out[c]; + } + else { + if (out[c] >= 10.) { + state[c] = FALLING; + + double remainder = (out[c] - in) / diff; + out[c] = 10.; + + return remainder * sampleTime; + } + else if (out[c] <= 0.) { + state[c] = RISING; + + double remainder = (out[c] - in) / diff; + out[c] = 0.; + + return remainder * sampleTime; + } + + } + return 0.; + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "removeDCAtAudioRates", json_boolean(removeDCAtAudioRates)); + json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex())); + + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + + json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); + if (oversamplingIndexJ) { + oversamplingIndex = json_integer_value(oversamplingIndexJ); + onSampleRateChange(); + } + + json_t* removeDCAtAudioRatesJ = json_object_get(rootJ, "removeDCAtAudioRates"); + if (removeDCAtAudioRatesJ) { + removeDCAtAudioRates = json_boolean_value(removeDCAtAudioRatesJ); + } + } }; + + struct SlewInLed : BlackNoiseLed { SlewInLed() { this->setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/slew_in.svg"))); @@ -226,6 +319,26 @@ struct SlewLFOWidget : ModuleWidget { addChild(createLightCentered(mm2px(Vec(3.403, 104.123)), module, SlewLFO::IN_LIGHT)); addChild(createLightCentered(mm2px(Vec(16.897, 104.123)), module, SlewLFO::OUT_LIGHT)); } + + + void appendContextMenu(Menu* menu) override { + SlewLFO* module = static_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + + menu->addChild(createIndexSubmenuItem("Oversampling (fast mode only)", + {"Off", "x2", "x4", "x8", "x16"}, + [ = ]() { + return module->oversamplingIndex; + }, + [ = ](int mode) { + module->oversamplingIndex = mode; + module->onSampleRateChange(); + })); + + menu->addChild(createBoolPtrMenuItem("Centre waveform at audio rates", "", &module->removeDCAtAudioRates)); + } };