From 700a5cce98c9b35af04fece3d60f94e83f22bd71 Mon Sep 17 00:00:00 2001
From: hemmer <915048+hemmer@users.noreply.github.com>
Date: Mon, 25 Mar 2024 06:45:33 +0000
Subject: [PATCH] Add Octaves, remove experimental EvenVCO implementation
---
CHANGELOG.md | 3 +
plugin.json | 23 +-
res/panels/EvenVCObeta.svg | 1850 ----------------------------
res/panels/Octaves.svg | 2383 ++++++++++++++++++++++++++++++++++++
src/EvenVCO2.cpp | 331 -----
src/Octaves.cpp | 344 ++++++
src/plugin.cpp | 2 +-
src/plugin.hpp | 2 +-
8 files changed, 2743 insertions(+), 2195 deletions(-)
delete mode 100644 res/panels/EvenVCObeta.svg
create mode 100644 res/panels/Octaves.svg
delete mode 100644 src/EvenVCO2.cpp
create mode 100644 src/Octaves.cpp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f239fcd..1eaeea3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
## v2.6.0 (in progress)
* Midi Thing 2
* Initial release
+ * Octaves
+ * Initial release
+
## v2.5.0
* Burst
diff --git a/plugin.json b/plugin.json
index 29d15db..08861d0 100644
--- a/plugin.json
+++ b/plugin.json
@@ -23,18 +23,6 @@
"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",
@@ -331,6 +319,17 @@
"Polyphonic",
"Utility"
]
+ },
+ {
+ "slug": "Octaves",
+ "name": "Octaves",
+ "description": "A harsh and funky take of an additive Oscillator.",
+ "manualUrl": "https://www.befaco.org/octaves-vco/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-octaves-vco",
+ "tags": [
+ "Hardware clone",
+ "VCO"
+ ]
}
]
}
\ No newline at end of file
diff --git a/res/panels/EvenVCObeta.svg b/res/panels/EvenVCObeta.svg
deleted file mode 100644
index 770d19f..0000000
--- a/res/panels/EvenVCObeta.svg
+++ /dev/null
@@ -1,1850 +0,0 @@
-
-
-
-
diff --git a/res/panels/Octaves.svg b/res/panels/Octaves.svg
new file mode 100644
index 0000000..2f99793
--- /dev/null
+++ b/res/panels/Octaves.svg
@@ -0,0 +1,2383 @@
+
+
diff --git a/src/EvenVCO2.cpp b/src/EvenVCO2.cpp
deleted file mode 100644
index 7886ff6..0000000
--- a/src/EvenVCO2.cpp
+++ /dev/null
@@ -1,331 +0,0 @@
-#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
new file mode 100644
index 0000000..33f53e3
--- /dev/null
+++ b/src/Octaves.cpp
@@ -0,0 +1,344 @@
+#include "plugin.hpp"
+
+
+float aliasSuppressedSaw(const float* phases, float pw) {
+ float sawBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float p = 2 * phases[i] - 1.0; // range -1 to +1
+ float pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
+ pwp += simd::ifelse(pwp > 1, -2, simd::ifelse(pwp < -1, +2, 0)); // modulo on [-1, +1]
+ sawBuffer[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
+ }
+
+ return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
+}
+
+float aliasSuppressedOffsetSaw(const float* phases, float pw) {
+ float sawOffsetBuff[3];
+
+ for (int i = 0; i < 3; ++i) {
+ float pwp = 2 * phases[i] - 2 * pw; // range -1 to +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]);
+}
+
+
+struct Octaves : Module {
+ enum ParamId {
+ PWM_CV_PARAM,
+ OCTAVE_PARAM,
+ TUNE_PARAM,
+ PWM_PARAM,
+ RANGE_PARAM,
+ GAIN_01F_PARAM,
+ GAIN_02F_PARAM,
+ GAIN_04F_PARAM,
+ GAIN_08F_PARAM,
+ GAIN_16F_PARAM,
+ GAIN_32F_PARAM,
+ PARAMS_LEN
+ };
+ enum InputId {
+ VOCT1_INPUT,
+ VOCT2_INPUT,
+ SYNC_INPUT,
+ PWM_INPUT,
+ GAIN_01F_INPUT,
+ GAIN_02F_INPUT,
+ GAIN_04F_INPUT,
+ GAIN_08F_INPUT,
+ GAIN_16F_INPUT,
+ GAIN_32F_INPUT,
+ INPUTS_LEN
+ };
+ enum OutputId {
+ OUT_01F_OUTPUT,
+ OUT_02F_OUTPUT,
+ OUT_04F_OUTPUT,
+ OUT_08F_OUTPUT,
+ OUT_16F_OUTPUT,
+ OUT_32F_OUTPUT,
+ OUT_OUTPUT,
+ OUT2_OUTPUT,
+ OUT_01F_OUTPUT_ALT,
+ OUT_02F_OUTPUT_ALT,
+ OUT_04F_OUTPUT_ALT,
+ OUT_08F_OUTPUT_ALT,
+ OUT_16F_OUTPUT_ALT,
+ OUT_32F_OUTPUT_ALT,
+ OUTPUTS_LEN
+ };
+ enum LightId {
+ LIGHTS_LEN
+ };
+
+ bool limitPW = true;
+ bool removePulseDC = false;
+ int oversamplingIndex = 0;
+
+ Octaves() {
+ config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
+ configParam(PWM_CV_PARAM, 0.f, 1.f, 1.f, "PWM CV attenuater");
+
+ auto octParam = configSwitch(OCTAVE_PARAM, 0.f, 6.f, 4.f, "Octave", {"C1", "C2", "C3", "C4", "C5", "C6", "C7"});
+ octParam->snapEnabled = true;
+
+ configParam(TUNE_PARAM, -1.f, 1.f, 0.f, "Tune");
+ configParam(PWM_PARAM, 0.5f, 0.f, 0.5f, "PWM");
+ auto rangeParam = configSwitch(RANGE_PARAM, 0.f, 2.f, 0.f, "Range", {"VCO: Full", "VCO: Octave", "VCO: Semitone"});
+ rangeParam->snapEnabled = true;
+
+ configParam(GAIN_01F_PARAM, 0.f, 1.f, 0.f, "Gain Fundamental");
+ configParam(GAIN_02F_PARAM, 0.f, 1.f, 0.f, "Gain x2 Fundamental");
+ configParam(GAIN_04F_PARAM, 0.f, 1.f, 0.f, "Gain x4 Fundamental");
+ configParam(GAIN_08F_PARAM, 0.f, 1.f, 0.f, "Gain x8 Fundamental");
+ configParam(GAIN_16F_PARAM, 0.f, 1.f, 0.f, "Gain x16 Fundamental");
+ configParam(GAIN_32F_PARAM, 0.f, 1.f, 0.f, "Gain x32 Fundamental");
+
+ configInput(VOCT1_INPUT, "V/Octave 1");
+ configInput(VOCT2_INPUT, "V/Octave 2");
+ configInput(SYNC_INPUT, "Sync");
+ configInput(PWM_INPUT, "PWM");
+ configInput(GAIN_01F_INPUT, "Gain x1F CV");
+ configInput(GAIN_02F_INPUT, "Gain x1F CV");
+ configInput(GAIN_04F_INPUT, "Gain x1F CV");
+ configInput(GAIN_08F_INPUT, "Gain x1F CV");
+ configInput(GAIN_16F_INPUT, "Gain x1F CV");
+ configInput(GAIN_32F_INPUT, "Gain x1F CV");
+
+ configOutput(OUT_01F_OUTPUT, "x1F");
+ configOutput(OUT_02F_OUTPUT, "x2F");
+ configOutput(OUT_04F_OUTPUT, "x4F");
+ configOutput(OUT_08F_OUTPUT, "x8F");
+ configOutput(OUT_16F_OUTPUT, "x16F");
+ configOutput(OUT_32F_OUTPUT, "x32F");
+ configOutput(OUT_OUTPUT, "debug");
+ }
+
+ float phase = 0.f;
+ float phases[3];
+ bool forceNaive = false;
+
+ void process(const ProcessArgs& args) override {
+
+ float pitch = 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);
+ // -1 to +1
+ 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;
+
+ // pwm in [-0.25 : +0.25]
+ float pwm = clamp(0.5 - params[PWM_PARAM].getValue() + 0.5 * pwmCV, -0.5f + pulseWidthLimit, 0.5f - pulseWidthLimit);
+ pwm /= 2.0;
+
+
+ float deltaPhase = freq * args.sampleTime;
+ phase += deltaPhase;
+ phase -= std::floor(phase);
+
+ float sum = 0.f;
+ float sumNaive = 0.f;
+ for (int c = 0; c < 6; c++) {
+ // derive phases for higher octaves from base phase (this keeps things in sync!)
+ const float n = (float)(1 << c);
+ // this is on [0, 1]
+ const float effectivePhaseRaw = n * std::fmod(phase, 1 / n);
+ // this is on [0, 1], and offset in time by 0.25
+ const float effectivePhase = std::fmod(effectivePhaseRaw + 0.25, 1);
+
+ const float effectiveDeltaPhase = deltaPhase * n;
+ const float gainCV = clamp(inputs[GAIN_01F_INPUT + c].getNormalVoltage(10.f) / 10.f, 0.f, 1.0f);
+ const float gain = params[GAIN_01F_PARAM + c].getValue() * gainCV;
+
+ // 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 bool lowFreqRegime = forceNaive; //effectiveDeltaPhase < 1e-3 || forceNaive;
+
+
+ //float waveTri = 1.0 - 2.0 * std::abs(2.f * effectivePhase - 1.0);
+ // float dpwOrder1 = (waveTri > 2 * pwm - 1) ? 1.0 : -1.0;
+
+ float dpwOrder1 = gain * (effectivePhaseRaw > pwm + 0.25 && effectivePhaseRaw < 0.75 - pwm ? -1.0 : +1.0);
+ dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pwm) : 0.f;
+
+ // dpwOrder1 = waveTri * gain;
+
+ sumNaive += dpwOrder1;
+
+ outputs[OUT_01F_OUTPUT_ALT + c].setVoltage(dpwOrder1);
+
+ float outForOctave = dpwOrder1;
+
+ if (!lowFreqRegime) {
+ phases[0] = effectivePhase - 2 * effectiveDeltaPhase + (effectivePhase < 2 * effectiveDeltaPhase ? 1.f : 0.f);
+ phases[1] = effectivePhase - 1 * effectiveDeltaPhase + (effectivePhase < 1 * effectiveDeltaPhase ? 1.f : 0.f);
+ phases[2] = effectivePhase;
+
+
+ float saw = aliasSuppressedSaw(phases, pwm);
+ float sawOffset = aliasSuppressedOffsetSaw(phases, pwm);
+ float denominatorInv = 0.25 / (effectiveDeltaPhase * effectiveDeltaPhase);
+ float dpwOrder3 = gain * (sawOffset - saw) * denominatorInv;
+
+ const float pulseDCOffset = (!removePulseDC) * 4.f * pwm * gain;
+ dpwOrder3 += pulseDCOffset;
+
+
+ outForOctave = dpwOrder3;
+ }
+
+
+ sum += outForOctave;
+ sum = clamp(sum, -1.f, 1.f);
+
+ if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
+ outputs[OUT_01F_OUTPUT + c].setVoltage(5 * sum);
+ sum = 0.f;
+ }
+
+ if (c == 0) {
+ outputs[OUT_OUTPUT].setVoltage(effectivePhase);
+
+ float saw = aliasSuppressedSaw(phases, 2*pwm);
+ float sawOffset = aliasSuppressedOffsetSaw(phases, 2*pwm);
+ float denominatorInv = 0.25 / (effectiveDeltaPhase * effectiveDeltaPhase);
+ float dpwOrder3_ = gain * (-saw) * denominatorInv;
+
+ outputs[OUT2_OUTPUT].setVoltage(dpwOrder3_);
+ }
+
+
+ }
+
+ //outputs[OUT_OUTPUT].setVoltage(sum);
+ //outputs[OUT2_OUTPUT].setVoltage(phase > 0.5 ? +5 : -5);
+
+ }
+
+
+ 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, "forceNaive", json_boolean(forceNaive));
+ // TODO:
+ // json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
+ return rootJ;
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+
+ json_t* removePulseDCJ = json_object_get(rootJ, "removePulseDC");
+ if (removePulseDCJ) {
+ removePulseDC = json_boolean_value(removePulseDCJ);
+ }
+
+ json_t* limitPWJ = json_object_get(rootJ, "limitPW");
+ if (limitPWJ) {
+ limitPW = json_boolean_value(limitPWJ);
+ }
+
+ json_t* forceNaiveJ = json_object_get(rootJ, "forceNaive");
+ if (forceNaiveJ) {
+ forceNaive = json_boolean_value(forceNaiveJ);
+ }
+
+ json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
+ if (oversamplingIndexJ) {
+ oversamplingIndex = json_integer_value(oversamplingIndexJ);
+ onSampleRateChange();
+ }
+ }
+};
+
+
+
+
+struct OctavesWidget : ModuleWidget {
+ OctavesWidget(Octaves* module) {
+ setModule(module);
+ setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Octaves.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(createParamCentered(mm2px(Vec(52.138, 15.037)), module, Octaves::PWM_CV_PARAM));
+ addParam(createParam(mm2px(Vec(22.171, 30.214)), module, Octaves::OCTAVE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(10.264, 33.007)), module, Octaves::TUNE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(45.384, 40.528)), module, Octaves::PWM_PARAM));
+ addParam(createParam(mm2px(Vec(6.023, 48.937)), module, Octaves::RANGE_PARAM));
+ addParam(createParam(mm2px(Vec(2.9830, 60.342)), module, Octaves::GAIN_01F_PARAM));
+ addParam(createParam(mm2px(Vec(12.967, 60.342)), module, Octaves::GAIN_02F_PARAM));
+ addParam(createParam(mm2px(Vec(22.951, 60.342)), module, Octaves::GAIN_04F_PARAM));
+ addParam(createParam(mm2px(Vec(32.936, 60.342)), module, Octaves::GAIN_08F_PARAM));
+ addParam(createParam(mm2px(Vec(42.920, 60.342)), module, Octaves::GAIN_16F_PARAM));
+ addParam(createParam(mm2px(Vec(52.905, 60.342)), module, Octaves::GAIN_32F_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(5.247, 15.181)), module, Octaves::VOCT1_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.282, 15.181)), module, Octaves::VOCT2_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.316, 15.181)), module, Octaves::SYNC_INPUT));
+ addInput(createInputCentered(mm2px(Vec(37.092, 15.135)), module, Octaves::PWM_INPUT));
+ addInput(createInputCentered(mm2px(Vec(5.247, 100.492)), module, Octaves::GAIN_01F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.282, 100.492)), module, Octaves::GAIN_02F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.316, 100.492)), module, Octaves::GAIN_04F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(35.35, 100.492)), module, Octaves::GAIN_08F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(45.384, 100.492)), module, Octaves::GAIN_16F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(55.418, 100.492)), module, Octaves::GAIN_32F_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(5.247, 113.508)), module, Octaves::OUT_01F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(15.282, 113.508)), module, Octaves::OUT_02F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(25.316, 113.508)), module, Octaves::OUT_04F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(35.35, 113.508)), module, Octaves::OUT_08F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(45.384, 113.508)), module, Octaves::OUT_16F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(55.418, 113.508)), module, Octaves::OUT_32F_OUTPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(25.316, 106.508)), module, Octaves::OUT_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(35.316, 106.508)), module, Octaves::OUT2_OUTPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(5.247, 120.508)), module, Octaves::OUT_01F_OUTPUT_ALT));
+ addOutput(createOutputCentered(mm2px(Vec(15.282, 120.508)), module, Octaves::OUT_02F_OUTPUT_ALT));
+ addOutput(createOutputCentered(mm2px(Vec(25.316, 120.508)), module, Octaves::OUT_04F_OUTPUT_ALT));
+ addOutput(createOutputCentered(mm2px(Vec(35.35, 120.508)), module, Octaves::OUT_08F_OUTPUT_ALT));
+ addOutput(createOutputCentered(mm2px(Vec(45.384, 120.508)), module, Octaves::OUT_16F_OUTPUT_ALT));
+ addOutput(createOutputCentered(mm2px(Vec(55.418, 120.508)), module, Octaves::OUT_32F_OUTPUT_ALT));
+
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ Octaves* module = dynamic_cast(this->module);
+ assert(module);
+
+ menu->addChild(new MenuSeparator());
+ menu->addChild(createSubmenuItem("Hardware compatibility", "",
+ [ = ](Menu * menu) {
+ 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();
+ }
+ ));
+
+ menu->addChild(createBoolPtrMenuItem("Force naive waveforms", "", &module->forceNaive));
+
+
+ }
+};
+
+
+Model* modelOctaves = createModel("Octaves");
\ No newline at end of file
diff --git a/src/plugin.cpp b/src/plugin.cpp
index 378cbf8..90d4b02 100644
--- a/src/plugin.cpp
+++ b/src/plugin.cpp
@@ -7,7 +7,6 @@ void init(rack::Plugin *p) {
pluginInstance = p;
p->addModel(modelEvenVCO);
- p->addModel(modelEvenVCO2);
p->addModel(modelRampage);
p->addModel(modelABC);
p->addModel(modelSpringReverb);
@@ -31,4 +30,5 @@ void init(rack::Plugin *p) {
p->addModel(modelBurst);
p->addModel(modelMidiThing);
p->addModel(modelVoltio);
+ p->addModel(modelOctaves);
}
diff --git a/src/plugin.hpp b/src/plugin.hpp
index 8e71739..f49104c 100644
--- a/src/plugin.hpp
+++ b/src/plugin.hpp
@@ -8,7 +8,6 @@ using namespace rack;
extern Plugin* pluginInstance;
extern Model* modelEvenVCO;
-extern Model* modelEvenVCO2;
extern Model* modelRampage;
extern Model* modelABC;
extern Model* modelSpringReverb;
@@ -32,6 +31,7 @@ extern Model* modelMotionMTR;
extern Model* modelBurst;
extern Model* modelMidiThing;
extern Model* modelVoltio;
+extern Model* modelOctaves;
struct Knurlie : SvgScrew {
Knurlie() {