Skip to content

Commit

Permalink
Possible workaround for SlewLFO staircasing
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
hemmer committed Sep 15, 2024
1 parent e38fe97 commit 9864bbc
Showing 1 changed file with 158 additions and 45 deletions.
203 changes: 158 additions & 45 deletions src/SlewLFO.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "plugin.hpp"
#include "ChowDSP.hpp"

using namespace simd;

Expand Down Expand Up @@ -40,6 +41,10 @@ struct SlewLFO : Module {
LFO,
SLEW
};
enum LFOState {
RISING,
FALLING
};

SlewLFO() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
Expand All @@ -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<double, double> getMinMaxSlewRates() {
const CapacitorModifier capacitor = static_cast<CapacitorModifier>(params[CAPACITOR_PARAM].getValue());
const RateMode rate = static_cast<RateMode>(params[RATE_PARAM].getValue());
std::pair<double, double> getMinMaxSlewRates(RateMode rate, CapacitorModifier capacitor) {

double slowestTime, fastestTime;
switch (capacitor) {
Expand Down Expand Up @@ -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<RateMode>(params[RATE_PARAM].getValue());
const CapacitorModifier capacitor = static_cast<CapacitorModifier>(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<SlewLFOMode>(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) {
Expand All @@ -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")));
Expand Down Expand Up @@ -226,6 +319,26 @@ struct SlewLFOWidget : ModuleWidget {
addChild(createLightCentered<SlewInLed>(mm2px(Vec(3.403, 104.123)), module, SlewLFO::IN_LIGHT));
addChild(createLightCentered<SlewOutLed>(mm2px(Vec(16.897, 104.123)), module, SlewLFO::OUT_LIGHT));
}


void appendContextMenu(Menu* menu) override {
SlewLFO* module = static_cast<SlewLFO*>(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));
}
};


Expand Down

0 comments on commit 9864bbc

Please sign in to comment.