From adcc9203245dae35ffc5a73020bdcc07ff90f557 Mon Sep 17 00:00:00 2001 From: Ewan <915048+hemmer@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:32:27 +0000 Subject: [PATCH] PonyVCO (v2.3.0) (#38) * Added Pony VCO * Improve Oversampling filter code * remove DC from EvenVCO pulse * Fix #37 DC on saw based waves * update README with hardware differences * Fix CPU spike with StereoStrip, fixes #39 --- CHANGELOG.md | 8 + README.md | 9 +- plugin.json | 17 +- res/components/SwitchTallVert_bg.svg | 100 ++ res/components/SwitchTallVert_fg.svg | 150 +++ res/components/SwitchWideHoriz_bg.svg | 97 ++ res/components/SwitchWideHoriz_fg.svg | 136 +++ res/panels/PonyVCO.svg | 1423 +++++++++++++++++++++++++ src/ChoppingKinky.cpp | 25 +- src/ChowDSP.hpp | 2 +- src/EvenVCO.cpp | 37 +- src/PonyVCO.cpp | 404 +++++++ src/StereoStrip.cpp | 9 +- src/plugin.cpp | 1 + src/plugin.hpp | 61 +- 15 files changed, 2455 insertions(+), 24 deletions(-) create mode 100644 res/components/SwitchTallVert_bg.svg create mode 100644 res/components/SwitchTallVert_fg.svg create mode 100644 res/components/SwitchWideHoriz_bg.svg create mode 100644 res/components/SwitchWideHoriz_fg.svg create mode 100644 res/panels/PonyVCO.svg create mode 100644 src/PonyVCO.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ce59d17..288c7ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## v2.3.0 + * PonyVCO + * Initial release + * EvenVCO + * Optionally remove DC from pulse wave output + * StereoStrip + * Address high CPU usage when using EQ sliders + ## v2.2.0 * StereoStrip diff --git a/README.md b/README.md index f66949a..1c110be 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,11 @@ We have tried to make the VCV implementations as authentic as possible, however * The hardware Muxlicer assigns multiple functions to the "Speed Div/Mult" dial, that cannot be reproduced with a single mouse click. Some of these have been moved to the context menu, specifically: quadratic gates, the "All In" normalled voltage, and the input/output clock division/mult. The "Speed Div/Mult" dial remains only for main clock div/mult. -* The Noise Plethora filters self-oscillate on the hardware version but not the software version. \ No newline at end of file +* The Noise Plethora filters self-oscillate on the hardware version but not the software version. + +* EvenVCO has the option (default true) to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles) + +* PonyVCO optionally allows the user: + * to filter DC from the TZFM input signal (hardware filters below 15mHz) + * to limit the pulsewidth from 5% to 95% (hardware is full range) + * to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles) \ No newline at end of file diff --git a/plugin.json b/plugin.json index 5019dd9..1ee9a54 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "slug": "Befaco", - "version": "2.2.0", + "version": "2.3.0", "license": "GPL-3.0-or-later", "name": "Befaco", "brand": "Befaco", @@ -246,6 +246,8 @@ "slug": "StereoStrip", "name": "Stereo Strip", "description": "Stereo VCA, panning, and EQ", + "manualUrl": "https://www.befaco.org/stereo-strip/", + "modularGridUrl": "https://www.modulargrid.net/e/divkid-stereo-strip", "tags": [ "Equalizer", "Hardware clone", @@ -254,6 +256,19 @@ "Panning", "Polyphonic" ] + }, + { + "slug": "PonyVCO", + "name": "PonyVCO", + "description": "Compact Thru-Zero (TZFM) oscillator with wavefolder and VCA", + "manualUrl": "https://www.befaco.org/pony-vco/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-pony-vco", + "tags": [ + "Hardware clone", + "Low-frequency oscillator", + "Oscillator", + "Waveshaper" + ] } ] } \ No newline at end of file diff --git a/res/components/SwitchTallVert_bg.svg b/res/components/SwitchTallVert_bg.svg new file mode 100644 index 0000000..4537f54 --- /dev/null +++ b/res/components/SwitchTallVert_bg.svg @@ -0,0 +1,100 @@ + +image/svg+xml + + + + + + + + diff --git a/res/components/SwitchTallVert_fg.svg b/res/components/SwitchTallVert_fg.svg new file mode 100644 index 0000000..2958df3 --- /dev/null +++ b/res/components/SwitchTallVert_fg.svg @@ -0,0 +1,150 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/res/components/SwitchWideHoriz_bg.svg b/res/components/SwitchWideHoriz_bg.svg new file mode 100644 index 0000000..aadd6a7 --- /dev/null +++ b/res/components/SwitchWideHoriz_bg.svg @@ -0,0 +1,97 @@ + +image/svg+xml + + + + + + + + diff --git a/res/components/SwitchWideHoriz_fg.svg b/res/components/SwitchWideHoriz_fg.svg new file mode 100644 index 0000000..74583d9 --- /dev/null +++ b/res/components/SwitchWideHoriz_fg.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/panels/PonyVCO.svg b/res/panels/PonyVCO.svg new file mode 100644 index 0000000..d61f467 --- /dev/null +++ b/res/panels/PonyVCO.svg @@ -0,0 +1,1423 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lienzo 1 + + Capa 1 + + + + + + Lienzo 1 + + Capa 1 + + + + + + Lienzo 1 + + Capa 1 + + + + + + Lienzo 1 + + Capa 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ChoppingKinky.cpp b/src/ChoppingKinky.cpp index 67b1e5d..2620a40 100644 --- a/src/ChoppingKinky.cpp +++ b/src/ChoppingKinky.cpp @@ -47,7 +47,7 @@ struct ChoppingKinky : Module { bool outputAToChopp = false; float previousA = 0.0; - chowdsp::VariableOversampling<> oversampler[NUM_CHANNELS]; + chowdsp::VariableOversampling<6> oversampler[NUM_CHANNELS]; // uses a 2*6=12th order Butterworth filter int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling DCBlocker blockDCFilter; @@ -345,21 +345,16 @@ struct ChoppingKinkyWidget : ModuleWidget { menu->addChild(createMenuLabel("Oversampling mode")); - struct ModeItem : MenuItem { - ChoppingKinky* module; - int oversamplingIndex; - void onAction(const event::Action& e) override { - module->oversamplingIndex = oversamplingIndex; - module->onSampleRateChange(); - } - }; - for (int i = 0; i < 5; i++) { - ModeItem* modeItem = createMenuItem(string::f("%dx", int (1 << i))); - modeItem->rightText = CHECKMARK(module->oversamplingIndex == i); - modeItem->module = module; - modeItem->oversamplingIndex = i; - menu->addChild(modeItem); + menu->addChild(createIndexSubmenuItem("Oversampling", + {"Off", "x2", "x4", "x8", "x16"}, + [ = ]() { + return module->oversamplingIndex; + }, + [ = ](int mode) { + module->oversamplingIndex = mode; + module->onSampleRateChange(); } + )); } }; diff --git a/src/ChowDSP.hpp b/src/ChowDSP.hpp index c4f6db5..873a4d9 100644 --- a/src/ChowDSP.hpp +++ b/src/ChowDSP.hpp @@ -251,7 +251,7 @@ public: * @param osRatio: The oversampling ratio at which the filter is being used */ void reset(float sampleRate, int osRatio) { - float fc = 0.98f * (sampleRate / 2.0f); + float fc = 0.85f * (sampleRate / 2.0f); auto Qs = calculateButterQs(2 * N); for (int i = 0; i < N; ++i) diff --git a/src/EvenVCO.cpp b/src/EvenVCO.cpp index afff33a..67d3d5e 100644 --- a/src/EvenVCO.cpp +++ b/src/EvenVCO.cpp @@ -34,10 +34,9 @@ struct EvenVCO : Module { /** The outputs */ /** Whether we are past the pulse width already */ bool halfPhase[PORT_MAX_CHANNELS] = {}; + bool removePulseDC = true; dsp::MinBlepGenerator<16, 32> triSquareMinBlep[PORT_MAX_CHANNELS]; - dsp::MinBlepGenerator<16, 32> triMinBlep[PORT_MAX_CHANNELS]; - dsp::MinBlepGenerator<16, 32> sineMinBlep[PORT_MAX_CHANNELS]; dsp::MinBlepGenerator<16, 32> doubleSawMinBlep[PORT_MAX_CHANNELS]; dsp::MinBlepGenerator<16, 32> sawMinBlep[PORT_MAX_CHANNELS]; dsp::MinBlepGenerator<16, 32> squareMinBlep[PORT_MAX_CHANNELS]; @@ -183,17 +182,25 @@ struct EvenVCO : Module { sine[c / 4] = 5.f * simd::cos(2 * M_PI * phase[c / 4]); + // minBlep adds a small amount of DC that becomes significant at higher frequencies, + // this subtracts DC based on empirical observvations about the scaling relationship + const float sawCorrect = -5.7; + const float_4 sawDCComp = deltaPhase[c / 4] * sawCorrect; + doubleSaw[c / 4] = simd::ifelse((phase[c / 4] < 0.5), (-1.f + 4.f * phase[c / 4]), (-1.f + 4.f * (phase[c / 4] - 0.5f))); doubleSaw[c / 4] += doubleSawMinBlepOut[c / 4]; + doubleSaw[c / 4] += 2.f * sawDCComp; doubleSaw[c / 4] *= 5.f; even[c / 4] = 0.55 * (doubleSaw[c / 4] + 1.27 * sine[c / 4]); saw[c / 4] = -1.f + 2.f * phase[c / 4]; saw[c / 4] += sawMinBlepOut[c / 4]; + saw[c / 4] += sawDCComp; saw[c / 4] *= 5.f; square[c / 4] = simd::ifelse((phase[c / 4] < pw[c / 4]), -1.f, +1.f); square[c / 4] += squareMinBlepOut[c / 4]; + square[c / 4] += removePulseDC * 2.f * (pw[c / 4] - 0.5f); square[c / 4] *= 5.f; // Set outputs @@ -211,6 +218,20 @@ struct EvenVCO : Module { outputs[SAW_OUTPUT].setChannels(channels); outputs[SQUARE_OUTPUT].setChannels(channels); } + + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* pulseDCJ = json_object_get(rootJ, "removePulseDC"); + if (pulseDCJ) { + removePulseDC = json_boolean_value(pulseDCJ); + } + } }; @@ -241,6 +262,18 @@ struct EvenVCOWidget : ModuleWidget { addOutput(createOutput(Vec(10, 327), module, EvenVCO::SAW_OUTPUT)); addOutput(createOutput(Vec(87, 327), module, EvenVCO::SQUARE_OUTPUT)); } + + void appendContextMenu(Menu* menu) override { + EvenVCO* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createSubmenuItem("Hardware compatibility", "", + [ = ](Menu * menu) { + menu->addChild(createBoolPtrMenuItem("Remove DC from pulse", "", &module->removePulseDC)); + } + )); + } }; diff --git a/src/PonyVCO.cpp b/src/PonyVCO.cpp new file mode 100644 index 0000000..26a7da0 --- /dev/null +++ b/src/PonyVCO.cpp @@ -0,0 +1,404 @@ +#include "plugin.hpp" +#include "ChowDSP.hpp" + + +// references: +// * "REDUCING THE ALIASING OF NONLINEAR WAVESHAPING USING CONTINUOUS-TIME CONVOLUTION" (https://www.dafx.de/paper-archive/2016/dafxpapers/20-DAFx-16_paper_41-PN.pdf) +// * "Antiderivative Antialiasing for Memoryless Nonlinearities" https://acris.aalto.fi/ws/portalfiles/portal/27135145/ELEC_bilbao_et_al_antiderivative_antialiasing_IEEESPL.pdf +// * https://ccrma.stanford.edu/~jatin/Notebooks/adaa.html +// * Pony waveshape https://www.desmos.com/calculator/1kvahyl4ti + +class FoldStage1 { +public: + + float process(float x, float xt) { + float y; + + if (fabs(x - xPrev) < 1e-5) { + y = f(0.5 * (xPrev + x), xt); + } + else { + y = (F(x, xt) - F(xPrev, xt)) / (x - xPrev); + } + xPrev = x; + return y; + } + + // xt - threshold x + static float f(float x, float xt) { + if (x > xt) { + return +5 * xt - 4 * x; + } + else if (x < -xt) { + return -5 * xt - 4 * x; + } + else { + return x; + } + } + + static float F(float x, float xt) { + if (x > xt) { + return 5 * xt * x - 2 * x * x - 2.5 * xt * xt; + } + else if (x < -xt) { + return -5 * xt * x - 2 * x * x - 2.5 * xt * xt; + + } + else { + return x * x / 2.f; + } + } +private: + float xPrev = 0.f; +}; + +class FoldStage2 { +public: + float process(float x) { + float y; + + if (fabs(x - xPrev) < 1e-5) { + y = f(0.5 * (xPrev + x)); + } + else { + y = (F(x) - F(xPrev)) / (x - xPrev); + } + xPrev = x; + return y; + } + + static float f(float x) { + if (-(x + 2) > c) { + return c; + } + else if (x < -1) { + return -(x + 2); + } + else if (x < 1) { + return x; + } + else if (-x + 2 > -c) { + return -x + 2; + } + else { + return -c; + } + } + + static float F(float x) { + if (x < 0) { + return F(-x); + } + else if (x < 1) { + return x * x * 0.5; + } + else if (x < 2 + c) { + return 2 * x * (1.f - x * 0.25f) - 1.f; + } + else { + return 2 * (2 + c) * (1 - (2 + c) * 0.25f) - 1.f - c * (x - 2 - c); + } + } + +private: + float xPrev = 0.f; + static constexpr float c = 0.1; +}; + + +struct PonyVCO : Module { + enum ParamId { + FREQ_PARAM, + RANGE_PARAM, + TIMBRE_PARAM, + OCT_PARAM, + WAVE_PARAM, + PARAMS_LEN + }; + enum InputId { + TZFM_INPUT, + TIMBRE_INPUT, + VOCT_INPUT, + SYNC_INPUT, + VCA_INPUT, + INPUTS_LEN + }; + enum OutputId { + OUT_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + LIGHTS_LEN + }; + enum Waveform { + WAVE_SIN, + WAVE_TRI, + WAVE_SAW, + WAVE_PULSE + }; + + float range[4] = {8.f, 1.f, 1.f / 12.f, 10.f}; + chowdsp::VariableOversampling<6> oversampler; // uses a 2*6=12th order Butterworth filter + int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling + + DCBlocker blockTZFMDCFilter; + bool blockTZFMDC = true; + + // hardware doesn't limit PW but some user might want to (to 5%->95%) + bool limitPW = true; + + // hardware has DC for non-50% duty cycle, optionally add/remove it + bool removePulseDC = true; + + dsp::SchmittTrigger syncTrigger; + + FoldStage1 stage1; + FoldStage2 stage2; + + PonyVCO() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configParam(FREQ_PARAM, -0.5f, 0.5f, 0.0f, "Frequency"); + auto rangeParam = configSwitch(RANGE_PARAM, 0.f, 3.f, 0.f, "Range", {"VCO: Full", "VCO: Octave", "VCO: Semitone", "LFO"}); + rangeParam->snapEnabled = true; + + configParam(TIMBRE_PARAM, 0.f, 1.f, 0.f, "Timbre"); + auto octParam = configSwitch(OCT_PARAM, 0.f, 6.f, 4.f, "Octave", {"C1", "C2", "C3", "C4", "C5", "C6", "C7"}); + octParam->snapEnabled = true; + + auto waveParam = configSwitch(WAVE_PARAM, 0.f, 3.f, 0.f, "Wave", {"Sin", "Triangle", "Sawtooth", "Pulse"}); + waveParam->snapEnabled = true; + + configInput(TZFM_INPUT, "Through-zero FM"); + configInput(TIMBRE_INPUT, "Timber (wavefolder/PWM)"); + configInput(VOCT_INPUT, "Volt per octave"); + configInput(SYNC_INPUT, "Hard sync"); + configInput(VCA_INPUT, "VCA"); + configOutput(OUT_OUTPUT, "Waveform"); + + // calculate up/downsampling rates + onSampleRateChange(); + } + + void onSampleRateChange() override { + float sampleRate = APP->engine->getSampleRate(); + blockTZFMDCFilter.setFrequency(5. / sampleRate); + oversampler.setOversamplingIndex(oversamplingIndex); + oversampler.reset(sampleRate); + } + + // implementation taken from "Alias-Suppressed Oscillators Based on Differentiated Polynomial Waveforms", + // also the notes from Surge Synthesier repo: + // https://github.com/surge-synthesizer/surge/blob/09f1ec8e103265bef6fc0d8a0fc188238197bf8c/src/common/dsp/oscillators/ModernOscillator.cpp#L19 + // Calculation is performed at double precision, as the differencing equations appeared to work poorly with only float. + + double phase = 0.0; // phase at current (sub)sample + double phases[3] = {}; // phase as extrapolated to the current and two previous samples + double sawBuffer[3] = {}, sawOffsetBuff[3] = {}, triBuffer[3] = {}; // buffers for storing the terms in the difference equation + + void process(const ProcessArgs& args) override { + + const int rangeIndex = params[RANGE_PARAM].getValue(); + const bool lfoMode = rangeIndex == 3; + + const Waveform waveform = (Waveform) params[WAVE_PARAM].getValue(); + const float mult = lfoMode ? 1.0 : dsp::FREQ_C4; + const float baseFreq = std::pow(2, (int)(params[OCT_PARAM].getValue() - 3)) * mult; + const int oversamplingRatio = lfoMode ? 1 : oversampler.getOversamplingRatio(); + const float timbre = clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getVoltage() / 10.f, 0.f, 1.f); + + float tzfmVoltage = inputs[TZFM_INPUT].getVoltage(); + if (blockTZFMDC) { + tzfmVoltage = blockTZFMDCFilter.process(tzfmVoltage); + } + + const double pitch = inputs[VOCT_INPUT].getVoltage() + params[FREQ_PARAM].getValue() * range[rangeIndex]; + const double freq = baseFreq * simd::pow(2.f, pitch); + const double deltaBasePhase = clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f); + // denominator for the second-order FD + const double denominator = 0.25 / (deltaBasePhase * deltaBasePhase); + // not clamped, but _total_ phase treated later with floor/ceil + const double deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio; + + float pw = timbre; + if (limitPW) { + pw = clamp(pw, 0.05, 0.95); + } + // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option + // for it to be added back in for hardware compatibility reasons + const float pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw); + + // hard sync + if (syncTrigger.process(inputs[SYNC_INPUT].getVoltage())) { + // hardware waveform is actually cos, so pi/2 phase offset is required + // - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25 + phase = (waveform == WAVE_SIN) ? 0.25f : 0.f; + } + + float* osBuffer = oversampler.getOSBuffer(); + for (int i = 0; i < oversamplingRatio; ++i) { + + phase += deltaBasePhase + deltaFMPhase; + if (phase > 1.f) { + phase -= floor(phase); + } + else if (phase < 0.f) { + phase += -ceil(phase) + 1; + } + + // sin is simple + if (waveform == WAVE_SIN) { + osBuffer[i] = sin2pi_pade_05_5_4(phase); + } + else { + + phases[0] = phase - 2 * deltaBasePhase + (phase < 2 * deltaBasePhase); + phases[1] = phase - deltaBasePhase + (phase < deltaBasePhase); + phases[2] = phase; + + switch (waveform) { + case WAVE_TRI: { + osBuffer[i] = aliasSuppressedTri() * denominator; + break; + } + case WAVE_SAW: { + osBuffer[i] = aliasSuppressedSaw() * denominator; + break; + } + case WAVE_PULSE: { + double saw = aliasSuppressedSaw(); + double sawOffset = aliasSuppressedOffsetSaw(pw); + + osBuffer[i] = (sawOffset - saw) * denominator; + osBuffer[i] += pulseDCOffset; + break; + } + default: break; + } + } + + if (waveform != WAVE_PULSE) { + osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre)); + } + } + + // downsample (if required) + const float out = (oversamplingRatio > 1) ? oversampler.downsample() : osBuffer[0]; + + // end of chain VCA + const float gain = std::max(0.f, inputs[VCA_INPUT].getNormalVoltage(10.f) / 10.f); + outputs[OUT_OUTPUT].setVoltage(5.f * out * gain); + } + + double aliasSuppressedTri() { + for (int i = 0; i < 3; ++i) { + double p = 2 * phases[i] - 1.0; // range -1.0 to +1.0 + double s = 0.5 - std::abs(p); // eq 30 + triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29 + } + return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]); + } + + double aliasSuppressedSaw() { + for (int i = 0; i < 3; ++i) { + double p = 2 * phases[i] - 1.0; // range -1 to +1 + sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11 + } + + return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]); + } + + double aliasSuppressedOffsetSaw(double pw) { + for (int i = 0; i < 3; ++i) { + double p = 2 * phases[i] - 1.0; // range -1 to +1 + double pwp = p + 2 * pw; // phase after pw (pw in [0, 1]) + pwp += (pwp > 1) * -2; // modulo on [-1, +1] + sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11 + } + return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]); + } + + float wavefolder(float x, float xt) { + return stage2.process(stage1.process(x, xt)); + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "blockTZFMDC", json_boolean(blockTZFMDC)); + json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); + json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler.getOversamplingIndex())); + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + + json_t* blockTZFMDCJ = json_object_get(rootJ, "blockTZFMDC"); + if (blockTZFMDCJ) { + blockTZFMDC = json_boolean_value(blockTZFMDCJ); + } + + json_t* removePulseDCJ = json_object_get(rootJ, "removePulseDC"); + if (removePulseDCJ) { + removePulseDC = json_boolean_value(removePulseDCJ); + } + + json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); + if (oversamplingIndexJ) { + oversamplingIndex = json_integer_value(oversamplingIndexJ); + onSampleRateChange(); + } + } +}; + + +struct PonyVCOWidget : ModuleWidget { + PonyVCOWidget(PonyVCO* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/PonyVCO.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(10.0, 14.999)), module, PonyVCO::FREQ_PARAM)); + addParam(createParam(mm2px(Vec(5.498, 27.414)), module, PonyVCO::RANGE_PARAM)); + addParam(createParam(mm2px(Vec(12.65, 37.0)), module, PonyVCO::TIMBRE_PARAM)); + addParam(createParam(mm2px(Vec(3.8, 40.54)), module, PonyVCO::OCT_PARAM)); + addParam(createParam(mm2px(Vec(5.681, 74.436)), module, PonyVCO::WAVE_PARAM)); + + addInput(createInputCentered(mm2px(Vec(5.014, 87.455)), module, PonyVCO::TZFM_INPUT)); + addInput(createInputCentered(mm2px(Vec(14.978, 87.455)), module, PonyVCO::TIMBRE_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.014, 100.413)), module, PonyVCO::VOCT_INPUT)); + addInput(createInputCentered(mm2px(Vec(14.978, 100.413)), module, PonyVCO::SYNC_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.014, 113.409)), module, PonyVCO::VCA_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(15.0, 113.363)), module, PonyVCO::OUT_OUTPUT)); + } + + void appendContextMenu(Menu* menu) override { + PonyVCO* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createSubmenuItem("Hardware compatibility", "", + [ = ](Menu * menu) { + menu->addChild(createBoolPtrMenuItem("Filter TZFM DC", "", &module->blockTZFMDC)); + menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW)); + menu->addChild(createBoolPtrMenuItem("Remove pulse DC", "", &module->removePulseDC)); + } + )); + + menu->addChild(createIndexSubmenuItem("Oversampling", + {"Off", "x2", "x4", "x8"}, + [ = ]() { + return module->oversamplingIndex; + }, + [ = ](int mode) { + module->oversamplingIndex = mode; + module->onSampleRateChange(); + } + )); + + } +}; + +Model* modelPonyVCO = createModel("PonyVCO"); \ No newline at end of file diff --git a/src/StereoStrip.cpp b/src/StereoStrip.cpp index 9a3c780..0c24b15 100644 --- a/src/StereoStrip.cpp +++ b/src/StereoStrip.cpp @@ -229,6 +229,8 @@ struct StereoStrip : Module { // for processing mutes dsp::SlewLimiter clickFilter; + dsp::ClockDivider sliderUpdate; + StereoStrip() { config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); configParam(HIGH_PARAM, -15.0f, 15.0f, 0.0f, "High shelf (2000 Hz) gain", " dB"); @@ -259,6 +261,9 @@ struct StereoStrip : Module { clickFilter.rise = 50.f; // Hz clickFilter.fall = 50.f; // Hz + + // only poll EQ sliders every 16 samples + sliderUpdate.setDivision(16); } void onSampleRateChange() override { @@ -321,7 +326,9 @@ struct StereoStrip : Module { const float switchGains = (params[IN_BOOST_PARAM].getValue() ? 2.0f : 1.0f) * (params[OUT_CUT_PARAM].getValue() ? 0.5f : 1.0f); const float preVCAGain = switchGains * muteGain * std::pow(10, params[LEVEL_PARAM].getValue() / 20.0f); - updateEQsIfChanged(); + if (sliderUpdate.process()) { + updateEQsIfChanged(); + } for (int c = 0; c < numPolyphonyEngines; c += 4) { diff --git a/src/plugin.cpp b/src/plugin.cpp index 95321b0..9d305fb 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -25,4 +25,5 @@ void init(rack::Plugin *p) { p->addModel(modelMex); p->addModel(modelNoisePlethora); p->addModel(modelChannelStrip); + p->addModel(modelPonyVCO); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 648f9e3..ca20a68 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -26,6 +26,7 @@ extern Model* modelMuxlicer; extern Model* modelMex; extern Model* modelNoisePlethora; extern Model* modelChannelStrip; +extern Model* modelPonyVCO; struct Knurlie : SvgScrew { Knurlie() { @@ -139,6 +140,59 @@ struct CKSSHoriz2 : app::SvgSwitch { } }; +struct CKSSVert7 : app::SvgSlider { + CKSSVert7() { + math::Vec margin = math::Vec(3.5, 3.5); + maxHandlePos = math::Vec(1, 1).plus(margin); + minHandlePos = math::Vec(1, 45).plus(margin); + setBackgroundSvg(Svg::load(asset::plugin(pluginInstance, "res/components/SwitchTallVert_bg.svg"))); + setHandleSvg(Svg::load(asset::plugin(pluginInstance, "res/components/SwitchTallVert_fg.svg"))); + background->box.pos = margin; + box.size = background->box.size.plus(margin.mult(2)); + } + + // disable double click as this messes with click to advance + void onDoubleClick(const event::DoubleClick& e) override { } + + // cycle through the values (with reset) on click only (not drag) + void onAction(const ActionEvent& e) override { + ParamQuantity* paramQuantity = getParamQuantity(); + float range = paramQuantity->maxValue - paramQuantity->minValue; + float newValue = paramQuantity->getValue() + 1.f; + if (newValue > paramQuantity->maxValue) { + newValue -= range + 1.f; + } + paramQuantity->setValue(newValue); + } +}; + +struct CKSSHoriz4 : app::SvgSlider { + CKSSHoriz4() { + setBackgroundSvg(Svg::load(asset::plugin(pluginInstance, "res/components/SwitchWideHoriz_bg.svg"))); + setHandleSvg(Svg::load(asset::plugin(pluginInstance, "res/components/SwitchWideHoriz_fg.svg"))); + minHandlePos = mm2px(Vec(0.3f, 0.3f)); + maxHandlePos = mm2px(Vec(6.3f, 0.3f)); + horizontal = true; + math::Vec margin = math::Vec(0, 0); + background->box.pos = margin; + box.size = background->box.size.plus(margin.mult(2)); + } + + // disable double click as this messes with click to advance + void onDoubleClick(const event::DoubleClick& e) override { } + + // cycle through the values (with reset) on click only (not drag) + void onAction(const ActionEvent& e) override { + ParamQuantity* paramQuantity = getParamQuantity(); + float range = paramQuantity->maxValue - paramQuantity->minValue; + float newValue = paramQuantity->getValue() + 1.f; + if (newValue > paramQuantity->maxValue) { + newValue -= range + 1.f; + } + paramQuantity->setValue(newValue); + } +}; + struct CKSSNarrow3 : app::SvgSwitch { CKSSNarrow3() { addFrame(Svg::load(asset::plugin(pluginInstance, "res/components/SwitchNarrow_0.svg"))); @@ -254,9 +308,10 @@ struct DCBlockerT { } float process(float x) { - - x = blockDCFilter[0].process(x); - return blockDCFilter[1].process(x); + for (int idx = 0; idx < N; idx++) { + x = blockDCFilter[idx].process(x); + } + return x; } private: