From aa6f4f5928a2a34127430e723346b6c4a4ab3e02 Mon Sep 17 00:00:00 2001
From: hemmer <915048+hemmer@users.noreply.github.com>
Date: Tue, 26 Mar 2024 08:40:51 +0000
Subject: [PATCH] Reinstate EvenVCO beta verion
* fix DC blocker bug Octaves
---
plugin.json | 12 +
res/panels/EvenVCObeta.svg | 1850 ++++++++++++++++++++++++++++++++++++
src/EvenVCO2.cpp | 331 +++++++
src/Octaves.cpp | 19 +-
src/plugin.cpp | 1 +
src/plugin.hpp | 1 +
6 files changed, 2209 insertions(+), 5 deletions(-)
create mode 100644 res/panels/EvenVCObeta.svg
create mode 100644 src/EvenVCO2.cpp
diff --git a/plugin.json b/plugin.json
index 08861d0..8daa779 100644
--- a/plugin.json
+++ b/plugin.json
@@ -23,6 +23,18 @@
"Polyphonic"
]
},
+ {
+ "slug": "EvenVCO2",
+ "name": "Even VCO (beta)",
+ "description": "Oscillator including even-harmonic waveform",
+ "manualUrl": "https://www.befaco.org/even-vco/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-",
+ "tags": [
+ "VCO",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
{
"slug": "Rampage",
"name": "Rampage",
diff --git a/res/panels/EvenVCObeta.svg b/res/panels/EvenVCObeta.svg
new file mode 100644
index 0000000..dd1e1e5
--- /dev/null
+++ b/res/panels/EvenVCObeta.svg
@@ -0,0 +1,1850 @@
+
+
+
+
diff --git a/src/EvenVCO2.cpp b/src/EvenVCO2.cpp
new file mode 100644
index 0000000..7886ff6
--- /dev/null
+++ b/src/EvenVCO2.cpp
@@ -0,0 +1,331 @@
+#include "plugin.hpp"
+#include "ChowDSP.hpp"
+
+using simd::float_4;
+
+struct EvenVCO2 : Module {
+ enum ParamIds {
+ OCTAVE_PARAM,
+ TUNE_PARAM,
+ PWM_PARAM,
+ NUM_PARAMS
+ };
+ enum InputIds {
+ PITCH1_INPUT,
+ PITCH2_INPUT,
+ FM_INPUT,
+ SYNC_INPUT,
+ PWM_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ TRI_OUTPUT,
+ SINE_OUTPUT,
+ EVEN_OUTPUT,
+ SAW_OUTPUT,
+ SQUARE_OUTPUT,
+ NUM_OUTPUTS
+ };
+
+
+ float_4 phase[4] = {};
+ dsp::TSchmittTrigger syncTrigger[4];
+ bool removePulseDC = true;
+ bool limitPW = true;
+
+ EvenVCO2() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS);
+ configParam(OCTAVE_PARAM, -5.0, 4.0, 0.0, "Octave", "'", 0.5);
+ getParamQuantity(OCTAVE_PARAM)->snapEnabled = true;
+ configParam(TUNE_PARAM, -7.0, 7.0, 0.0, "Tune", " semitones");
+ configParam(PWM_PARAM, -1.0, 1.0, 0.0, "Pulse width");
+
+ configInput(PITCH1_INPUT, "Pitch 1");
+ configInput(PITCH2_INPUT, "Pitch 2");
+ configInput(FM_INPUT, "FM");
+ configInput(SYNC_INPUT, "Sync");
+ configInput(PWM_INPUT, "Pulse Width Modulation");
+
+ configOutput(TRI_OUTPUT, "Triangle");
+ configOutput(SINE_OUTPUT, "Sine");
+ configOutput(EVEN_OUTPUT, "Even");
+ configOutput(SAW_OUTPUT, "Sawtooth");
+ configOutput(SQUARE_OUTPUT, "Square");
+
+ // calculate up/downsampling rates
+ onSampleRateChange();
+ }
+
+ void onSampleRateChange() override {
+ float sampleRate = APP->engine->getSampleRate();
+ for (int i = 0; i < NUM_OUTPUTS; ++i) {
+ for (int c = 0; c < 4; c++) {
+ oversampler[i][c].setOversamplingIndex(oversamplingIndex);
+ oversampler[i][c].reset(sampleRate);
+ }
+ }
+
+ const float lowFreqRegime = oversampler[0][0].getOversamplingRatio() * 1e-3 * sampleRate;
+ DEBUG("Low freq regime: %g", lowFreqRegime);
+ }
+
+ float_4 aliasSuppressedTri(float_4* phases) {
+ float_4 triBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0
+ float_4 s = 0.5 - simd::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]);
+ }
+
+ float_4 aliasSuppressedSaw(float_4* phases) {
+ float_4 sawBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float_4 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]);
+ }
+
+ float_4 aliasSuppressedDoubleSaw(float_4* phases) {
+ float_4 sawBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float_4 p = 4.0 * simd::ifelse(phases[i] < 0.5, phases[i], phases[i] - 0.5) - 1.0;
+ sawBuffer[i] = (p * p * p - p) / 24.0; // eq 11 (modified for doubled freq)
+ }
+
+ return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
+ }
+
+ float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) {
+ float_4 sawOffsetBuff[3];
+
+ for (int i = 0; i < 3; ++i) {
+ float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
+ float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
+ pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1]
+ sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
+ }
+ return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]);
+ }
+
+ chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
+ int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling
+
+ void process(const ProcessArgs& args) override {
+
+ // pitch inputs determine number of polyphony engines
+ const int channels = std::max({1, inputs[PITCH1_INPUT].getChannels(), inputs[PITCH2_INPUT].getChannels()});
+
+ const float pitchKnobs = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
+ const int oversamplingRatio = oversampler[0][0].getOversamplingRatio();
+
+ for (int c = 0; c < channels; c += 4) {
+ float_4 pw = simd::clamp(params[PWM_PARAM].getValue() + inputs[PWM_INPUT].getPolyVoltageSimd(c) / 5.f, -1.f, 1.f);
+ if (limitPW) {
+ pw = simd::rescale(pw, -1, +1, 0.05f, 0.95f);
+ }
+ else {
+ pw = simd::rescale(pw, -1.f, +1.f, 0.f, 1.f);
+ }
+
+ const float_4 fmVoltage = inputs[FM_INPUT].getPolyVoltageSimd(c) * 0.25f;
+ const float_4 pitch = inputs[PITCH1_INPUT].getPolyVoltageSimd(c) + inputs[PITCH2_INPUT].getPolyVoltageSimd(c);
+ const float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitchKnobs + pitch + fmVoltage);
+ const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, 1e-6, 0.5f);
+ // floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator
+ // becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't
+ // a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz.
+ const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3;
+ // 1 / denominator for the second-order FD
+ const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase);
+
+ // 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_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);
+
+ // hard sync
+ const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd(c));
+ phase[c / 4] = simd::ifelse(syncMask, 0.5f, phase[c / 4]);
+
+ float_4* osBufferTri = oversampler[TRI_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferSaw = oversampler[SAW_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferSin = oversampler[SINE_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferSquare = oversampler[SQUARE_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferEven = oversampler[EVEN_OUTPUT][c / 4].getOSBuffer();
+ for (int i = 0; i < oversamplingRatio; ++i) {
+
+ phase[c / 4] += deltaBasePhase;
+ // ensure within [0, 1]
+ phase[c / 4] -= simd::floor(phase[c / 4]);
+
+ float_4 phases[3]; // phase as extrapolated to the current and two previous samples
+
+ phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f);
+ phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f);
+ phases[2] = phase[c / 4];
+
+ if (outputs[SINE_OUTPUT].isConnected() || outputs[EVEN_OUTPUT].isConnected()) {
+ // sin doesn't need PDW
+ osBufferSin[i] = -simd::cos(2.0 * M_PI * phase[c / 4]);
+ }
+
+ if (outputs[TRI_OUTPUT].isConnected()) {
+ const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0);
+ const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv;
+
+ osBufferTri[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ }
+
+ if (outputs[SAW_OUTPUT].isConnected()) {
+ const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0;
+ const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv;
+
+ osBufferSaw[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ }
+
+ if (outputs[SQUARE_OUTPUT].isConnected()) {
+
+ float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < pw, -1.0, +1.0);
+ dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f;
+
+ float_4 saw = aliasSuppressedSaw(phases);
+ float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw);
+ float_4 dpwOrder3 = (saw - sawOffset) * denominatorInv + pulseDCOffset;
+
+ osBufferSquare[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ }
+
+ if (outputs[EVEN_OUTPUT].isConnected()) {
+
+ float_4 dpwOrder1 = 4.0 * simd::ifelse(phase[c / 4] < 0.5, phase[c / 4], phase[c / 4] - 0.5) - 1.0;
+ float_4 dpwOrder3 = aliasSuppressedDoubleSaw(phases) * denominatorInv;
+ float_4 doubleSaw = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ osBufferEven[i] = 0.55 * (doubleSaw + 1.27 * osBufferSin[i]);
+ }
+
+
+ } // end of oversampling loop
+
+ // downsample (if required)
+ if (outputs[SINE_OUTPUT].isConnected()) {
+ const float_4 outSin = (oversamplingRatio > 1) ? oversampler[SINE_OUTPUT][c / 4].downsample() : osBufferSin[0];
+ outputs[SINE_OUTPUT].setVoltageSimd(5.f * outSin, c);
+ }
+
+ if (outputs[TRI_OUTPUT].isConnected()) {
+ const float_4 outTri = (oversamplingRatio > 1) ? oversampler[TRI_OUTPUT][c / 4].downsample() : osBufferTri[0];
+ outputs[TRI_OUTPUT].setVoltageSimd(5.f * outTri, c);
+ }
+
+ if (outputs[SAW_OUTPUT].isConnected()) {
+ const float_4 outSaw = (oversamplingRatio > 1) ? oversampler[SAW_OUTPUT][c / 4].downsample() : osBufferSaw[0];
+ outputs[SAW_OUTPUT].setVoltageSimd(5.f * outSaw, c);
+ }
+
+ if (outputs[SQUARE_OUTPUT].isConnected()) {
+ const float_4 outSquare = (oversamplingRatio > 1) ? oversampler[SQUARE_OUTPUT][c / 4].downsample() : osBufferSquare[0];
+ outputs[SQUARE_OUTPUT].setVoltageSimd(5.f * outSquare, c);
+ }
+
+ if (outputs[EVEN_OUTPUT].isConnected()) {
+ const float_4 outEven = (oversamplingRatio > 1) ? oversampler[EVEN_OUTPUT][c / 4].downsample() : osBufferEven[0];
+ outputs[EVEN_OUTPUT].setVoltageSimd(5.f * outEven, c);
+ }
+
+ } // end of channels loop
+
+ // Outputs
+ outputs[TRI_OUTPUT].setChannels(channels);
+ outputs[SINE_OUTPUT].setChannels(channels);
+ outputs[EVEN_OUTPUT].setChannels(channels);
+ 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));
+ json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
+ json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0][0].getOversamplingIndex()));
+ return rootJ;
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+ json_t* pulseDCJ = json_object_get(rootJ, "removePulseDC");
+ if (pulseDCJ) {
+ removePulseDC = json_boolean_value(pulseDCJ);
+ }
+
+ json_t* limitPWJ = json_object_get(rootJ, "limitPW");
+ if (limitPWJ) {
+ limitPW = json_boolean_value(limitPWJ);
+ }
+
+ json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
+ if (oversamplingIndexJ) {
+ oversamplingIndex = json_integer_value(oversamplingIndexJ);
+ onSampleRateChange();
+ }
+ }
+};
+
+
+struct EvenVCO2Widget : ModuleWidget {
+ EvenVCO2Widget(EvenVCO2* module) {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/EvenVCObeta.svg")));
+
+ addChild(createWidget(Vec(15, 0)));
+ addChild(createWidget(Vec(15, 365)));
+ addChild(createWidget(Vec(15 * 6, 0)));
+ addChild(createWidget(Vec(15 * 6, 365)));
+
+ addParam(createParam(Vec(22, 32), module, EvenVCO2::OCTAVE_PARAM));
+ addParam(createParam(Vec(73, 131), module, EvenVCO2::TUNE_PARAM));
+ addParam(createParam(Vec(16, 230), module, EvenVCO2::PWM_PARAM));
+
+ addInput(createInput(Vec(8, 120), module, EvenVCO2::PITCH1_INPUT));
+ addInput(createInput(Vec(19, 157), module, EvenVCO2::PITCH2_INPUT));
+ addInput(createInput(Vec(48, 183), module, EvenVCO2::FM_INPUT));
+ addInput(createInput(Vec(86, 189), module, EvenVCO2::SYNC_INPUT));
+
+ addInput(createInput(Vec(72, 236), module, EvenVCO2::PWM_INPUT));
+
+ addOutput(createOutput(Vec(10, 283), module, EvenVCO2::TRI_OUTPUT));
+ addOutput(createOutput(Vec(87, 283), module, EvenVCO2::SINE_OUTPUT));
+ addOutput(createOutput(Vec(48, 306), module, EvenVCO2::EVEN_OUTPUT));
+ addOutput(createOutput(Vec(10, 327), module, EvenVCO2::SAW_OUTPUT));
+ addOutput(createOutput(Vec(87, 327), module, EvenVCO2::SQUARE_OUTPUT));
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ EvenVCO2* 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));
+ menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW));
+ }
+ ));
+
+ menu->addChild(createIndexSubmenuItem("Oversampling",
+ {"Off", "x2", "x4", "x8"},
+ [ = ]() {
+ return module->oversamplingIndex;
+ },
+ [ = ](int mode) {
+ module->oversamplingIndex = mode;
+ module->onSampleRateChange();
+ }
+ ));
+ }
+};
+
+
+Model* modelEvenVCO2 = createModel("EvenVCO2");
diff --git a/src/Octaves.cpp b/src/Octaves.cpp
index bd88e24..d6cf2f5 100644
--- a/src/Octaves.cpp
+++ b/src/Octaves.cpp
@@ -98,6 +98,7 @@ struct Octaves : Module {
bool limitPW = true;
bool removePulseDC = false;
+ bool useTriangleCore = false;
static const int NUM_OUTPUTS = 6;
const float ranges[3] = {4.f, 1.f, 1.f / 12.f}; // full, octave, semitone
@@ -105,7 +106,7 @@ struct Octaves : Module {
chowdsp::VariableOversampling<6, float> oversampler[NUM_OUTPUTS]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling
- DCBlocker blockDCFilter; // optionally block DC with RC filter @ ~22 Hz
+ DCBlocker blockDCFilter[NUM_OUTPUTS]; // optionally block DC with RC filter @ ~22 Hz
dsp::SchmittTrigger syncTrigger; // for hard sync
Octaves() {
@@ -154,8 +155,8 @@ struct Octaves : Module {
for (int c = 0; c < NUM_OUTPUTS; c++) {
oversampler[c].setOversamplingIndex(oversamplingIndex);
oversampler[c].reset(sampleRate);
+ blockDCFilter[c].setFrequency(22.05 / sampleRate);
}
- blockDCFilter.setFrequency(22.05 / sampleRate);
}
@@ -164,7 +165,7 @@ struct Octaves : Module {
float pitch = ranges[rangeIndex] * params[TUNE_PARAM].getValue() + inputs[VOCT1_INPUT].getVoltage() + inputs[VOCT2_INPUT].getVoltage();
pitch += params[OCTAVE_PARAM].getValue() - 3;
- float freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
+ const float freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
// -1 to +1
const float pwmCV = params[PWM_CV_PARAM].getValue() * clamp(inputs[PWM_INPUT].getVoltage() / 10.f, -1.f, 1.f);
const float pulseWidthLimit = limitPW ? 0.05f : 0.0f;
@@ -206,7 +207,7 @@ struct Octaves : Module {
// build square from triangle + comparator
const float waveSquare = (waveTri > pwm) ? +1 : -1;
- sum += waveSquare * gain;
+ sum += (useTriangleCore ? waveTri : waveSquare) * gain;
sum = clamp(sum, -1.f, 1.f);
if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
@@ -224,7 +225,7 @@ struct Octaves : Module {
float out = (oversamplingRatio > 1) ? oversampler[c].downsample() : oversampler[c].getOSBuffer()[0];
if (removePulseDC) {
- out = blockDCFilter.process(out);
+ out = blockDCFilter[c].process(out);
}
outputs[OUT_01F_OUTPUT + c].setVoltage(5.f * out);
@@ -248,6 +249,8 @@ struct Octaves : Module {
json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC));
json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
+ json_object_set_new(rootJ, "useTriangleCore", json_boolean(useTriangleCore));
+
return rootJ;
}
@@ -268,6 +271,11 @@ struct Octaves : Module {
oversamplingIndex = json_integer_value(oversamplingIndexJ);
onSampleRateChange();
}
+
+ json_t* useTriangleCoreJ = json_object_get(rootJ, "useTriangleCore");
+ if (useTriangleCoreJ) {
+ useTriangleCore = json_boolean_value(useTriangleCoreJ);
+ }
}
};
@@ -322,6 +330,7 @@ struct OctavesWidget : ModuleWidget {
[ = ](Menu * menu) {
menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW));
menu->addChild(createBoolPtrMenuItem("Remove pulse DC", "", &module->removePulseDC));
+ menu->addChild(createBoolPtrMenuItem("Use triangle core", "", &module->useTriangleCore));
}
));
diff --git a/src/plugin.cpp b/src/plugin.cpp
index 90d4b02..3a9f56d 100644
--- a/src/plugin.cpp
+++ b/src/plugin.cpp
@@ -7,6 +7,7 @@ void init(rack::Plugin *p) {
pluginInstance = p;
p->addModel(modelEvenVCO);
+ p->addModel(modelEvenVCO2);
p->addModel(modelRampage);
p->addModel(modelABC);
p->addModel(modelSpringReverb);
diff --git a/src/plugin.hpp b/src/plugin.hpp
index f49104c..ad54826 100644
--- a/src/plugin.hpp
+++ b/src/plugin.hpp
@@ -8,6 +8,7 @@ using namespace rack;
extern Plugin* pluginInstance;
extern Model* modelEvenVCO;
+extern Model* modelEvenVCO2;
extern Model* modelRampage;
extern Model* modelABC;
extern Model* modelSpringReverb;