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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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: