diff --git a/README.md b/README.md
index f316620..c69463a 100644
--- a/README.md
+++ b/README.md
@@ -2,45 +2,4 @@
Based on [Befaco](http://www.befaco.org/) Eurorack modules.
-### EvenVCO
-
-Based on [EvenVCO](http://www.befaco.org/even-vco/)
-
-
-
-### Rampage
-
-Based on [Rampage](http://www.befaco.org/rampage-2/), [Manual PDF](https://befaco.org/docs/Rampage/Rampage_User_Manual.pdf)
-
-
-
-### A\*B+C
-
-Based on [A\*B+C](http://www.befaco.org/abc/), [Manual PDF](https://befaco.org/docs/AB%2BC/AB%2BC_User_Manual.pdf)
-
-
-
-### Spring Reverb
-
-Based on [Spring Reverb](http://www.befaco.org/spring-reverb/)
-
-
-
-### Mixer
-
-Based on [Mixer](http://www.befaco.org/mixer-2/)
-
-
-
-### Slew Limiter
-
-Based on [Slew Limiter](http://www.befaco.org/slew-limiter/)
-
-
-
-
-### Dual Atenuverter
-
-Based on [Dual Atenuverter](http://www.befaco.org/dual-atenuverter/)
-
-
+[VCV Library page](https://library.vcvrack.com/Befaco)
\ No newline at end of file
diff --git a/plugin.json b/plugin.json
index ce6d69d..309c11f 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,93 +1,146 @@
-{
- "slug": "Befaco",
- "version": "1.0.1",
- "license": "GPL-3.0-or-later",
- "name": "Befaco",
- "author": "VCV",
- "authorEmail": "contact@vcvrack.com",
- "pluginUrl": "https://vcvrack.com/Befaco.html",
- "authorUrl": "https://vcvrack.com/",
- "sourceUrl": "https://github.com/VCVRack/Befaco",
- "modules": [
- {
- "slug": "EvenVCO",
- "name": "EvenVCO",
- "description": "Oscillator including even-harmonic waveform",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-",
- "tags": [
- "VCO",
- "Hardware clone"
- ]
- },
- {
- "slug": "Rampage",
- "name": "Rampage",
- "description": "Dual ramp generator",
- "manualUrl": "https://befaco.org/docs/Rampage/Rampage_User_Manual.pdf",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-rampage",
- "tags": [
- "Function Generator",
- "Logic",
- "Slew Limiter",
- "Envelope Follower",
- "Dual",
- "Hardware clone"
- ]
- },
- {
- "slug": "ABC",
- "name": "A*B+C",
- "description": "Dual four-quadrant multiplier with VC offset",
- "manualUrl": "https://befaco.org/docs/AB%2BC/AB%2BC_User_Manual.pdf",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-a-b-c",
- "tags": [
- "Ring Modulator",
- "Attenuator",
- "Dual",
- "Hardware clone"
- ]
- },
- {
- "slug": "SpringReverb",
- "name": "Spring Reverb",
- "description": "Spring reverb tank driver",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-spring-reverb-",
- "tags": [
- "Reverb",
- "Hardware clone"
- ]
- },
- {
- "slug": "Mixer",
- "name": "Mixer",
- "description": "Four-channel mixer for audio or CV",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-mixer-",
- "tags": [
- "Mixer",
- "Hardware clone"
- ]
- },
- {
- "slug": "SlewLimiter",
- "name": "Slew Limiter",
- "description": "Voltage controlled slew limiter, AKA lag processor",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-slew-limiter-",
- "tags": [
- "Slew Limiter",
- "Envelope Follower",
- "Hardware clone"
- ]
- },
- {
- "slug": "DualAtenuverter",
- "name": "Dual Atenuverter",
- "description": "Attenuates, inverts, and applies offset to a signal",
- "modularGridUrl": "https://www.modulargrid.net/e/befaco-dual-atenuverter-",
- "tags": [
- "Attenuator",
- "Dual",
- "Hardware clone"
- ]
- }
- ]
-}
\ No newline at end of file
+{
+ "slug": "Befaco",
+ "version": "1.1.0",
+ "license": "GPL-3.0-or-later",
+ "name": "Befaco",
+ "author": "VCV",
+ "authorEmail": "contact@vcvrack.com",
+ "pluginUrl": "https://vcvrack.com/Befaco.html",
+ "authorUrl": "https://vcvrack.com/",
+ "sourceUrl": "https://github.com/VCVRack/Befaco",
+ "modules": [
+ {
+ "slug": "EvenVCO",
+ "name": "EvenVCO",
+ "description": "Oscillator including even-harmonic waveform",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-",
+ "tags": [
+ "VCO",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "Rampage",
+ "name": "Rampage",
+ "description": "Dual ramp generator",
+ "manualUrl": "https://befaco.org/docs/Rampage/Rampage_User_Manual.pdf",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-rampage",
+ "tags": [
+ "Function Generator",
+ "Logic",
+ "Slew Limiter",
+ "Envelope Follower",
+ "Dual",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "ABC",
+ "name": "A*B+C",
+ "description": "Dual four-quadrant multiplier with VC offset",
+ "manualUrl": "https://befaco.org/docs/AB%2BC/AB%2BC_User_Manual.pdf",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-a-b-c",
+ "tags": [
+ "Ring Modulator",
+ "Attenuator",
+ "Dual",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "SpringReverb",
+ "name": "Spring Reverb",
+ "description": "Spring reverb tank driver",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-spring-reverb-",
+ "tags": [
+ "Reverb",
+ "Hardware clone"
+ ]
+ },
+ {
+ "slug": "Mixer",
+ "name": "Mixer",
+ "description": "Four-channel mixer for audio or CV",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-mixer-",
+ "tags": [
+ "Mixer",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "SlewLimiter",
+ "name": "Slew Limiter",
+ "description": "Voltage controlled slew limiter, AKA lag processor",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-slew-limiter-",
+ "tags": [
+ "Slew Limiter",
+ "Envelope Follower",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "DualAtenuverter",
+ "name": "Dual Atenuverter",
+ "description": "Attenuates, inverts, and applies offset to a signal",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-dual-atenuverter-",
+ "tags": [
+ "Attenuator",
+ "Dual",
+ "Hardware clone",
+ "Polyphonic"
+ ]
+ },
+ {
+ "slug": "Percall",
+ "name": "Percall",
+ "description": "Percussive Envelope Generator",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-percall",
+ "tags": [
+ "Envelope generator",
+ "Mixer",
+ "Polyphonic",
+ "Hardware clone"
+ ]
+ },
+ {
+ "slug": "HexmixVCA",
+ "name": "HexmixVCA",
+ "description": "Six channel VCA with response curve range from logarithmic to linear and to exponential",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-hexmix-vca",
+ "tags": [
+ "Mixer",
+ "Hardware clone",
+ "Polyphonic",
+ "VCA"
+ ]
+ },
+ {
+ "slug": "ChoppingKinky",
+ "name": "ChoppingKinky",
+ "description": "Voltage controllable, dual channel wavefolder",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-chopping-kinky",
+ "tags": [
+ "Dual",
+ "Hardware clone",
+ "Voltage-controlled amplifier",
+ "Waveshaper"
+ ]
+ },
+ {
+ "slug": "Kickall",
+ "name": "Kickall",
+ "description": "Bassdrum module, with pitch and volume envelopes",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-kickall",
+ "tags": [
+ "Drum",
+ "Hardware clone",
+ "Synth voice"
+ ]
+ }
+ ]
+}
diff --git a/res/BefacoInputPort.svg b/res/BefacoInputPort.svg
new file mode 100644
index 0000000..407827a
--- /dev/null
+++ b/res/BefacoInputPort.svg
@@ -0,0 +1,210 @@
+
+
diff --git a/res/BefacoOutputPort.svg b/res/BefacoOutputPort.svg
new file mode 100644
index 0000000..3248287
--- /dev/null
+++ b/res/BefacoOutputPort.svg
@@ -0,0 +1,210 @@
+
+
diff --git a/res/BefacoTinyKnobGrey.svg b/res/BefacoTinyKnobGrey.svg
new file mode 100644
index 0000000..6954d84
--- /dev/null
+++ b/res/BefacoTinyKnobGrey.svg
@@ -0,0 +1,85 @@
+
+
diff --git a/res/BefacoTinyKnobRed.svg b/res/BefacoTinyKnobRed.svg
new file mode 100644
index 0000000..2fea4c0
--- /dev/null
+++ b/res/BefacoTinyKnobRed.svg
@@ -0,0 +1,85 @@
+
+
diff --git a/res/ChoppingKinky.svg b/res/ChoppingKinky.svg
new file mode 100644
index 0000000..4f17f35
--- /dev/null
+++ b/res/ChoppingKinky.svg
@@ -0,0 +1,1064 @@
+
+
diff --git a/res/Davies1900hLargeGrey.svg b/res/Davies1900hLargeGrey.svg
new file mode 100644
index 0000000..0e5c95a
--- /dev/null
+++ b/res/Davies1900hLargeGrey.svg
@@ -0,0 +1,105 @@
+
+
diff --git a/res/HexmixVCA.svg b/res/HexmixVCA.svg
new file mode 100644
index 0000000..6c7606a
--- /dev/null
+++ b/res/HexmixVCA.svg
@@ -0,0 +1,1371 @@
+
+
diff --git a/res/Kickall.svg b/res/Kickall.svg
new file mode 100644
index 0000000..99fc77a
--- /dev/null
+++ b/res/Kickall.svg
@@ -0,0 +1,1016 @@
+
+
diff --git a/res/Percall.svg b/res/Percall.svg
new file mode 100644
index 0000000..3fdfb5d
--- /dev/null
+++ b/res/Percall.svg
@@ -0,0 +1,1497 @@
+
+
diff --git a/src/ABC.cpp b/src/ABC.cpp
index 0655814..91afc15 100644
--- a/src/ABC.cpp
+++ b/src/ABC.cpp
@@ -1,17 +1,20 @@
#include "plugin.hpp"
+using simd::float_4;
-inline float clip(float x) {
+template
+static T clip4(T x) {
// Pade approximant of x/(1 + x^12)^(1/12)
- const float limit = 1.16691853009184f;
- x = clamp(x, -limit, limit);
- return (x + 1.45833f * std::pow(x, 13) + 0.559028f * std::pow(x, 25) + 0.0427035f * std::pow(x, 37))
- / (1 + 1.54167f * std::pow(x, 12) + 0.642361f * std::pow(x, 24) + 0.0579909f * std::pow(x, 36));
+ const T limit = 1.16691853009184f;
+ x = clamp(x * 0.1f, -limit, limit);
+ return 10.0f * (x + 1.45833f * simd::pow(x, 13) + 0.559028f * simd::pow(x, 25) + 0.0427035f * simd::pow(x, 37))
+ / (1.0f + 1.54167f * simd::pow(x, 12) + 0.642361f * simd::pow(x, 24) + 0.0579909f * simd::pow(x, 36));
}
-inline float exponentialBipolar80Pade_5_4(float x) {
- return (0.109568f * x + 0.281588f * std::pow(x, 3) + 0.133841f * std::pow(x, 5))
- / (1 - 0.630374f * std::pow(x, 2) + 0.166271f * std::pow(x, 4));
+
+static float exponentialBipolar80Pade_5_4(float x) {
+ return (0.109568 * x + 0.281588 * std::pow(x, 3) + 0.133841 * std::pow(x, 5))
+ / (1. - 0.630374 * std::pow(x, 2) + 0.166271 * std::pow(x, 4));
}
@@ -38,8 +41,8 @@ struct ABC : Module {
NUM_OUTPUTS
};
enum LightIds {
- ENUMS(OUT1_LIGHT, 2),
- ENUMS(OUT2_LIGHT, 2),
+ ENUMS(OUT1_LIGHT, 3),
+ ENUMS(OUT2_LIGHT, 3),
NUM_LIGHTS
};
@@ -51,39 +54,121 @@ struct ABC : Module {
configParam(C2_LEVEL_PARAM, -1.0, 1.0, 0.0, "C2 Level");
}
- void process(const ProcessArgs &args) override {
- float a1 = inputs[A1_INPUT].getVoltage();
- float b1 = inputs[B1_INPUT].getNormalVoltage(5.f) * 2.f*exponentialBipolar80Pade_5_4(params[B1_LEVEL_PARAM].getValue());
- float c1 = inputs[C1_INPUT].getNormalVoltage(10.f) * exponentialBipolar80Pade_5_4(params[C1_LEVEL_PARAM].getValue());
- float out1 = a1 * b1 / 5.f + c1;
+ int processSection(simd::float_4* out, InputIds inputA, InputIds inputB, InputIds inputC, ParamIds levelB, ParamIds levelC) {
- float a2 = inputs[A2_INPUT].getVoltage();
- float b2 = inputs[B2_INPUT].getNormalVoltage(5.f) * 2.f*exponentialBipolar80Pade_5_4(params[B2_LEVEL_PARAM].getValue());
- float c2 = inputs[C2_INPUT].getNormalVoltage(10.f) * exponentialBipolar80Pade_5_4(params[C2_LEVEL_PARAM].getValue());
- float out2 = a2 * b2 / 5.f + c2;
+ float_4 inA[4] = {};
+ float_4 inB[4] = {};
+ float_4 inC[4] = {};
- // Set outputs
- if (outputs[OUT1_OUTPUT].isConnected()) {
- outputs[OUT1_OUTPUT].setVoltage(clip(out1 / 10.f) * 10.f);
+ int channelsA = inputs[inputA].getChannels();
+ int channelsB = inputs[inputB].getChannels();
+ int channelsC = inputs[inputC].getChannels();
+
+ // this sets the number of active engines (according to polyphony standard)
+ // NOTE: A*B + C has the number of active engines set by any one of the three inputs
+ int activeEngines = std::max(1, channelsA);
+ activeEngines = std::max(activeEngines, channelsB);
+ activeEngines = std::max(activeEngines, channelsC);
+
+ float mult_B = (2.f / 5.f) * exponentialBipolar80Pade_5_4(params[levelB].getValue());
+ float mult_C = exponentialBipolar80Pade_5_4(params[levelC].getValue());
+
+ if (inputs[inputA].isConnected()) {
+ for (int c = 0; c < activeEngines; c += 4)
+ inA[c / 4] = inputs[inputA].getPolyVoltageSimd(c);
+ }
+
+ if (inputs[inputB].isConnected()) {
+ for (int c = 0; c < activeEngines; c += 4)
+ inB[c / 4] = inputs[inputB].getPolyVoltageSimd(c) * mult_B;
+ }
+ else {
+ for (int c = 0; c < activeEngines; c += 4)
+ inB[c / 4] = 5.f * mult_B;
+ }
+
+ if (inputs[inputC].isConnected()) {
+ for (int c = 0; c < activeEngines; c += 4)
+ inC[c / 4] = inputs[inputC].getPolyVoltageSimd(c) * mult_C;
}
else {
- out2 += out1;
+ for (int c = 0; c < activeEngines; c += 4)
+ inC[c / 4] = 10.f * mult_C;
+ }
+
+ for (int c = 0; c < activeEngines; c += 4)
+ out[c / 4] = clip4(inA[c / 4] * inB[c / 4] + inC[c / 4]);
+
+ return activeEngines;
+ }
+
+ void process(const ProcessArgs& args) override {
+
+ // process upper section
+ float_4 out1[4] = {};
+ int activeEngines1 = 1;
+ if (outputs[OUT1_OUTPUT].isConnected() || outputs[OUT2_OUTPUT].isConnected()) {
+ activeEngines1 = processSection(out1, A1_INPUT, B1_INPUT, C1_INPUT, B1_LEVEL_PARAM, C1_LEVEL_PARAM);
}
+
+ float_4 out2[4] = {};
+ int activeEngines2 = 1;
+ // process lower section
if (outputs[OUT2_OUTPUT].isConnected()) {
- outputs[OUT2_OUTPUT].setVoltage(clip(out2 / 10.f) * 10.f);
+ activeEngines2 = processSection(out2, A2_INPUT, B2_INPUT, C2_INPUT, B2_LEVEL_PARAM, C2_LEVEL_PARAM);
+ }
+
+ // Set outputs
+ if (outputs[OUT1_OUTPUT].isConnected()) {
+ outputs[OUT1_OUTPUT].setChannels(activeEngines1);
+ for (int c = 0; c < activeEngines1; c += 4)
+ outputs[OUT1_OUTPUT].setVoltageSimd(out1[c / 4], c);
+ }
+ else if (outputs[OUT2_OUTPUT].isConnected()) {
+
+ for (int c = 0; c < activeEngines1; c += 4)
+ out2[c / 4] += out1[c / 4];
+
+ activeEngines2 = std::max(activeEngines1, activeEngines2);
+
+ outputs[OUT2_OUTPUT].setChannels(activeEngines2);
+ for (int c = 0; c < activeEngines2; c += 4)
+ outputs[OUT2_OUTPUT].setVoltageSimd(out2[c / 4], c);
}
// Lights
- lights[OUT1_LIGHT + 0].setSmoothBrightness(out1 / 5.f, args.sampleTime);
- lights[OUT1_LIGHT + 1].setSmoothBrightness(-out1 / 5.f, args.sampleTime);
- lights[OUT2_LIGHT + 0].setSmoothBrightness(out2 / 5.f, args.sampleTime);
- lights[OUT2_LIGHT + 1].setSmoothBrightness(-out2 / 5.f, args.sampleTime);
+
+ if (activeEngines1 == 1) {
+ float b = out1[0].s[0];
+ lights[OUT1_LIGHT + 0].setSmoothBrightness(b / 5.f, args.sampleTime);
+ lights[OUT1_LIGHT + 1].setSmoothBrightness(-b / 5.f, args.sampleTime);
+ lights[OUT1_LIGHT + 2].setBrightness(0.f);
+ }
+ else {
+ float b = 10.f;
+ lights[OUT1_LIGHT + 0].setBrightness(0.0f);
+ lights[OUT1_LIGHT + 1].setBrightness(0.0f);
+ lights[OUT1_LIGHT + 2].setBrightness(b);
+ }
+
+ if (activeEngines2 == 1) {
+ float b = out2[0].s[0];
+ lights[OUT2_LIGHT + 0].setSmoothBrightness(b / 5.f, args.sampleTime);
+ lights[OUT2_LIGHT + 1].setSmoothBrightness(-b / 5.f, args.sampleTime);
+ lights[OUT2_LIGHT + 2].setBrightness(0.f);
+ }
+ else {
+ float b = 10.f;
+ lights[OUT2_LIGHT + 0].setBrightness(0.0f);
+ lights[OUT2_LIGHT + 1].setBrightness(0.0f);
+ lights[OUT2_LIGHT + 2].setBrightness(b);
+ }
}
};
struct ABCWidget : ModuleWidget {
- ABCWidget(ABC *module) {
+ ABCWidget(ABC* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ABC.svg")));
@@ -95,19 +180,19 @@ struct ABCWidget : ModuleWidget {
addParam(createParam(Vec(45, 204), module, ABC::B2_LEVEL_PARAM));
addParam(createParam(Vec(45, 274), module, ABC::C2_LEVEL_PARAM));
- addInput(createInput(Vec(7, 28), module, ABC::A1_INPUT));
- addInput(createInput(Vec(7, 70), module, ABC::B1_INPUT));
- addInput(createInput(Vec(7, 112), module, ABC::C1_INPUT));
- addOutput(createOutput(Vec(7, 154), module, ABC::OUT1_OUTPUT));
- addInput(createInput(Vec(7, 195), module, ABC::A2_INPUT));
- addInput(createInput(Vec(7, 237), module, ABC::B2_INPUT));
- addInput(createInput(Vec(7, 279), module, ABC::C2_INPUT));
- addOutput(createOutput(Vec(7, 321), module, ABC::OUT2_OUTPUT));
-
- addChild(createLight>(Vec(37, 162), module, ABC::OUT1_LIGHT));
- addChild(createLight>(Vec(37, 329), module, ABC::OUT2_LIGHT));
+ addInput(createInput(Vec(7, 28), module, ABC::A1_INPUT));
+ addInput(createInput(Vec(7, 70), module, ABC::B1_INPUT));
+ addInput(createInput(Vec(7, 112), module, ABC::C1_INPUT));
+ addOutput(createOutput(Vec(7, 154), module, ABC::OUT1_OUTPUT));
+ addInput(createInput(Vec(7, 195), module, ABC::A2_INPUT));
+ addInput(createInput(Vec(7, 237), module, ABC::B2_INPUT));
+ addInput(createInput(Vec(7, 279), module, ABC::C2_INPUT));
+ addOutput(createOutput(Vec(7, 321), module, ABC::OUT2_OUTPUT));
+
+ addChild(createLight>(Vec(37, 162), module, ABC::OUT1_LIGHT));
+ addChild(createLight>(Vec(37, 329), module, ABC::OUT2_LIGHT));
}
};
-Model *modelABC = createModel("ABC");
+Model* modelABC = createModel("ABC");
diff --git a/src/ChoppingKinky.cpp b/src/ChoppingKinky.cpp
new file mode 100644
index 0000000..65f9a93
--- /dev/null
+++ b/src/ChoppingKinky.cpp
@@ -0,0 +1,362 @@
+#include "plugin.hpp"
+#include "ChowDSP.hpp"
+
+
+struct ChoppingKinky : Module {
+ enum ParamIds {
+ FOLD_A_PARAM,
+ FOLD_B_PARAM,
+ CV_A_PARAM,
+ CV_B_PARAM,
+ NUM_PARAMS
+ };
+ enum InputIds {
+ IN_A_INPUT,
+ IN_B_INPUT,
+ IN_GATE_INPUT,
+ CV_A_INPUT,
+ VCA_CV_A_INPUT,
+ CV_B_INPUT,
+ VCA_CV_B_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ OUT_CHOPP_OUTPUT,
+ OUT_A_OUTPUT,
+ OUT_B_OUTPUT,
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ LED_A_LIGHT,
+ LED_B_LIGHT,
+ NUM_LIGHTS
+ };
+ enum {
+ CHANNEL_A,
+ CHANNEL_B,
+ CHANNEL_CHOPP,
+ NUM_CHANNELS
+ };
+
+ static const int WAVESHAPE_CACHE_SIZE = 256;
+ float waveshapeA[WAVESHAPE_CACHE_SIZE + 1] = {};
+ float waveshapeBPositive[WAVESHAPE_CACHE_SIZE + 1] = {};
+ float waveshapeBNegative[WAVESHAPE_CACHE_SIZE + 1] = {};
+
+ dsp::SchmittTrigger trigger;
+ bool outputAToChopp = false;
+ float previousA = 0.0;
+
+ chowdsp::VariableOversampling<> oversampler[NUM_CHANNELS];
+ int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling
+
+ dsp::BiquadFilter blockDCFilter;
+ bool blockDC = false;
+
+ ChoppingKinky() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ configParam(FOLD_A_PARAM, 0.f, 2.f, 0.f, "Gain/shape control for channel A");
+ configParam(FOLD_B_PARAM, 0.f, 2.f, 0.f, "Gain/shape control for channel B");
+ configParam(CV_A_PARAM, -1.f, 1.f, 0.f, "Channel A CV control attenuverter");
+ configParam(CV_B_PARAM, -1.f, 1.f, 0.f, "Channel A CV control attenuverter");
+
+ cacheWaveshaperResponses();
+
+ // calculate up/downsampling rates
+ onSampleRateChange();
+ }
+
+ void onSampleRateChange() override {
+ float sampleRate = APP->engine->getSampleRate();
+
+ blockDCFilter.setParameters(dsp::BiquadFilter::HIGHPASS, 10.3f / sampleRate, M_SQRT1_2, 1.0f);
+
+ for (int channel_idx = 0; channel_idx < NUM_CHANNELS; channel_idx++) {
+ oversampler[channel_idx].setOversamplingIndex(oversamplingIndex);
+ oversampler[channel_idx].reset(sampleRate);
+ }
+ }
+
+ void process(const ProcessArgs& args) override {
+
+ float gainA = params[FOLD_A_PARAM].getValue();
+ gainA += params[CV_A_PARAM].getValue() * inputs[CV_A_INPUT].getVoltage() / 10.f;
+ gainA += inputs[VCA_CV_A_INPUT].getVoltage() / 10.f;
+ gainA = std::max(gainA, 0.f);
+
+ // CV_B_INPUT is normalled to CV_A_INPUT (input with attenuverter)
+ float gainB = params[FOLD_B_PARAM].getValue();
+ gainB += params[CV_B_PARAM].getValue() * inputs[CV_B_INPUT].getNormalVoltage(inputs[CV_A_INPUT].getVoltage()) / 10.f;
+ gainB += inputs[VCA_CV_B_INPUT].getVoltage() / 10.f;
+ gainB = std::max(gainB, 0.f);
+
+ const float inA = inputs[IN_A_INPUT].getVoltage();
+ const float inB = inputs[IN_B_INPUT].getNormalVoltage(inputs[IN_A_INPUT].getVoltage());
+
+ // if the CHOPP gate is wired in, do chop logic
+ if (inputs[IN_GATE_INPUT].isConnected()) {
+ // TODO: check rescale?
+ trigger.process(rescale(inputs[IN_GATE_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f));
+ outputAToChopp = trigger.isHigh();
+ }
+ // else zero-crossing detector on input A switches between A and B
+ else {
+ if (previousA > 0 && inA < 0) {
+ outputAToChopp = false;
+ }
+ else if (previousA < 0 && inA > 0) {
+ outputAToChopp = true;
+ }
+ }
+ previousA = inA;
+
+ const bool choppIsRequired = outputs[OUT_CHOPP_OUTPUT].isConnected();
+ const bool aIsRequired = outputs[OUT_A_OUTPUT].isConnected() || choppIsRequired;
+ const bool bIsRequired = outputs[OUT_B_OUTPUT].isConnected() || choppIsRequired;
+
+ if (aIsRequired) {
+ oversampler[CHANNEL_A].upsample(inA * gainA);
+ }
+ if (bIsRequired) {
+ oversampler[CHANNEL_B].upsample(inB * gainB);
+ }
+ if (choppIsRequired) {
+ oversampler[CHANNEL_CHOPP].upsample(outputAToChopp ? 1.f : 0.f);
+ }
+
+ float* osBufferA = oversampler[CHANNEL_A].getOSBuffer();
+ float* osBufferB = oversampler[CHANNEL_B].getOSBuffer();
+ float* osBufferChopp = oversampler[CHANNEL_CHOPP].getOSBuffer();
+
+ for (int i = 0; i < oversampler[0].getOversamplingRatio(); i++) {
+ if (aIsRequired) {
+ //osBufferA[i] = wavefolderAResponse(osBufferA[i]);
+ osBufferA[i] = wavefolderAResponseCached(osBufferA[i]);
+ }
+ if (bIsRequired) {
+ //osBufferB[i] = wavefolderBResponse(osBufferB[i]);
+ osBufferB[i] = wavefolderBResponseCached(osBufferB[i]);
+ }
+ if (choppIsRequired) {
+ osBufferChopp[i] = osBufferChopp[i] * osBufferA[i] + (1.f - osBufferChopp[i]) * osBufferB[i];
+ }
+ }
+
+ float outA = aIsRequired ? oversampler[CHANNEL_A].downsample() : 0.f;
+ float outB = bIsRequired ? oversampler[CHANNEL_B].downsample() : 0.f;
+ float outChopp = choppIsRequired ? oversampler[CHANNEL_CHOPP].downsample() : 0.f;
+
+ if (blockDC) {
+ outChopp = blockDCFilter.process(outChopp);
+ }
+
+ outputs[OUT_A_OUTPUT].setVoltage(outA);
+ outputs[OUT_B_OUTPUT].setVoltage(outB);
+ outputs[OUT_CHOPP_OUTPUT].setVoltage(outChopp);
+
+ if (inputs[IN_GATE_INPUT].isConnected()) {
+ lights[LED_A_LIGHT].setSmoothBrightness((float) outputAToChopp, args.sampleTime);
+ lights[LED_B_LIGHT].setSmoothBrightness((float)(!outputAToChopp), args.sampleTime);
+ }
+ else {
+ lights[LED_A_LIGHT].setBrightness(0.f);
+ lights[LED_B_LIGHT].setBrightness(0.f);
+ }
+ }
+
+ float wavefolderAResponseCached(float x) {
+ if (x >= 0) {
+ float j = rescale(clamp(x, 0.f, 10.f), 0.f, 10.f, 0, WAVESHAPE_CACHE_SIZE - 1);
+ return interpolateLinear(waveshapeA, j);
+ }
+ else {
+ return -wavefolderAResponseCached(-x);
+ }
+ }
+
+ float wavefolderBResponseCached(float x) {
+ if (x >= 0) {
+ float j = rescale(clamp(x, 0.f, 10.f), 0.f, 10.f, 0, WAVESHAPE_CACHE_SIZE - 1);
+ return interpolateLinear(waveshapeBPositive, j);
+ }
+ else {
+ float j = rescale(clamp(-x, 0.f, 10.f), 0.f, 10.f, 0, WAVESHAPE_CACHE_SIZE - 1);
+ return interpolateLinear(waveshapeBNegative, j);
+ }
+ }
+
+ static float wavefolderAResponse(float x) {
+ if (x < 0) {
+ return -wavefolderAResponse(-x);
+ }
+
+ float xScaleFactor = 1.f / 20.f;
+ float yScaleFactor = 12.5f;
+ x = x * xScaleFactor;
+
+ float piecewiseX1 = 0.087;
+ float piecewiseX2 = 0.245;
+ float piecewiseX3 = 0.3252;
+
+ if (x < piecewiseX1) {
+ float x_ = x / piecewiseX1;
+ return -0.38 * yScaleFactor * (std::sin(M_PI * std::pow(x_, 0.8)) + 1.0 / (3 * 1.6) * std::sin(3 * M_PI * std::pow(x_, 0.8)));
+ }
+ else if (x < piecewiseX2) {
+ float x_ = x - piecewiseX1;
+ return -yScaleFactor * (-0.2 * std::sin(0.5 * M_PI * 12.69 * x_) - 0.24 * std::sin(1.5 * M_PI * 12.69 * x_));
+ }
+ else if (x < piecewiseX3) {
+ float x_ = 9.8 * (x - piecewiseX2);
+ return -0.33 * yScaleFactor * std::sin(x_ / 0.165) * (1 + 0.9 * std::pow(x_, 3) / (1.0 + 2.0 * std::pow(x_, 6)));
+ }
+ else {
+ float x_ = (x - piecewiseX3) / 0.05;
+ return yScaleFactor * ((0.4274 - 0.031) * std::exp(-std::pow(x_, 2.0)) + 0.031);
+ }
+ }
+
+ static float wavefolderBResponse(float x) {
+ float xScaleFactor = 1.f / 20.f;
+ float yScaleFactor = 12.5f;
+ x = x * xScaleFactor;
+
+ // assymetric response
+ if (x > 0) {
+ float piecewiseX1 = 0.117;
+ float piecewiseX2 = 0.2837;
+
+ if (x < piecewiseX1) {
+ float x_ = x / piecewiseX1;
+ return -0.3 * yScaleFactor * (std::sin(M_PI * std::pow(x_, 0.67)) + 1.0 / (3 * 0.8) * std::sin(3 * M_PI * std::pow(x_, 0.67)));
+ }
+ else if (x < piecewiseX2) {
+ float x_ = x - piecewiseX1;
+ return 0.35 * yScaleFactor * std::sin(12. * M_PI * x_);
+ }
+ else {
+ float x_ = (x - piecewiseX2);
+ return 0.57 * yScaleFactor * std::tanh(x_ / 0.03);
+ }
+ }
+ else {
+ float piecewiseX1 = -0.105;
+ float piecewiseX2 = -0.20722;
+
+ if (x > piecewiseX1) {
+ float x_ = x / piecewiseX1;
+ return 0.37 * yScaleFactor * (std::sin(M_PI * std::pow(x_, 0.65)) + 1.0 / (3 * 1.2) * std::sin(3 * M_PI * std::pow(x_, 0.65)));
+ }
+ else if (x > piecewiseX2) {
+ float x_ = x - piecewiseX1;
+ return 0.2 * yScaleFactor * std::sin(15 * M_PI * x_) * (1.0 - 10.f * x_);
+ }
+ else {
+ float x_ = (x - piecewiseX2) / 0.07;
+ return yScaleFactor * ((0.4022 - 0.065) * std::exp(-std::pow(x_, 2)) + 0.065);
+ }
+ }
+ }
+
+ // functional form for waveshapers uses a lot of transcendental functions, so we cache
+ // the response in a LUT
+ void cacheWaveshaperResponses() {
+ for (int i = 0; i < WAVESHAPE_CACHE_SIZE; ++i) {
+ float x = rescale(i, 0, WAVESHAPE_CACHE_SIZE - 1, 0.0, 10.f);
+ waveshapeA[i] = wavefolderAResponse(x);
+ waveshapeBPositive[i] = wavefolderBResponse(+x);
+ waveshapeBNegative[i] = wavefolderBResponse(-x);
+ }
+ }
+
+ json_t* dataToJson() override {
+ json_t* rootJ = json_object();
+ json_object_set_new(rootJ, "filterDC", json_boolean(blockDC));
+ json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
+ return rootJ;
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+ json_t* filterDCJ = json_object_get(rootJ, "filterDC");
+ if (filterDCJ) {
+ blockDC = json_boolean_value(filterDCJ);
+ }
+
+ json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
+ if (oversamplingIndexJ) {
+ oversamplingIndex = json_integer_value(oversamplingIndexJ);
+ onSampleRateChange();
+ }
+ }
+};
+
+
+struct ChoppingKinkyWidget : ModuleWidget {
+ ChoppingKinkyWidget(ChoppingKinky* module) {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ChoppingKinky.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(26.051, 21.999)), module, ChoppingKinky::FOLD_A_PARAM));
+ addParam(createParamCentered(mm2px(Vec(26.051, 62.768)), module, ChoppingKinky::FOLD_B_PARAM));
+ addParam(createParamCentered(mm2px(Vec(10.266, 83.297)), module, ChoppingKinky::CV_A_PARAM));
+ addParam(createParamCentered(mm2px(Vec(30.277, 83.297)), module, ChoppingKinky::CV_B_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(6.127, 27.843)), module, ChoppingKinky::IN_A_INPUT));
+ addInput(createInputCentered(mm2px(Vec(26.057, 42.228)), module, ChoppingKinky::IN_GATE_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.104, 56.382)), module, ChoppingKinky::IN_B_INPUT));
+ addInput(createInputCentered(mm2px(Vec(5.209, 98.499)), module, ChoppingKinky::CV_A_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.259, 98.499)), module, ChoppingKinky::VCA_CV_A_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.308, 98.499)), module, ChoppingKinky::CV_B_INPUT));
+ addInput(createInputCentered(mm2px(Vec(35.358, 98.499)), module, ChoppingKinky::VCA_CV_B_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(20.23, 109.669)), module, ChoppingKinky::OUT_CHOPP_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(31.091, 110.747)), module, ChoppingKinky::OUT_B_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(9.589, 110.777)), module, ChoppingKinky::OUT_A_OUTPUT));
+
+ addChild(createLightCentered>(mm2px(Vec(26.057, 33.307)), module, ChoppingKinky::LED_A_LIGHT));
+ addChild(createLightCentered>(mm2px(Vec(26.057, 51.53)), module, ChoppingKinky::LED_B_LIGHT));
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ ChoppingKinky* module = dynamic_cast(this->module);
+ assert(module);
+
+ menu->addChild(new MenuSeparator());
+
+ struct DCMenuItem : MenuItem {
+ ChoppingKinky* module;
+ void onAction(const event::Action& e) override {
+ module->blockDC ^= true;
+ }
+ };
+ DCMenuItem* dcItem = createMenuItem("Block DC on Chopp", CHECKMARK(module->blockDC));
+ dcItem->module = module;
+ menu->addChild(dcItem);
+
+ 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);
+ }
+ }
+};
+
+
+Model* modelChoppingKinky = createModel("ChoppingKinky");
\ No newline at end of file
diff --git a/src/ChowDSP.hpp b/src/ChowDSP.hpp
new file mode 100644
index 0000000..c4f6db5
--- /dev/null
+++ b/src/ChowDSP.hpp
@@ -0,0 +1,422 @@
+#pragma once
+#include
+
+
+namespace chowdsp {
+ // code taken from https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/, commit 21701fb
+ // * AAFilter.hpp
+ // * VariableOversampling.hpp
+ // * oversampling.hpp
+ // * iir.hpp
+
+template
+struct IIRFilter {
+ /** transfer function numerator coefficients: b_0, b_1, etc.*/
+ T b[ORDER] = {};
+
+ /** transfer function denominator coefficients: a_0, a_1, etc.*/
+ T a[ORDER] = {};
+
+ /** filter state */
+ T z[ORDER];
+
+ IIRFilter() {
+ reset();
+ }
+
+ void reset() {
+ std::fill(z, &z[ORDER], 0.0f);
+ }
+
+ void setCoefficients(const T* b, const T* a) {
+ for (int i = 0; i < ORDER; i++) {
+ this->b[i] = b[i];
+ }
+ for (int i = 1; i < ORDER; i++) {
+ this->a[i] = a[i];
+ }
+ }
+
+ template
+ inline typename std::enable_if ::type process(T x) noexcept {
+ T y = z[1] + x * b[0];
+ z[1] = x * b[1] - y * a[1];
+ return y;
+ }
+
+ template
+ inline typename std::enable_if ::type process(T x) noexcept {
+ T y = z[1] + x * b[0];
+ z[1] = z[2] + x * b[1] - y * a[1];
+ z[2] = x * b[2] - y * a[2];
+ return y;
+ }
+
+ template
+ inline typename std::enable_if < (N > 3), T >::type process(T x) noexcept {
+ T y = z[1] + x * b[0];
+
+ for (int i = 1; i < ORDER - 1; ++i)
+ z[i] = z[i + 1] + x * b[i] - y * a[i];
+
+ z[ORDER - 1] = x * b[ORDER - 1] - y * a[ORDER - 1];
+
+ return y;
+ }
+
+ /** Computes the complex transfer function $H(s)$ at a particular frequency
+ s: normalized angular frequency equal to $2 \pi f / f_{sr}$ ($\pi$ is the Nyquist frequency)
+ */
+ std::complex getTransferFunction(T s) {
+ // Compute sum(a_k z^-k) / sum(b_k z^-k) where z = e^(i s)
+ std::complex bSum(b[0], 0);
+ std::complex aSum(1, 0);
+ for (int i = 1; i < ORDER; i++) {
+ T p = -i * s;
+ std::complex z(simd::cos(p), simd::sin(p));
+ bSum += b[i] * z;
+ aSum += a[i - 1] * z;
+ }
+ return bSum / aSum;
+ }
+
+ T getFrequencyResponse(T f) {
+ return simd::abs(getTransferFunction(2 * M_PI * f));
+ }
+
+ T getFrequencyPhase(T f) {
+ return simd::arg(getTransferFunction(2 * M_PI * f));
+ }
+};
+
+template
+struct TBiquadFilter : IIRFilter<3, T> {
+ enum Type {
+ LOWPASS,
+ HIGHPASS,
+ LOWSHELF,
+ HIGHSHELF,
+ BANDPASS,
+ PEAK,
+ NOTCH,
+ NUM_TYPES
+ };
+
+ TBiquadFilter() {
+ setParameters(LOWPASS, 0.f, 0.f, 1.f);
+ }
+
+ /** Calculates and sets the biquad transfer function coefficients.
+ f: normalized frequency (cutoff frequency / sample rate), must be less than 0.5
+ Q: quality factor
+ V: gain
+ */
+ void setParameters(Type type, float f, float Q, float V) {
+ float K = std::tan(M_PI * f);
+ switch (type) {
+ case LOWPASS: {
+ float norm = 1.f / (1.f + K / Q + K * K);
+ this->b[0] = K * K * norm;
+ this->b[1] = 2.f * this->b[0];
+ this->b[2] = this->b[0];
+ this->a[1] = 2.f * (K * K - 1.f) * norm;
+ this->a[2] = (1.f - K / Q + K * K) * norm;
+ } break;
+
+ case HIGHPASS: {
+ float norm = 1.f / (1.f + K / Q + K * K);
+ this->b[0] = norm;
+ this->b[1] = -2.f * this->b[0];
+ this->b[2] = this->b[0];
+ this->a[1] = 2.f * (K * K - 1.f) * norm;
+ this->a[2] = (1.f - K / Q + K * K) * norm;
+
+ } break;
+
+ case LOWSHELF: {
+ float sqrtV = std::sqrt(V);
+ if (V >= 1.f) {
+ float norm = 1.f / (1.f + M_SQRT2 * K + K * K);
+ this->b[0] = (1.f + M_SQRT2 * sqrtV * K + V * K * K) * norm;
+ this->b[1] = 2.f * (V * K * K - 1.f) * norm;
+ this->b[2] = (1.f - M_SQRT2 * sqrtV * K + V * K * K) * norm;
+ this->a[1] = 2.f * (K * K - 1.f) * norm;
+ this->a[2] = (1.f - M_SQRT2 * K + K * K) * norm;
+ }
+ else {
+ float norm = 1.f / (1.f + M_SQRT2 / sqrtV * K + K * K / V);
+ this->b[0] = (1.f + M_SQRT2 * K + K * K) * norm;
+ this->b[1] = 2.f * (K * K - 1) * norm;
+ this->b[2] = (1.f - M_SQRT2 * K + K * K) * norm;
+ this->a[1] = 2.f * (K * K / V - 1.f) * norm;
+ this->a[2] = (1.f - M_SQRT2 / sqrtV * K + K * K / V) * norm;
+ }
+ } break;
+
+ case HIGHSHELF: {
+ float sqrtV = std::sqrt(V);
+ if (V >= 1.f) {
+ float norm = 1.f / (1.f + M_SQRT2 * K + K * K);
+ this->b[0] = (V + M_SQRT2 * sqrtV * K + K * K) * norm;
+ this->b[1] = 2.f * (K * K - V) * norm;
+ this->b[2] = (V - M_SQRT2 * sqrtV * K + K * K) * norm;
+ this->a[1] = 2.f * (K * K - 1.f) * norm;
+ this->a[2] = (1.f - M_SQRT2 * K + K * K) * norm;
+ }
+ else {
+ float norm = 1.f / (1.f / V + M_SQRT2 / sqrtV * K + K * K);
+ this->b[0] = (1.f + M_SQRT2 * K + K * K) * norm;
+ this->b[1] = 2.f * (K * K - 1.f) * norm;
+ this->b[2] = (1.f - M_SQRT2 * K + K * K) * norm;
+ this->a[1] = 2.f * (K * K - 1.f / V) * norm;
+ this->a[2] = (1.f / V - M_SQRT2 / sqrtV * K + K * K) * norm;
+ }
+ } break;
+
+ case BANDPASS: {
+ float norm = 1.f / (1.f + K / Q + K * K);
+ this->b[0] = K / Q * norm;
+ this->b[1] = 0.f;
+ this->b[2] = -this->b[0];
+ this->a[1] = 2.f * (K * K - 1.f) * norm;
+ this->a[2] = (1.f - K / Q + K * K) * norm;
+ } break;
+
+ case PEAK: {
+ float c = 1.0f / K;
+ float phi = c * c;
+ float Knum = c / Q;
+ float Kdenom = Knum;
+
+ if (V > 1.0f)
+ Knum *= V;
+ else
+ Kdenom /= V;
+
+ float norm = phi + Kdenom + 1.0;
+ this->b[0] = (phi + Knum + 1.0f) / norm;
+ this->b[1] = 2.0f * (1.0f - phi) / norm;
+ this->b[2] = (phi - Knum + 1.0f) / norm;
+ this->a[1] = 2.0f * (1.0f - phi) / norm;
+ this->a[2] = (phi - Kdenom + 1.0f) / norm;
+ } break;
+
+ case NOTCH: {
+ float norm = 1.f / (1.f + K / Q + K * K);
+ this->b[0] = (1.f + K * K) * norm;
+ this->b[1] = 2.f * (K * K - 1.f) * norm;
+ this->b[2] = this->b[0];
+ this->a[1] = this->b[1];
+ this->a[2] = (1.f - K / Q + K * K) * norm;
+ } break;
+
+ default: break;
+ }
+ }
+};
+
+typedef TBiquadFilter<> BiquadFilter;
+
+
+/**
+ High-order filter to be used for anti-aliasing or anti-imaging.
+ The template parameter N should be 1/2 the desired filter order.
+
+ Currently uses an 2*N-th order Butterworth filter.
+ source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp
+*/
+template
+class AAFilter {
+public:
+ AAFilter() = default;
+
+ /** Calculate Q values for a Butterworth filter of a given order */
+ static std::vector calculateButterQs(int order) {
+ const int lim = int (order / 2);
+ std::vector Qs;
+
+ for (int k = 1; k <= lim; ++k) {
+ auto b = -2.0f * std::cos((2.0f * k + order - 1) * 3.14159 / (2.0f * order));
+ Qs.push_back(1.0f / b);
+ }
+
+ std::reverse(Qs.begin(), Qs.end());
+ return Qs;
+ }
+
+ /**
+ * Resets the filter to process at a new sample rate.
+ *
+ * @param sampleRate: The base (i.e. pre-oversampling) sample rate of the audio being processed
+ * @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);
+ auto Qs = calculateButterQs(2 * N);
+
+ for (int i = 0; i < N; ++i)
+ filters[i].setParameters(BiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f);
+ }
+
+ inline float process(float x) noexcept {
+ for (int i = 0; i < N; ++i)
+ x = filters[i].process(x);
+
+ return x;
+ }
+
+private:
+ BiquadFilter filters[N];
+};
+
+
+/**
+ * Base class for oversampling of any order
+ * source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp
+ */
+class BaseOversampling {
+public:
+ BaseOversampling() = default;
+ virtual ~BaseOversampling() {}
+
+ /** Resets the oversampler for processing at some base sample rate */
+ virtual void reset(float /*baseSampleRate*/) = 0;
+
+ /** Upsample a single input sample and update the oversampled buffer */
+ virtual void upsample(float) noexcept = 0;
+
+ /** Output a downsampled output sample from the current oversampled buffer */
+ virtual float downsample() noexcept = 0;
+
+ /** Returns a pointer to the oversampled buffer */
+ virtual float* getOSBuffer() noexcept = 0;
+};
+
+
+/**
+ Class to implement an oversampled process.
+ To use, create an object and prepare using `reset()`.
+
+ Then use the following code to process samples:
+ @code
+ oversample.upsample(x);
+ for(int k = 0; k < ratio; k++)
+ oversample.osBuffer[k] = processSample(oversample.osBuffer[k]);
+ float y = oversample.downsample();
+ @endcode
+*/
+template
+class Oversampling : public BaseOversampling {
+public:
+ Oversampling() = default;
+ virtual ~Oversampling() {}
+
+ void reset(float baseSampleRate) override {
+ aaFilter.reset(baseSampleRate, ratio);
+ aiFilter.reset(baseSampleRate, ratio);
+ std::fill(osBuffer, &osBuffer[ratio], 0.0f);
+ }
+
+ inline void upsample(float x) noexcept override {
+ osBuffer[0] = ratio * x;
+ std::fill(&osBuffer[1], &osBuffer[ratio], 0.0f);
+
+ for (int k = 0; k < ratio; k++)
+ osBuffer[k] = aiFilter.process(osBuffer[k]);
+ }
+
+ inline float downsample() noexcept override {
+ float y = 0.0f;
+ for (int k = 0; k < ratio; k++)
+ y = aaFilter.process(osBuffer[k]);
+
+ return y;
+ }
+
+ inline float* getOSBuffer() noexcept override {
+ return osBuffer;
+ }
+
+ float osBuffer[ratio];
+
+private:
+ AAFilter aaFilter; // anti-aliasing filter
+ AAFilter aiFilter; // anti-imaging filter
+};
+
+
+
+/**
+ Class to implement an oversampled process, with variable
+ oversampling factor. To use, create an object, set the oversampling
+ factor using `setOversamplingindex()` and prepare using `reset()`.
+
+ Then use the following code to process samples:
+ @code
+ oversample.upsample(x);
+ float* osBuffer = oversample.getOSBuffer();
+ for(int k = 0; k < ratio; k++)
+ osBuffer[k] = processSample(osBuffer[k]);
+ float y = oversample.downsample();
+ @endcode
+
+ source (modified): https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/VariableOversampling.hpp
+*/
+template
+class VariableOversampling {
+public:
+ VariableOversampling() = default;
+
+ /** Prepare the oversampler to process audio at a given sample rate */
+ void reset(float sampleRate) {
+ for (auto* os : oss)
+ os->reset(sampleRate);
+ }
+
+ /** Sets the oversampling factor as 2^idx */
+ void setOversamplingIndex(int newIdx) {
+ osIdx = newIdx;
+ }
+
+ /** Returns the oversampling index */
+ int getOversamplingIndex() const noexcept {
+ return osIdx;
+ }
+
+ /** Upsample a single input sample and update the oversampled buffer */
+ inline void upsample(float x) noexcept {
+ oss[osIdx]->upsample(x);
+ }
+
+ /** Output a downsampled output sample from the current oversampled buffer */
+ inline float downsample() noexcept {
+ return oss[osIdx]->downsample();
+ }
+
+ /** Returns a pointer to the oversampled buffer */
+ inline float* getOSBuffer() noexcept {
+ return oss[osIdx]->getOSBuffer();
+ }
+
+ /** Returns the current oversampling factor */
+ int getOversamplingRatio() const noexcept {
+ return 1 << osIdx;
+ }
+
+
+private:
+ enum {
+ NumOS = 5, // number of oversampling options
+ };
+
+ int osIdx = 0;
+
+ Oversampling < 1 << 0, filtN > os0; // 1x
+ Oversampling < 1 << 1, filtN > os1; // 2x
+ Oversampling < 1 << 2, filtN > os2; // 4x
+ Oversampling < 1 << 3, filtN > os3; // 8x
+ Oversampling < 1 << 4, filtN > os4; // 16x
+ BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };
+};
+
+} // namespace chowdsp
diff --git a/src/DualAtenuverter.cpp b/src/DualAtenuverter.cpp
index 37df22d..dd262db 100644
--- a/src/DualAtenuverter.cpp
+++ b/src/DualAtenuverter.cpp
@@ -1,6 +1,5 @@
#include "plugin.hpp"
-
struct DualAtenuverter : Module {
enum ParamIds {
ATEN1_PARAM,
@@ -20,10 +19,8 @@ struct DualAtenuverter : Module {
NUM_OUTPUTS
};
enum LightIds {
- OUT1_POS_LIGHT,
- OUT1_NEG_LIGHT,
- OUT2_POS_LIGHT,
- OUT2_NEG_LIGHT,
+ ENUMS(OUT1_LIGHT, 3),
+ ENUMS(OUT2_LIGHT, 3),
NUM_LIGHTS
};
@@ -35,24 +32,70 @@ struct DualAtenuverter : Module {
configParam(OFFSET2_PARAM, -10.0, 10.0, 0.0, "Ch 2 offset", " V");
}
- void process(const ProcessArgs &args) override {
- float out1 = inputs[IN1_INPUT].getVoltage() * params[ATEN1_PARAM].getValue() + params[OFFSET1_PARAM].getValue();
- float out2 = inputs[IN2_INPUT].getVoltage() * params[ATEN2_PARAM].getValue() + params[OFFSET2_PARAM].getValue();
- out1 = clamp(out1, -10.f, 10.f);
- out2 = clamp(out2, -10.f, 10.f);
-
- outputs[OUT1_OUTPUT].setVoltage(out1);
- outputs[OUT2_OUTPUT].setVoltage(out2);
- lights[OUT1_POS_LIGHT].setSmoothBrightness(out1 / 5.f, args.sampleTime);
- lights[OUT1_NEG_LIGHT].setSmoothBrightness(-out1 / 5.f, args.sampleTime);
- lights[OUT2_POS_LIGHT].setSmoothBrightness(out2 / 5.f, args.sampleTime);
- lights[OUT2_NEG_LIGHT].setSmoothBrightness(-out2 / 5.f, args.sampleTime);
+ void process(const ProcessArgs& args) override {
+ using simd::float_4;
+
+ float_4 out1[4] = {};
+ float_4 out2[4] = {};
+
+ int channels1 = inputs[IN1_INPUT].getChannels();
+ channels1 = channels1 > 0 ? channels1 : 1;
+ int channels2 = inputs[IN2_INPUT].getChannels();
+ channels2 = channels2 > 0 ? channels2 : 1;
+
+ float att1 = params[ATEN1_PARAM].getValue();
+ float att2 = params[ATEN2_PARAM].getValue();
+
+ float offset1 = params[OFFSET1_PARAM].getValue();
+ float offset2 = params[OFFSET2_PARAM].getValue();
+
+ for (int c = 0; c < channels1; c += 4) {
+ out1[c / 4] = clamp(inputs[IN1_INPUT].getVoltageSimd(c) * att1 + offset1, -10.f, 10.f);
+ }
+ for (int c = 0; c < channels2; c += 4) {
+ out2[c / 4] = clamp(inputs[IN2_INPUT].getVoltageSimd(c) * att2 + offset2, -10.f, 10.f);
+ }
+
+ outputs[OUT1_OUTPUT].setChannels(channels1);
+ outputs[OUT2_OUTPUT].setChannels(channels2);
+
+ for (int c = 0; c < channels1; c += 4) {
+ outputs[OUT1_OUTPUT].setVoltageSimd(out1[c / 4], c);
+ }
+ for (int c = 0; c < channels2; c += 4) {
+ outputs[OUT2_OUTPUT].setVoltageSimd(out2[c / 4], c);
+ }
+
+ float light1 = outputs[OUT1_OUTPUT].getVoltageSum() / channels1;
+ float light2 = outputs[OUT2_OUTPUT].getVoltageSum() / channels2;
+
+ if (channels1 == 1) {
+ lights[OUT1_LIGHT + 0].setSmoothBrightness(light1 / 5.f, args.sampleTime);
+ lights[OUT1_LIGHT + 1].setSmoothBrightness(-light1 / 5.f, args.sampleTime);
+ lights[OUT1_LIGHT + 2].setBrightness(0.0f);
+ }
+ else {
+ lights[OUT1_LIGHT + 0].setBrightness(0.0f);
+ lights[OUT1_LIGHT + 1].setBrightness(0.0f);
+ lights[OUT1_LIGHT + 2].setBrightness(10.0f);
+ }
+
+ if (channels2 == 1) {
+ lights[OUT2_LIGHT + 0].setSmoothBrightness(light2 / 5.f, args.sampleTime);
+ lights[OUT2_LIGHT + 1].setSmoothBrightness(-light2 / 5.f, args.sampleTime);
+ lights[OUT2_LIGHT + 2].setBrightness(0.0f);
+ }
+ else {
+ lights[OUT2_LIGHT + 0].setBrightness(0.0f);
+ lights[OUT2_LIGHT + 1].setBrightness(0.0f);
+ lights[OUT2_LIGHT + 2].setBrightness(10.0f);
+ }
}
};
struct DualAtenuverterWidget : ModuleWidget {
- DualAtenuverterWidget(DualAtenuverter *module) {
+ DualAtenuverterWidget(DualAtenuverter* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/DualAtenuverter.svg")));
@@ -64,16 +107,16 @@ struct DualAtenuverterWidget : ModuleWidget {
addParam(createParam(Vec(20, 201), module, DualAtenuverter::ATEN2_PARAM));
addParam(createParam(Vec(20, 260), module, DualAtenuverter::OFFSET2_PARAM));
- addInput(createInput(Vec(7, 152), module, DualAtenuverter::IN1_INPUT));
- addOutput(createOutput(Vec(43, 152), module, DualAtenuverter::OUT1_OUTPUT));
+ addInput(createInput(Vec(7, 152), module, DualAtenuverter::IN1_INPUT));
+ addOutput(createOutput(Vec(43, 152), module, DualAtenuverter::OUT1_OUTPUT));
- addInput(createInput(Vec(7, 319), module, DualAtenuverter::IN2_INPUT));
- addOutput(createOutput(Vec(43, 319), module, DualAtenuverter::OUT2_OUTPUT));
+ addInput(createInput(Vec(7, 319), module, DualAtenuverter::IN2_INPUT));
+ addOutput(createOutput(Vec(43, 319), module, DualAtenuverter::OUT2_OUTPUT));
- addChild(createLight>(Vec(33, 143), module, DualAtenuverter::OUT1_POS_LIGHT));
- addChild(createLight>(Vec(33, 311), module, DualAtenuverter::OUT2_POS_LIGHT));
+ addChild(createLight>(Vec(33, 143), module, DualAtenuverter::OUT1_LIGHT));
+ addChild(createLight>(Vec(33, 311), module, DualAtenuverter::OUT2_LIGHT));
}
};
-Model *modelDualAtenuverter = createModel("DualAtenuverter");
+Model* modelDualAtenuverter = createModel("DualAtenuverter");
\ No newline at end of file
diff --git a/src/EvenVCO.cpp b/src/EvenVCO.cpp
index db7b0bc..fa2c4b3 100644
--- a/src/EvenVCO.cpp
+++ b/src/EvenVCO.cpp
@@ -1,5 +1,6 @@
#include "plugin.hpp"
+using simd::float_4;
struct EvenVCO : Module {
enum ParamIds {
@@ -25,22 +26,21 @@ struct EvenVCO : Module {
NUM_OUTPUTS
};
- float phase = 0.0;
+ float_4 phase[4] = {};
+ float_4 tri[4] = {};
+
/** The value of the last sync input */
float sync = 0.0;
/** The outputs */
- float tri = 0.0;
/** Whether we are past the pulse width already */
- bool halfPhase = false;
-
- dsp::MinBlepGenerator<16, 32> triSquareMinBlep;
- dsp::MinBlepGenerator<16, 32> triMinBlep;
- dsp::MinBlepGenerator<16, 32> sineMinBlep;
- dsp::MinBlepGenerator<16, 32> doubleSawMinBlep;
- dsp::MinBlepGenerator<16, 32> sawMinBlep;
- dsp::MinBlepGenerator<16, 32> squareMinBlep;
+ bool halfPhase[PORT_MAX_CHANNELS] = {};
- dsp::RCFilter triFilter;
+ 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];
EvenVCO() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS);
@@ -49,102 +49,184 @@ struct EvenVCO : Module {
configParam(PWM_PARAM, -1.0, 1.0, 0.0, "Pulse width");
}
- void process(const ProcessArgs &args) override {
+ void process(const ProcessArgs& args) override {
+
+ int channels_pitch1 = inputs[PITCH1_INPUT].getChannels();
+ int channels_pitch2 = inputs[PITCH2_INPUT].getChannels();
+
+ int channels = 1;
+ channels = std::max(channels, channels_pitch1);
+ channels = std::max(channels, channels_pitch2);
+
+ float pitch_0 = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
+
// Compute frequency, pitch is 1V/oct
- float pitch = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
- pitch += inputs[PITCH1_INPUT].getVoltage() + inputs[PITCH2_INPUT].getVoltage();
- pitch += inputs[FM_INPUT].getVoltage() / 4.f;
- float freq = dsp::FREQ_C4 * std::pow(2.f, pitch);
- freq = clamp(freq, 0.f, 20000.f);
-
- // Pulse width
- float pw = params[PWM_PARAM].getValue() + inputs[PWM_INPUT].getVoltage() / 5.f;
- const float minPw = 0.05;
- pw = rescale(clamp(pw, -1.f, 1.f), -1.f, 1.f, minPw, 1.f - minPw);
-
- // Advance phase
- float deltaPhase = clamp(freq * args.sampleTime, 1e-6f, 0.5f);
- float oldPhase = phase;
- phase += deltaPhase;
-
- if (oldPhase < 0.5 && phase >= 0.5) {
- float crossing = -(phase - 0.5) / deltaPhase;
- triSquareMinBlep.insertDiscontinuity(crossing, 2.f);
- doubleSawMinBlep.insertDiscontinuity(crossing, -2.f);
+ float_4 pitch[4] = {};
+ for (int c = 0; c < channels; c += 4)
+ pitch[c / 4] = pitch_0;
+
+ if (inputs[PITCH1_INPUT].isConnected()) {
+ for (int c = 0; c < channels; c += 4)
+ pitch[c / 4] += inputs[PITCH1_INPUT].getPolyVoltageSimd(c);
}
- if (!halfPhase && phase >= pw) {
- float crossing = -(phase - pw) / deltaPhase;
- squareMinBlep.insertDiscontinuity(crossing, 2.f);
- halfPhase = true;
+ if (inputs[PITCH2_INPUT].isConnected()) {
+ for (int c = 0; c < channels; c += 4)
+ pitch[c / 4] += inputs[PITCH2_INPUT].getPolyVoltageSimd(c);
}
- // Reset phase if at end of cycle
- if (phase >= 1.f) {
- phase -= 1.f;
- float crossing = -phase / deltaPhase;
- triSquareMinBlep.insertDiscontinuity(crossing, -2.f);
- doubleSawMinBlep.insertDiscontinuity(crossing, -2.f);
- squareMinBlep.insertDiscontinuity(crossing, -2.f);
- sawMinBlep.insertDiscontinuity(crossing, -2.f);
- halfPhase = false;
+ if (inputs[FM_INPUT].isConnected()) {
+ for (int c = 0; c < channels; c += 4)
+ pitch[c / 4] += inputs[FM_INPUT].getPolyVoltageSimd(c) / 4.f;
+ }
+
+ float_4 freq[4] = {};
+ for (int c = 0; c < channels; c += 4) {
+ freq[c / 4] = dsp::FREQ_C4 * simd::pow(2.f, pitch[c / 4]);
+ freq[c / 4] = clamp(freq[c / 4], 0.f, 20000.f);
+ }
+
+ // Pulse width
+ float_4 pw[4] = {};
+ for (int c = 0; c < channels; c += 4)
+ pw[c / 4] = params[PWM_PARAM].getValue();
+
+ if (inputs[PWM_INPUT].isConnected()) {
+ for (int c = 0; c < channels; c += 4)
+ pw[c / 4] += inputs[PWM_INPUT].getPolyVoltageSimd(c) / 5.f;
+ }
+
+ float_4 deltaPhase[4] = {};
+ float_4 oldPhase[4] = {};
+ for (int c = 0; c < channels; c += 4) {
+ pw[c / 4] = rescale(clamp(pw[c / 4], -1.0f, 1.0f), -1.0f, 1.0f, 0.05f, 1.0f - 0.05f);
+
+ // Advance phase
+ deltaPhase[c / 4] = clamp(freq[c / 4] * args.sampleTime, 1e-6f, 0.5f);
+ oldPhase[c / 4] = phase[c / 4];
+ phase[c / 4] += deltaPhase[c / 4];
+ }
+
+ // the next block can't be done with SIMD instructions:
+ for (int c = 0; c < channels; c++) {
+
+ if (oldPhase[c / 4].s[c % 4] < 0.5 && phase[c / 4].s[c % 4] >= 0.5) {
+ float crossing = -(phase[c / 4].s[c % 4] - 0.5) / deltaPhase[c / 4].s[c % 4];
+ triSquareMinBlep[c].insertDiscontinuity(crossing, 2.f);
+ doubleSawMinBlep[c].insertDiscontinuity(crossing, -2.f);
+ }
+
+ if (!halfPhase[c] && phase[c / 4].s[c % 4] >= pw[c / 4].s[c % 4]) {
+ float crossing = -(phase[c / 4].s[c % 4] - pw[c / 4].s[c % 4]) / deltaPhase[c / 4].s[c % 4];
+ squareMinBlep[c].insertDiscontinuity(crossing, 2.f);
+ halfPhase[c] = true;
+ }
+
+ // Reset phase if at end of cycle
+ if (phase[c / 4].s[c % 4] >= 1.f) {
+ phase[c / 4].s[c % 4] -= 1.f;
+ float crossing = -phase[c / 4].s[c % 4] / deltaPhase[c / 4].s[c % 4];
+ triSquareMinBlep[c].insertDiscontinuity(crossing, -2.f);
+ doubleSawMinBlep[c].insertDiscontinuity(crossing, -2.f);
+ squareMinBlep[c].insertDiscontinuity(crossing, -2.f);
+ sawMinBlep[c].insertDiscontinuity(crossing, -2.f);
+ halfPhase[c] = false;
+ }
+ }
+
+ float_4 triSquareMinBlepOut[4] = {};
+ float_4 doubleSawMinBlepOut[4] = {};
+ float_4 sawMinBlepOut[4] = {};
+ float_4 squareMinBlepOut[4] = {};
+
+ float_4 triSquare[4] = {};
+ float_4 sine[4] = {};
+ float_4 doubleSaw[4] = {};
+
+ float_4 even[4] = {};
+ float_4 saw[4] = {};
+ float_4 square[4] = {};
+ float_4 triOut[4] = {};
+
+ for (int c = 0; c < channels; c++) {
+ triSquareMinBlepOut[c / 4].s[c % 4] = triSquareMinBlep[c].process();
+ doubleSawMinBlepOut[c / 4].s[c % 4] = doubleSawMinBlep[c].process();
+ sawMinBlepOut[c / 4].s[c % 4] = sawMinBlep[c].process();
+ squareMinBlepOut[c / 4].s[c % 4] = squareMinBlep[c].process();
}
// Outputs
- float triSquare = (phase < 0.5) ? -1.f : 1.f;
- triSquare += triSquareMinBlep.process();
-
- // Integrate square for triangle
- tri += 4.f * triSquare * freq * args.sampleTime;
- tri *= (1.f - 40.f * args.sampleTime);
-
- float sine = -std::cos(2*M_PI * phase);
- float doubleSaw = (phase < 0.5) ? (-1.f + 4.f*phase) : (-1.f + 4.f*(phase - 0.5));
- doubleSaw += doubleSawMinBlep.process();
- float even = 0.55 * (doubleSaw + 1.27 * sine);
- float saw = -1.f + 2.f*phase;
- saw += sawMinBlep.process();
- float square = (phase < pw) ? -1.f : 1.f;
- square += squareMinBlep.process();
-
- // Set outputs
- outputs[TRI_OUTPUT].setVoltage(5.f*tri);
- outputs[SINE_OUTPUT].setVoltage(5.f*sine);
- outputs[EVEN_OUTPUT].setVoltage(5.f*even);
- outputs[SAW_OUTPUT].setVoltage(5.f*saw);
- outputs[SQUARE_OUTPUT].setVoltage(5.f*square);
+ 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);
+
+ for (int c = 0; c < channels; c += 4) {
+
+ triSquare[c / 4] = simd::ifelse((phase[c / 4] < 0.5f), -1.f, +1.f);
+ triSquare[c / 4] += triSquareMinBlepOut[c / 4];
+
+ // Integrate square for triangle
+
+ tri[c / 4] += (4.f * triSquare[c / 4]) * (freq[c / 4] * args.sampleTime);
+ tri[c / 4] *= (1.f - 40.f * args.sampleTime);
+ triOut[c / 4] = 5.f * tri[c / 4];
+
+ sine[c / 4] = 5.f * simd::cos(2 * M_PI * phase[c / 4]);
+
+ 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] *= 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] *= 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] *= 5.f;
+
+ // Set outputs
+ outputs[TRI_OUTPUT].setVoltageSimd(triOut[c / 4], c);
+ outputs[SINE_OUTPUT].setVoltageSimd(sine[c / 4], c);
+ outputs[EVEN_OUTPUT].setVoltageSimd(even[c / 4], c);
+ outputs[SAW_OUTPUT].setVoltageSimd(saw[c / 4], c);
+ outputs[SQUARE_OUTPUT].setVoltageSimd(square[c / 4], c);
+ }
}
};
struct EvenVCOWidget : ModuleWidget {
- EvenVCOWidget(EvenVCO *module) {
+ EvenVCOWidget(EvenVCO* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/EvenVCO.svg")));
addChild(createWidget(Vec(15, 0)));
addChild(createWidget(Vec(15, 365)));
- addChild(createWidget(Vec(15*6, 0)));
- addChild(createWidget(Vec(15*6, 365)));
+ addChild(createWidget(Vec(15 * 6, 0)));
+ addChild(createWidget(Vec(15 * 6, 365)));
addParam(createParam(Vec(22, 32), module, EvenVCO::OCTAVE_PARAM));
addParam(createParam(Vec(73, 131), module, EvenVCO::TUNE_PARAM));
addParam(createParam(Vec(16, 230), module, EvenVCO::PWM_PARAM));
- addInput(createInput(Vec(8, 120), module, EvenVCO::PITCH1_INPUT));
- addInput(createInput(Vec(19, 157), module, EvenVCO::PITCH2_INPUT));
- addInput(createInput(Vec(48, 183), module, EvenVCO::FM_INPUT));
- addInput(createInput(Vec(86, 189), module, EvenVCO::SYNC_INPUT));
+ addInput(createInput(Vec(8, 120), module, EvenVCO::PITCH1_INPUT));
+ addInput(createInput(Vec(19, 157), module, EvenVCO::PITCH2_INPUT));
+ addInput(createInput(Vec(48, 183), module, EvenVCO::FM_INPUT));
+ addInput(createInput(Vec(86, 189), module, EvenVCO::SYNC_INPUT));
- addInput(createInput(Vec(72, 236), module, EvenVCO::PWM_INPUT));
+ addInput(createInput(Vec(72, 236), module, EvenVCO::PWM_INPUT));
- addOutput(createOutput(Vec(10, 283), module, EvenVCO::TRI_OUTPUT));
- addOutput(createOutput(Vec(87, 283), module, EvenVCO::SINE_OUTPUT));
- addOutput(createOutput(Vec(48, 306), module, EvenVCO::EVEN_OUTPUT));
- addOutput(createOutput(Vec(10, 327), module, EvenVCO::SAW_OUTPUT));
- addOutput(createOutput(Vec(87, 327), module, EvenVCO::SQUARE_OUTPUT));
+ addOutput(createOutput(Vec(10, 283), module, EvenVCO::TRI_OUTPUT));
+ addOutput(createOutput(Vec(87, 283), module, EvenVCO::SINE_OUTPUT));
+ addOutput(createOutput(Vec(48, 306), module, EvenVCO::EVEN_OUTPUT));
+ addOutput(createOutput(Vec(10, 327), module, EvenVCO::SAW_OUTPUT));
+ addOutput(createOutput(Vec(87, 327), module, EvenVCO::SQUARE_OUTPUT));
}
};
-Model *modelEvenVCO = createModel("EvenVCO");
+Model* modelEvenVCO = createModel("EvenVCO");
diff --git a/src/HexmixVCA.cpp b/src/HexmixVCA.cpp
new file mode 100644
index 0000000..43e1fd7
--- /dev/null
+++ b/src/HexmixVCA.cpp
@@ -0,0 +1,212 @@
+#include "plugin.hpp"
+
+using simd::float_4;
+
+static float gainFunction(float x, float shape) {
+ float lin = x;
+ if (shape > 0.f) {
+ float log = 11.f * x / (10.f * x + 1.f);
+ return crossfade(lin, log, shape);
+ }
+ else {
+ float exp = std::pow(x, 4);
+ return crossfade(lin, exp, -shape);
+ }
+}
+
+struct HexmixVCA : Module {
+ enum ParamIds {
+ ENUMS(SHAPE_PARAM, 6),
+ ENUMS(VOL_PARAM, 6),
+ NUM_PARAMS
+ };
+ enum InputIds {
+ ENUMS(IN_INPUT, 6),
+ ENUMS(CV_INPUT, 6),
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ ENUMS(OUT_OUTPUT, 6),
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ NUM_LIGHTS
+ };
+
+ const static int numRows = 6;
+ dsp::ClockDivider cvDivider;
+ float outputLevels[numRows] = {};
+ float shapes[numRows] = {};
+ bool finalRowIsMix = true;
+
+ HexmixVCA() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ for (int i = 0; i < numRows; ++i) {
+ configParam(SHAPE_PARAM + i, -1.f, 1.f, 0.f, string::f("Channel %d VCA response", i));
+ configParam(VOL_PARAM + i, 0.f, 1.f, 1.f, string::f("Channel %d output level", i));
+ }
+ cvDivider.setDivision(16);
+
+ for (int row = 0; row < numRows; ++row) {
+ outputLevels[row] = 1.f;
+ }
+ }
+
+ void process(const ProcessArgs& args) override {
+ float_4 mix[4] = {};
+ int maxChannels = 1;
+
+ // only calculate gains/shapes every 16 samples
+ if (cvDivider.process()) {
+ for (int row = 0; row < numRows; ++row) {
+ shapes[row] = params[SHAPE_PARAM + row].getValue();
+ outputLevels[row] = params[VOL_PARAM + row].getValue();
+ }
+ }
+
+ for (int row = 0; row < numRows; ++row) {
+ bool finalRow = (row == numRows - 1);
+ int channels = 1;
+ float_4 in[4] = {};
+ bool inputIsConnected = inputs[IN_INPUT + row].isConnected();
+ if (inputIsConnected) {
+ channels = inputs[row].getChannels();
+
+ // if we're in "mixer" mode, an input only counts towards the main output polyphony count if it's
+ // not taken out of the mix (i.e. patched in). the final row should count towards polyphony calc.
+ if (finalRowIsMix && (finalRow || !outputs[OUT_OUTPUT + row].isConnected())) {
+ maxChannels = std::max(maxChannels, channels);
+ }
+
+ float cvGain = clamp(inputs[CV_INPUT + row].getNormalVoltage(10.f) / 10.f, 0.f, 1.f);
+ float gain = gainFunction(cvGain, shapes[row]) * outputLevels[row];
+
+ for (int c = 0; c < channels; c += 4) {
+ in[c / 4] = inputs[row].getVoltageSimd(c) * gain;
+ }
+ }
+
+ if (!finalRow) {
+ if (outputs[OUT_OUTPUT + row].isConnected()) {
+ // if output is connected, we don't add to mix
+ outputs[OUT_OUTPUT + row].setChannels(channels);
+ for (int c = 0; c < channels; c += 4) {
+ outputs[OUT_OUTPUT + row].setVoltageSimd(in[c / 4], c);
+ }
+ }
+ else if (finalRowIsMix) {
+ // else add to mix (if setting enabled)
+ for (int c = 0; c < channels; c += 4) {
+ mix[c / 4] += in[c / 4];
+ }
+ }
+ }
+ // final row
+ else {
+ if (outputs[OUT_OUTPUT + row].isConnected()) {
+ if (finalRowIsMix) {
+ outputs[OUT_OUTPUT + row].setChannels(maxChannels);
+
+ // last channel must always go into mix
+ for (int c = 0; c < channels; c += 4) {
+ mix[c / 4] += in[c / 4];
+ }
+
+ for (int c = 0; c < maxChannels; c += 4) {
+ outputs[OUT_OUTPUT + row].setVoltageSimd(mix[c / 4], c);
+ }
+ }
+ else {
+ // same as other rows
+ outputs[OUT_OUTPUT + row].setChannels(channels);
+ for (int c = 0; c < channels; c += 4) {
+ outputs[OUT_OUTPUT + row].setVoltageSimd(in[c / 4], c);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+ json_t* modeJ = json_object_get(rootJ, "finalRowIsMix");
+ if (modeJ) {
+ finalRowIsMix = json_boolean_value(modeJ);
+ }
+ }
+
+ json_t* dataToJson() override {
+ json_t* rootJ = json_object();
+ json_object_set_new(rootJ, "finalRowIsMix", json_boolean(finalRowIsMix));
+ return rootJ;
+ }
+};
+
+
+struct HexmixVCAWidget : ModuleWidget {
+ HexmixVCAWidget(HexmixVCA* module) {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/HexmixVCA.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(20.412, 15.51)), module, HexmixVCA::SHAPE_PARAM + 0));
+ addParam(createParamCentered(mm2px(Vec(20.412, 34.115)), module, HexmixVCA::SHAPE_PARAM + 1));
+ addParam(createParamCentered(mm2px(Vec(20.412, 52.72)), module, HexmixVCA::SHAPE_PARAM + 2));
+ addParam(createParamCentered(mm2px(Vec(20.412, 71.325)), module, HexmixVCA::SHAPE_PARAM + 3));
+ addParam(createParamCentered(mm2px(Vec(20.412, 89.93)), module, HexmixVCA::SHAPE_PARAM + 4));
+ addParam(createParamCentered(mm2px(Vec(20.412, 108.536)), module, HexmixVCA::SHAPE_PARAM + 5));
+
+ addParam(createParamCentered(mm2px(Vec(35.458, 15.51)), module, HexmixVCA::VOL_PARAM + 0));
+ addParam(createParamCentered(mm2px(Vec(35.458, 34.115)), module, HexmixVCA::VOL_PARAM + 1));
+ addParam(createParamCentered(mm2px(Vec(35.458, 52.72)), module, HexmixVCA::VOL_PARAM + 2));
+ addParam(createParamCentered(mm2px(Vec(35.458, 71.325)), module, HexmixVCA::VOL_PARAM + 3));
+ addParam(createParamCentered(mm2px(Vec(35.458, 89.93)), module, HexmixVCA::VOL_PARAM + 4));
+ addParam(createParamCentered(mm2px(Vec(35.458, 108.536)), module, HexmixVCA::VOL_PARAM + 5));
+
+ addInput(createInputCentered(mm2px(Vec(6.581, 15.51)), module, HexmixVCA::IN_INPUT + 0));
+ addInput(createInputCentered(mm2px(Vec(6.581, 34.115)), module, HexmixVCA::IN_INPUT + 1));
+ addInput(createInputCentered(mm2px(Vec(6.581, 52.72)), module, HexmixVCA::IN_INPUT + 2));
+ addInput(createInputCentered(mm2px(Vec(6.581, 71.325)), module, HexmixVCA::IN_INPUT + 3));
+ addInput(createInputCentered(mm2px(Vec(6.581, 89.93)), module, HexmixVCA::IN_INPUT + 4));
+ addInput(createInputCentered(mm2px(Vec(6.581, 108.536)), module, HexmixVCA::IN_INPUT + 5));
+
+ addInput(createInputCentered(mm2px(Vec(52.083, 15.51)), module, HexmixVCA::CV_INPUT + 0));
+ addInput(createInputCentered(mm2px(Vec(52.083, 34.115)), module, HexmixVCA::CV_INPUT + 1));
+ addInput(createInputCentered(mm2px(Vec(52.083, 52.72)), module, HexmixVCA::CV_INPUT + 2));
+ addInput(createInputCentered(mm2px(Vec(52.083, 71.325)), module, HexmixVCA::CV_INPUT + 3));
+ addInput(createInputCentered(mm2px(Vec(52.083, 89.93)), module, HexmixVCA::CV_INPUT + 4));
+ addInput(createInputCentered(mm2px(Vec(52.083, 108.536)), module, HexmixVCA::CV_INPUT + 5));
+
+ addOutput(createOutputCentered(mm2px(Vec(64.222, 15.51)), module, HexmixVCA::OUT_OUTPUT + 0));
+ addOutput(createOutputCentered(mm2px(Vec(64.222, 34.115)), module, HexmixVCA::OUT_OUTPUT + 1));
+ addOutput(createOutputCentered(mm2px(Vec(64.222, 52.72)), module, HexmixVCA::OUT_OUTPUT + 2));
+ addOutput(createOutputCentered(mm2px(Vec(64.222, 71.325)), module, HexmixVCA::OUT_OUTPUT + 3));
+ addOutput(createOutputCentered(mm2px(Vec(64.222, 89.93)), module, HexmixVCA::OUT_OUTPUT + 4));
+ addOutput(createOutputCentered(mm2px(Vec(64.222, 108.536)), module, HexmixVCA::OUT_OUTPUT + 5));
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ HexmixVCA* module = dynamic_cast(this->module);
+ assert(module);
+
+ menu->addChild(new MenuSeparator());
+
+ struct MixMenuItem : MenuItem {
+ HexmixVCA* module;
+ void onAction(const event::Action& e) override {
+ module->finalRowIsMix ^= true;
+ }
+ };
+
+ MixMenuItem* mixItem = createMenuItem("Final row is mix", CHECKMARK(module->finalRowIsMix));
+ mixItem->module = module;
+ menu->addChild(mixItem);
+ }
+};
+
+
+Model* modelHexmixVCA = createModel("HexmixVCA");
\ No newline at end of file
diff --git a/src/Kickall.cpp b/src/Kickall.cpp
new file mode 100644
index 0000000..6be5259
--- /dev/null
+++ b/src/Kickall.cpp
@@ -0,0 +1,146 @@
+#include "plugin.hpp"
+#include "ChowDSP.hpp"
+
+
+struct Kickall : Module {
+ enum ParamIds {
+ TUNE_PARAM,
+ TRIGG_BUTTON_PARAM,
+ SHAPE_PARAM,
+ DECAY_PARAM,
+ TIME_PARAM,
+ BEND_PARAM,
+ NUM_PARAMS
+ };
+ enum InputIds {
+ TRIGG_INPUT,
+ VOLUME_INPUT,
+ TUNE_INPUT,
+ SHAPE_INPUT,
+ DECAY_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ OUT_OUTPUT,
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ ENV_LIGHT,
+ NUM_LIGHTS
+ };
+
+ static constexpr float FREQ_A0 = 27.5f;
+ static constexpr float FREQ_B2 = 123.471f;
+ static constexpr float minVolumeDecay = 0.075f;
+ static constexpr float maxVolumeDecay = 4.f;
+ static constexpr float minPitchDecay = 0.0075f;
+ static constexpr float maxPitchDecay = 1.f;
+ static constexpr float bendRange = 10000;
+ float phase = 0.f;
+ ADEnvelope volume;
+ ADEnvelope pitch;
+
+ dsp::SchmittTrigger trigger;
+
+ static const int UPSAMPLE = 8;
+ chowdsp::Oversampling oversampler;
+
+ Kickall() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ // TODO: review this mapping, using displayBase multiplier seems more normal
+ configParam(TUNE_PARAM, FREQ_A0, FREQ_B2, 0.5 * (FREQ_A0 + FREQ_B2), "Tune", "Hz");
+ configParam(TRIGG_BUTTON_PARAM, 0.f, 1.f, 0.f, "Manual trigger");
+ configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Wave shape");
+ configParam(DECAY_PARAM, 0.f, 1.f, 0.01f, "VCA Envelope decay time");
+ configParam(TIME_PARAM, 0.f, 1.0f, 0.f, "Pitch envelope decay time");
+ configParam(BEND_PARAM, 0.f, 1.f, 0.f, "Pitch envelope attenuator");
+
+ volume.attackTime = 0.01;
+ volume.attackShape = 0.5;
+ volume.decayShape = 3.0;
+ pitch.attackTime = 0.00165;
+ pitch.decayShape = 3.0;
+
+ // calculate up/downsampling rates
+ onSampleRateChange();
+ }
+
+ void onSampleRateChange() override {
+ oversampler.reset(APP->engine->getSampleRate());
+ }
+
+ void process(const ProcessArgs& args) override {
+ // TODO: check values
+ if (trigger.process(inputs[TRIGG_INPUT].getVoltage() / 2.0f + params[TRIGG_BUTTON_PARAM].getValue() * 10.0)) {
+ volume.trigger();
+ pitch.trigger();
+ }
+
+ const float vcaGain = clamp(inputs[VOLUME_INPUT].getNormalVoltage(10.f) / 10.f, 0.f, 1.0f);
+
+ // pitch envelope
+ const float bend = bendRange * std::pow(params[BEND_PARAM].getValue(), 3.0);
+ pitch.decayTime = rescale(params[TIME_PARAM].getValue(), 0.f, 1.0f, minPitchDecay, maxPitchDecay);
+ pitch.process(args.sampleTime);
+
+ // volume envelope
+ const float volumeDecay = minVolumeDecay * std::pow(2.f, params[DECAY_PARAM].getValue() * std::log2(maxVolumeDecay / minVolumeDecay));
+ volume.decayTime = clamp(volumeDecay + inputs[DECAY_INPUT].getVoltage() * 0.1f, 0.01, 10.0);
+ volume.process(args.sampleTime);
+
+ float freq = params[TUNE_PARAM].getValue();
+ freq *= std::pow(2.f, inputs[TUNE_INPUT].getVoltage());
+
+ const float kickFrequency = std::max(10.0f, freq + bend * pitch.env);
+ const float phaseInc = clamp(args.sampleTime * kickFrequency / UPSAMPLE, 1e-6, 0.35f);
+
+ const float shape = clamp(inputs[SHAPE_INPUT].getVoltage() / 10.f + params[SHAPE_PARAM].getValue(), 0.0f, 1.0f) * 0.99f;
+ const float shapeB = (1.0f - shape) / (1.0f + shape);
+ const float shapeA = (4.0f * shape) / ((1.0f - shape) * (1.0f + shape));
+
+ float* inputBuf = oversampler.getOSBuffer();
+ for (int i = 0; i < UPSAMPLE; ++i) {
+ phase += phaseInc;
+ phase -= std::floor(phase);
+
+ inputBuf[i] = sin2pi_pade_05_5_4(phase);
+ inputBuf[i] = inputBuf[i] * (shapeA + shapeB) / ((std::abs(inputBuf[i]) * shapeA) + shapeB);
+ }
+
+ const float out = volume.env * oversampler.downsample() * 5.0f * vcaGain;
+ outputs[OUT_OUTPUT].setVoltage(out);
+
+ lights[ENV_LIGHT].setBrightness(volume.env);
+ }
+};
+
+
+struct KickallWidget : ModuleWidget {
+ KickallWidget(Kickall* module) {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Kickall.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(createParamCentered(mm2px(Vec(8.472, 28.97)), module, Kickall::TUNE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(22.409, 29.159)), module, Kickall::TRIGG_BUTTON_PARAM));
+ addParam(createParamCentered(mm2px(Vec(15.526, 49.292)), module, Kickall::SHAPE_PARAM));
+ addParam(createParam(mm2px(Vec(19.667, 63.897)), module, Kickall::DECAY_PARAM));
+ addParam(createParamCentered(mm2px(Vec(8.521, 71.803)), module, Kickall::TIME_PARAM));
+ addParam(createParamCentered(mm2px(Vec(8.521, 93.517)), module, Kickall::BEND_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(15.501, 14.508)), module, Kickall::VOLUME_INPUT));
+ addInput(createInputCentered(mm2px(Vec(5.499, 14.536)), module, Kickall::TRIGG_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.525, 113.191)), module, Kickall::DECAY_INPUT));
+ addInput(createInputCentered(mm2px(Vec(5.499, 113.208)), module, Kickall::TUNE_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.485, 113.208)), module, Kickall::SHAPE_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(25.525, 14.52)), module, Kickall::OUT_OUTPUT));
+
+ addChild(createLightCentered>(mm2px(Vec(15.535, 34.943)), module, Kickall::ENV_LIGHT));
+ }
+};
+
+
+Model* modelKickall = createModel("Kickall");
\ No newline at end of file
diff --git a/src/Mixer.cpp b/src/Mixer.cpp
index d097e75..1322f00 100644
--- a/src/Mixer.cpp
+++ b/src/Mixer.cpp
@@ -1,5 +1,6 @@
#include "plugin.hpp"
+using simd::float_4;
struct Mixer : Module {
enum ParamIds {
@@ -24,6 +25,7 @@ struct Mixer : Module {
enum LightIds {
OUT_POS_LIGHT,
OUT_NEG_LIGHT,
+ OUT_BLUE_LIGHT,
NUM_LIGHTS
};
@@ -35,25 +37,71 @@ struct Mixer : Module {
configParam(CH4_PARAM, 0.0, 1.0, 0.0, "Ch 4 level", "%", 0, 100);
}
- void process(const ProcessArgs &args) override {
- float in1 = inputs[IN1_INPUT].getVoltage() * params[CH1_PARAM].getValue();
- float in2 = inputs[IN2_INPUT].getVoltage() * params[CH2_PARAM].getValue();
- float in3 = inputs[IN3_INPUT].getVoltage() * params[CH3_PARAM].getValue();
- float in4 = inputs[IN4_INPUT].getVoltage() * params[CH4_PARAM].getValue();
-
- float out = in1 + in2 + in3 + in4;
- outputs[OUT1_OUTPUT].setVoltage(out);
- outputs[OUT2_OUTPUT].setVoltage(-out);
- lights[OUT_POS_LIGHT].setSmoothBrightness(out / 5.f, args.sampleTime);
- lights[OUT_NEG_LIGHT].setSmoothBrightness(-out / 5.f, args.sampleTime);
+ void process(const ProcessArgs& args) override {
+ int channels1 = inputs[IN1_INPUT].getChannels();
+ int channels2 = inputs[IN2_INPUT].getChannels();
+ int channels3 = inputs[IN3_INPUT].getChannels();
+ int channels4 = inputs[IN4_INPUT].getChannels();
+
+ int out_channels = 1;
+ out_channels = std::max(out_channels, channels1);
+ out_channels = std::max(out_channels, channels2);
+ out_channels = std::max(out_channels, channels3);
+ out_channels = std::max(out_channels, channels4);
+
+ float_4 out[4] = {};
+
+ if (inputs[IN1_INPUT].isConnected()) {
+ for (int c = 0; c < channels1; c += 4)
+ out[c / 4] += inputs[IN1_INPUT].getVoltageSimd(c) * params[CH1_PARAM].getValue();
+ }
+
+ if (inputs[IN2_INPUT].isConnected()) {
+ for (int c = 0; c < channels2; c += 4)
+ out[c / 4] += inputs[IN2_INPUT].getVoltageSimd(c) * params[CH2_PARAM].getValue();
+ }
+
+ if (inputs[IN3_INPUT].isConnected()) {
+ for (int c = 0; c < channels3; c += 4)
+ out[c / 4] += inputs[IN3_INPUT].getVoltageSimd(c) * params[CH3_PARAM].getValue();
+ }
+
+ if (inputs[IN4_INPUT].isConnected()) {
+ for (int c = 0; c < channels4; c += 4)
+ out[c / 4] += inputs[IN4_INPUT].getVoltageSimd(c) * params[CH4_PARAM].getValue();
+ }
+
+ outputs[OUT1_OUTPUT].setChannels(out_channels);
+ outputs[OUT2_OUTPUT].setChannels(out_channels);
+
+ for (int c = 0; c < out_channels; c += 4) {
+ outputs[OUT1_OUTPUT].setVoltageSimd(out[c / 4], c);
+ out[c / 4] *= -1.f;
+ outputs[OUT2_OUTPUT].setVoltageSimd(out[c / 4], c);
+ }
+
+ if (out_channels == 1) {
+ float light = outputs[OUT1_OUTPUT].getVoltage();
+ lights[OUT_POS_LIGHT].setSmoothBrightness(light / 5.f, args.sampleTime);
+ lights[OUT_NEG_LIGHT].setSmoothBrightness(-light / 5.f, args.sampleTime);
+ }
+ else {
+ float light = 0.0f;
+ for (int c = 0; c < out_channels; c++) {
+ float tmp = outputs[OUT1_OUTPUT].getVoltage(c);
+ light += tmp * tmp;
+ }
+ light = std::sqrt(light);
+ lights[OUT_POS_LIGHT].setBrightness(0.0f);
+ lights[OUT_NEG_LIGHT].setBrightness(0.0f);
+ lights[OUT_BLUE_LIGHT].setSmoothBrightness(light / 5.f, args.sampleTime);
+ }
}
};
-
-
struct MixerWidget : ModuleWidget {
- MixerWidget(Mixer *module) {
+ MixerWidget(Mixer* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Mixer.svg")));
@@ -65,18 +113,18 @@ struct MixerWidget : ModuleWidget {
addParam(createParam(Vec(19, 137), module, Mixer::CH3_PARAM));
addParam(createParam(Vec(19, 190), module, Mixer::CH4_PARAM));
- addInput(createInput(Vec(7, 242), module, Mixer::IN1_INPUT));
- addInput(createInput(Vec(43, 242), module, Mixer::IN2_INPUT));
+ addInput(createInput(Vec(7, 242), module, Mixer::IN1_INPUT));
+ addInput(createInput(Vec(43, 242), module, Mixer::IN2_INPUT));
- addInput(createInput(Vec(7, 281), module, Mixer::IN3_INPUT));
- addInput(createInput(Vec(43, 281), module, Mixer::IN4_INPUT));
+ addInput(createInput(Vec(7, 281), module, Mixer::IN3_INPUT));
+ addInput(createInput(Vec(43, 281), module, Mixer::IN4_INPUT));
- addOutput(createOutput(Vec(7, 324), module, Mixer::OUT1_OUTPUT));
- addOutput(createOutput(Vec(43, 324), module, Mixer::OUT2_OUTPUT));
+ addOutput(createOutput(Vec(7, 324), module, Mixer::OUT1_OUTPUT));
+ addOutput(createOutput(Vec(43, 324), module, Mixer::OUT2_OUTPUT));
- addChild(createLight>(Vec(32.7, 310), module, Mixer::OUT_POS_LIGHT));
+ addChild(createLight>(Vec(32.7, 310), module, Mixer::OUT_POS_LIGHT));
}
};
-Model *modelMixer = createModel("Mixer");
+Model* modelMixer = createModel("Mixer");
diff --git a/src/Percall.cpp b/src/Percall.cpp
new file mode 100644
index 0000000..175bd06
--- /dev/null
+++ b/src/Percall.cpp
@@ -0,0 +1,220 @@
+#include "plugin.hpp"
+
+using simd::float_4;
+
+struct Percall : Module {
+ enum ParamIds {
+ ENUMS(VOL_PARAMS, 4),
+ ENUMS(DECAY_PARAMS, 4),
+ ENUMS(CHOKE_PARAMS, 2),
+ NUM_PARAMS
+ };
+ enum InputIds {
+ ENUMS(CH_INPUTS, 4),
+ STRENGTH_INPUT,
+ ENUMS(TRIG_INPUTS, 4),
+ ENUMS(CV_INPUTS, 4),
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ ENUMS(CH_OUTPUTS, 4),
+ ENUMS(ENV_OUTPUTS, 4),
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ ENUMS(LEDS, 4),
+ NUM_LIGHTS
+ };
+
+ ADEnvelope envs[4];
+
+ float gains[4] = {};
+
+ dsp::SchmittTrigger trigger[4];
+ dsp::ClockDivider cvDivider;
+ dsp::ClockDivider lightDivider;
+ const int LAST_CHANNEL_ID = 3;
+
+ const float attackTime = 1.5e-3;
+ const float minDecayTime = 4.5e-3;
+ const float maxDecayTime = 4.f;
+
+ Percall() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ for (int i = 0; i < 4; i++) {
+ configParam(VOL_PARAMS + i, 0.f, 1.f, 1.f, string::f("Channel %d level", i + 1), "%", 0, 100);
+ configParam(DECAY_PARAMS + i, 0.f, 1.f, 0.f, string::f("Channel %d decay time", i + 1));
+ envs[i].attackTime = attackTime;
+ envs[i].attackShape = 0.5f;
+ envs[i].decayShape = 2.0f;
+ }
+
+ for (int i = 0; i < 2; i++) {
+ configParam(CHOKE_PARAMS + i, 0.f, 1.f, 0.f, string::f("Choke %d to %d", 2 * i + 1, 2 * i + 2));
+ }
+
+ cvDivider.setDivision(16);
+ lightDivider.setDivision(128);
+ }
+
+ void process(const ProcessArgs& args) override {
+
+ float strength = 1.0f;
+ if (inputs[STRENGTH_INPUT].isConnected()) {
+ strength = std::sqrt(clamp(inputs[STRENGTH_INPUT].getVoltage() / 10.0f, 0.0f, 1.0f));
+ }
+
+ // only calculate gains/decays every 16 samples
+ if (cvDivider.process()) {
+ for (int i = 0; i < 4; i++) {
+ gains[i] = std::pow(params[VOL_PARAMS + i].getValue(), 2.f) * strength;
+
+ float fallCv = inputs[CV_INPUTS + i].getVoltage() * 0.05f + params[DECAY_PARAMS + i].getValue();
+ envs[i].decayTime = rescale(std::pow(clamp(fallCv, 0.f, 1.0f), 2.f), 0.f, 1.f, minDecayTime, maxDecayTime);
+ }
+ }
+
+ float_4 mix[4] = {};
+ int maxPolyphonyChannels = 1;
+
+ // Mixer channels
+ for (int i = 0; i < 4; i++) {
+
+ if (trigger[i].process(rescale(inputs[TRIG_INPUTS + i].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) {
+ envs[i].trigger();
+ }
+ // if choke is enabled, and current channel is odd and left channel is in attack
+ if ((i % 2) && params[CHOKE_PARAMS + i / 2].getValue() && envs[i - 1].stage == ADEnvelope::STAGE_ATTACK) {
+ // TODO: is there a more graceful way to choke, e.g. rapid envelope?
+ // TODO: this will just silence it instantly, maybe switch to STAGE_DECAY and modify fall time
+ envs[i].stage = ADEnvelope::STAGE_OFF;
+ }
+
+ envs[i].process(args.sampleTime);
+
+ int polyphonyChannels = 1;
+ float_4 in[4] = {};
+ bool inputIsConnected = inputs[CH_INPUTS + i].isConnected();
+ bool inputIsNormed = !inputIsConnected && (i % 2) && inputs[CH_INPUTS + i - 1].isConnected();
+ if ((inputIsConnected || inputIsNormed)) {
+ int channelToReadFrom = inputIsNormed ? CH_INPUTS + i - 1 : CH_INPUTS + i;
+ polyphonyChannels = inputs[channelToReadFrom].getChannels();
+
+ // an input only counts towards the main output polyphony count if it's not taken out of the mix
+ // (i.e. an output is patched in). the final input should always count towards polyphony count.
+ if (i == CH_INPUTS_LAST || !outputs[CH_OUTPUTS + i].isConnected()) {
+ maxPolyphonyChannels = std::max(maxPolyphonyChannels, polyphonyChannels);
+ }
+
+ // only process input audio if envelope is active
+ if (envs[i].stage != ADEnvelope::STAGE_OFF) {
+ float gain = gains[i] * envs[i].env;
+ for (int c = 0; c < polyphonyChannels; c += 4) {
+ in[c / 4] = inputs[channelToReadFrom].getVoltageSimd(c) * gain;
+ }
+ }
+ }
+
+ if (i != LAST_CHANNEL_ID) {
+ // if connected, output via the jack (and don't add to mix)
+ if (outputs[CH_OUTPUTS + i].isConnected()) {
+ outputs[CH_OUTPUTS + i].setChannels(polyphonyChannels);
+ for (int c = 0; c < polyphonyChannels; c += 4) {
+ outputs[CH_OUTPUTS + i].setVoltageSimd(in[c / 4], c);
+ }
+ }
+ else {
+ // else add to mix
+ for (int c = 0; c < polyphonyChannels; c += 4) {
+ mix[c / 4] += in[c / 4];
+ }
+ }
+ }
+ // otherwise if it is the final channel and it's wired in
+ else if (outputs[CH_OUTPUTS + i].isConnected()) {
+
+ outputs[CH_OUTPUTS + i].setChannels(maxPolyphonyChannels);
+
+ // last channel must always go into mix
+ for (int c = 0; c < polyphonyChannels; c += 4) {
+ mix[c / 4] += in[c / 4];
+ }
+
+ for (int c = 0; c < maxPolyphonyChannels; c += 4) {
+ outputs[CH_OUTPUTS + i].setVoltageSimd(mix[c / 4], c);
+ }
+ }
+
+ // set env output
+ if (outputs[ENV_OUTPUTS + i].isConnected()) {
+ outputs[ENV_OUTPUTS + i].setVoltage(10.f * strength * envs[i].env);
+ }
+ }
+
+ if (lightDivider.process()) {
+ for (int i = 0; i < 4; i++) {
+ lights[LEDS + i].setBrightness(envs[i].env);
+ }
+ }
+
+ }
+};
+
+
+struct PercallWidget : ModuleWidget {
+ PercallWidget(Percall* module) {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Percall.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(8.048, 41.265)), module, Percall::VOL_PARAMS + 0));
+ addParam(createParamCentered(mm2px(Vec(22.879, 41.265)), module, Percall::VOL_PARAMS + 1));
+ addParam(createParamCentered(mm2px(Vec(37.709, 41.265)), module, Percall::VOL_PARAMS + 2));
+ addParam(createParamCentered(mm2px(Vec(52.54, 41.265)), module, Percall::VOL_PARAMS + 3));
+ addParam(createParam(mm2px(Vec(5.385, 53.912)), module, Percall::DECAY_PARAMS + 0));
+ addParam(createParam(mm2px(Vec(20.292, 53.912)), module, Percall::DECAY_PARAMS + 1));
+ addParam(createParam(mm2px(Vec(35.173, 53.912)), module, Percall::DECAY_PARAMS + 2));
+ addParam(createParam(mm2px(Vec(49.987, 53.912)), module, Percall::DECAY_PARAMS + 3));
+
+ addParam(createParam(mm2px(Vec(13.365, 58.672)), module, Percall::CHOKE_PARAMS + 0));
+ addParam(createParam(mm2px(Vec(42.993, 58.672)), module, Percall::CHOKE_PARAMS + 1));
+
+ addInput(createInputCentered(mm2px(Vec(7.15, 12.905)), module, Percall::CH_INPUTS + 0));
+ addInput(createInputCentered(mm2px(Vec(20.298, 12.905)), module, Percall::CH_INPUTS + 1));
+ addInput(createInputCentered(mm2px(Vec(40.266, 12.905)), module, Percall::CH_INPUTS + 2));
+ addInput(createInputCentered(mm2px(Vec(53.437, 12.905)), module, Percall::CH_INPUTS + 3));
+
+ addInput(createInputCentered(mm2px(Vec(30.282, 18.221)), module, Percall::STRENGTH_INPUT));
+ addInput(createInputCentered(mm2px(Vec(7.15, 24.827)), module, Percall::TRIG_INPUTS + 0));
+ addInput(createInputCentered(mm2px(Vec(18.488, 23.941)), module, Percall::TRIG_INPUTS + 1));
+ addInput(createInputCentered(mm2px(Vec(42.171, 23.95)), module, Percall::TRIG_INPUTS + 2));
+ addInput(createInputCentered(mm2px(Vec(53.437, 24.827)), module, Percall::TRIG_INPUTS + 3));
+
+ addInput(createInputCentered(mm2px(Vec(5.037, 101.844)), module, Percall::CV_INPUTS + 0));
+ addInput(createInputCentered(mm2px(Vec(15.159, 101.844)), module, Percall::CV_INPUTS + 1));
+ addInput(createInputCentered(mm2px(Vec(25.28, 101.844)), module, Percall::CV_INPUTS + 2));
+ addInput(createInputCentered(mm2px(Vec(35.402, 101.844)), module, Percall::CV_INPUTS + 3));
+
+ addOutput(createOutputCentered(mm2px(Vec(45.524, 101.844)), module, Percall::CH_OUTPUTS + 0));
+ addOutput(createOutputCentered(mm2px(Vec(55.645, 101.844)), module, Percall::CH_OUTPUTS + 1));
+ addOutput(createOutputCentered(mm2px(Vec(45.524, 113.766)), module, Percall::CH_OUTPUTS + 2));
+ addOutput(createOutputCentered(mm2px(Vec(55.645, 113.766)), module, Percall::CH_OUTPUTS + 3));
+
+ addOutput(createOutputCentered(mm2px(Vec(5.037, 113.766)), module, Percall::ENV_OUTPUTS + 0));
+ addOutput(createOutputCentered(mm2px(Vec(15.159, 113.766)), module, Percall::ENV_OUTPUTS + 1));
+ addOutput(createOutputCentered(mm2px(Vec(25.28, 113.766)), module, Percall::ENV_OUTPUTS + 2));
+ addOutput(createOutputCentered(mm2px(Vec(35.402, 113.766)), module, Percall::ENV_OUTPUTS + 3));
+
+ addChild(createLightCentered>(mm2px(Vec(8.107, 49.221)), module, Percall::LEDS + 0));
+ addChild(createLightCentered>(mm2px(Vec(22.934, 49.221)), module, Percall::LEDS + 1));
+ addChild(createLightCentered>(mm2px(Vec(37.762, 49.221)), module, Percall::LEDS + 2));
+ addChild(createLightCentered>(mm2px(Vec(52.589, 49.221)), module, Percall::LEDS + 3));
+ }
+};
+
+
+Model* modelPercall = createModel("Percall");
\ No newline at end of file
diff --git a/src/PulseGenerator_4.hpp b/src/PulseGenerator_4.hpp
new file mode 100644
index 0000000..f3229ad
--- /dev/null
+++ b/src/PulseGenerator_4.hpp
@@ -0,0 +1,28 @@
+#pragma once
+#include
+
+
+/** When triggered, holds a high value for a specified time before going low again */
+struct PulseGenerator_4 {
+ simd::float_4 remaining = 0.f;
+
+ /** Immediately disables the pulse */
+ void reset() {
+ remaining = 0.f;
+ }
+
+ /** Advances the state by `deltaTime`. Returns whether the pulse is in the HIGH state. */
+ simd::float_4 process(float deltaTime) {
+
+ simd::float_4 mask = (remaining > 0.f);
+
+ remaining -= ifelse(mask, deltaTime, 0.f);
+ return ifelse(mask, simd::float_4::mask(), 0.f);
+ }
+
+ /** Begins a trigger with the given `duration`. */
+ void trigger(simd::float_4 mask, float duration = 1e-3f) {
+ // Keep the previous pulse if the existing pulse will be held longer than the currently requested one.
+ remaining = ifelse(mask & (duration > remaining), duration, remaining);
+ }
+};
diff --git a/src/Rampage.cpp b/src/Rampage.cpp
index 0e99035..24a7d92 100644
--- a/src/Rampage.cpp
+++ b/src/Rampage.cpp
@@ -1,19 +1,20 @@
#include "plugin.hpp"
+#include "PulseGenerator_4.hpp"
+using simd::float_4;
-static float shapeDelta(float delta, float tau, float shape) {
- float lin = sgn(delta) * 10.f / tau;
+static float_4 shapeDelta(float_4 delta, float_4 tau, float shape) {
+ float_4 lin = simd::sgn(delta) * 10.f / tau;
if (shape < 0.f) {
- float log = sgn(delta) * 40.f / tau / (std::fabs(delta) + 1.f);
- return crossfade(lin, log, -shape * 0.95f);
+ float_4 log = simd::sgn(delta) * 40.f / tau / (simd::fabs(delta) + 1.f);
+ return simd::crossfade(lin, log, -shape * 0.95f);
}
else {
- float exp = M_E * delta / tau;
- return crossfade(lin, exp, shape * 0.90f);
+ float_4 exp = M_E * delta / tau;
+ return simd::crossfade(lin, exp, shape * 0.90f);
}
}
-
struct Rampage : Module {
enum ParamIds {
RANGE_A_PARAM,
@@ -61,22 +62,26 @@ struct Rampage : Module {
NUM_OUTPUTS
};
enum LightIds {
- COMPARATOR_LIGHT,
- MIN_LIGHT,
- MAX_LIGHT,
- OUT_A_LIGHT,
- OUT_B_LIGHT,
- RISING_A_LIGHT,
- RISING_B_LIGHT,
- FALLING_A_LIGHT,
- FALLING_B_LIGHT,
+ ENUMS(COMPARATOR_LIGHT, 3),
+ ENUMS(MIN_LIGHT, 3),
+ ENUMS(MAX_LIGHT, 3),
+ ENUMS(OUT_A_LIGHT, 3),
+ ENUMS(OUT_B_LIGHT, 3),
+ ENUMS(RISING_A_LIGHT, 3),
+ ENUMS(RISING_B_LIGHT, 3),
+ ENUMS(FALLING_A_LIGHT, 3),
+ ENUMS(FALLING_B_LIGHT, 3),
NUM_LIGHTS
};
- float out[2] = {};
- bool gate[2] = {};
- dsp::SchmittTrigger trigger[2];
- dsp::PulseGenerator endOfCyclePulse[2];
+
+ float_4 out[2][4] = {};
+ float_4 gate[2][4] = {}; // use simd __m128 logic instead of bool
+
+ dsp::TSchmittTrigger trigger_4[2][4];
+ PulseGenerator_4 endOfCyclePulse[2][4];
+
+ // ChannelMask channelMask;
Rampage() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
@@ -95,103 +100,230 @@ struct Rampage : Module {
configParam(BALANCE_PARAM, 0.0, 1.0, 0.5, "Balance");
}
- void process(const ProcessArgs &args) override {
- for (int c = 0; c < 2; c++) {
- float in = inputs[IN_A_INPUT + c].getVoltage();
- if (trigger[c].process(params[TRIGG_A_PARAM + c].getValue() * 10.0 + inputs[TRIGG_A_INPUT + c].getVoltage() / 2.0)) {
- gate[c] = true;
+ void process(const ProcessArgs& args) override {
+ int channels_in[2] = {};
+ int channels_trig[2] = {};
+ int channels[2] = {}; // the larger of in or trig (per-part)
+
+ // determine number of channels:
+
+ for (int part = 0; part < 2; part++) {
+
+ channels_in[part] = inputs[IN_A_INPUT + part].getChannels();
+ channels_trig[part] = inputs[TRIGG_A_INPUT + part].getChannels();
+ channels[part] = std::max(channels_in[part], channels_trig[part]);
+ channels[part] = std::max(1, channels[part]);
+
+ outputs[OUT_A_OUTPUT + part].setChannels(channels[part]);
+ outputs[RISING_A_OUTPUT + part].setChannels(channels[part]);
+ outputs[FALLING_A_OUTPUT + part].setChannels(channels[part]);
+ outputs[EOC_A_OUTPUT + part].setChannels(channels[part]);
+ }
+
+ // total number of active polyphony engines, accounting for both halves
+ // (channels[0] / channels[1] are the number of active engines per section)
+ const int channels_max = std::max(channels[0], channels[1]);
+
+ outputs[COMPARATOR_OUTPUT].setChannels(channels_max);
+ outputs[MIN_OUTPUT].setChannels(channels_max);
+ outputs[MAX_OUTPUT].setChannels(channels_max);
+
+ // loop over two parts of Rampage:
+ for (int part = 0; part < 2; part++) {
+
+ float_4 in[4] = {};
+ float_4 in_trig[4] = {};
+ float_4 riseCV[4] = {};
+ float_4 fallCV[4] = {};
+ float_4 cycle[4] = {};
+
+ // get parameters:
+ float minTime;
+ switch ((int) params[RANGE_A_PARAM + part].getValue()) {
+ case 0:
+ minTime = 1e-2;
+ break;
+ case 1:
+ minTime = 1e-3;
+ break;
+ default:
+ minTime = 1e-1;
+ break;
}
- if (gate[c]) {
- in = 10.0;
+
+ float_4 param_rise = params[RISE_A_PARAM + part].getValue() * 10.0f;
+ float_4 param_fall = params[FALL_A_PARAM + part].getValue() * 10.0f;
+ float_4 param_trig = params[TRIGG_A_PARAM + part].getValue() * 20.0f;
+ float_4 param_cycle = params[CYCLE_A_PARAM + part].getValue() * 10.0f;
+
+ for (int c = 0; c < channels[part]; c += 4) {
+ riseCV[c / 4] = param_rise;
+ fallCV[c / 4] = param_fall;
+ cycle[c / 4] = param_cycle;
+ in_trig[c / 4] = param_trig;
}
- float shape = params[SHAPE_A_PARAM + c].getValue();
- float delta = in - out[c];
+ // read inputs:
+ if (inputs[IN_A_INPUT + part].isConnected()) {
+ for (int c = 0; c < channels[part]; c += 4)
+ in[c / 4] = inputs[IN_A_INPUT + part].getPolyVoltageSimd(c);
+ }
- // Integrator
- float minTime;
- switch ((int) params[RANGE_A_PARAM + c].getValue()) {
- case 0: minTime = 1e-2; break;
- case 1: minTime = 1e-3; break;
- default: minTime = 1e-1; break;
+ if (inputs[TRIGG_A_INPUT + part].isConnected()) {
+ for (int c = 0; c < channels[part]; c += 4)
+ in_trig[c / 4] += inputs[TRIGG_A_INPUT + part].getPolyVoltageSimd(c);
}
- bool rising = false;
- bool falling = false;
-
- if (delta > 0) {
- // Rise
- float riseCv = params[RISE_A_PARAM + c].getValue() - inputs[EXP_CV_A_INPUT + c].getVoltage() / 10.0 + inputs[RISE_CV_A_INPUT + c].getVoltage() / 10.0;
- riseCv = clamp(riseCv, 0.0f, 1.0f);
- float rise = minTime * std::pow(2.0, riseCv * 10.0);
- out[c] += shapeDelta(delta, rise, shape) * args.sampleTime;
- rising = (in - out[c] > 1e-3);
- if (!rising) {
- gate[c] = false;
+ if (inputs[EXP_CV_A_INPUT + part].isConnected()) {
+ float_4 expCV[4] = {};
+ for (int c = 0; c < channels[part]; c += 4)
+ expCV[c / 4] = inputs[EXP_CV_A_INPUT + part].getPolyVoltageSimd(c);
+
+ for (int c = 0; c < channels[part]; c += 4) {
+ riseCV[c / 4] -= expCV[c / 4];
+ fallCV[c / 4] -= expCV[c / 4];
}
}
- else if (delta < 0) {
- // Fall
- float fallCv = params[FALL_A_PARAM + c].getValue() - inputs[EXP_CV_A_INPUT + c].getVoltage() / 10.0 + inputs[FALL_CV_A_INPUT + c].getVoltage() / 10.0;
- fallCv = clamp(fallCv, 0.0f, 1.0f);
- float fall = minTime * std::pow(2.0, fallCv * 10.0);
- out[c] += shapeDelta(delta, fall, shape) * args.sampleTime;
- falling = (in - out[c] < -1e-3);
- if (!falling) {
- // End of cycle, check if we should turn the gate back on (cycle mode)
- endOfCyclePulse[c].trigger(1e-3);
- if (params[CYCLE_A_PARAM + c].getValue() * 10.0 + inputs[CYCLE_A_INPUT + c].getVoltage() >= 4.0) {
- gate[c] = true;
- }
- }
+
+ for (int c = 0; c < channels[part]; c += 4)
+ riseCV[c / 4] += inputs[RISE_CV_A_INPUT + part].getPolyVoltageSimd(c);
+ for (int c = 0; c < channels[part]; c += 4)
+ fallCV[c / 4] += inputs[FALL_CV_A_INPUT + part].getPolyVoltageSimd(c);
+ for (int c = 0; c < channels[part]; c += 4)
+ cycle[c / 4] += inputs[CYCLE_A_INPUT + part].getPolyVoltageSimd(c);
+
+ // start processing:
+ for (int c = 0; c < channels[part]; c += 4) {
+
+ // process SchmittTriggers
+ float_4 trig_mask = trigger_4[part][c / 4].process(in_trig[c / 4] / 2.0);
+ gate[part][c / 4] = ifelse(trig_mask, float_4::mask(), gate[part][c / 4]);
+ in[c / 4] = ifelse(gate[part][c / 4], 10.0f, in[c / 4]);
+
+ float_4 delta = in[c / 4] - out[part][c / 4];
+
+ // rise / fall branching
+ float_4 delta_gt_0 = delta > 0.f;
+ float_4 delta_lt_0 = delta < 0.f;
+ float_4 delta_eq_0 = ~(delta_lt_0 | delta_gt_0);
+
+ float_4 rateCV = ifelse(delta_gt_0, riseCV[c / 4], 0.f);
+ rateCV = ifelse(delta_lt_0, fallCV[c / 4], rateCV);
+ rateCV = clamp(rateCV, 0.f, 10.0f);
+
+ float_4 rate = minTime * simd::pow(2.0f, rateCV);
+
+ float shape = params[SHAPE_A_PARAM + part].getValue();
+ out[part][c / 4] += shapeDelta(delta, rate, shape) * args.sampleTime;
+
+ float_4 rising = (in[c / 4] - out[part][c / 4]) > 1e-3f;
+ float_4 falling = (in[c / 4] - out[part][c / 4]) < -1e-3f;
+ float_4 end_of_cycle = simd::andnot(falling, delta_lt_0);
+
+ endOfCyclePulse[part][c / 4].trigger(end_of_cycle, 1e-3);
+
+ gate[part][c / 4] = ifelse(simd::andnot(rising, delta_gt_0), 0.f, gate[part][c / 4]);
+ gate[part][c / 4] = ifelse(end_of_cycle & (cycle[c / 4] >= 4.0f), float_4::mask(), gate[part][c / 4]);
+ gate[part][c / 4] = ifelse(delta_eq_0, 0.f, gate[part][c / 4]);
+
+ out[part][c / 4] = ifelse(rising | falling, out[part][c / 4], in[c / 4]);
+
+ float_4 out_rising = ifelse(rising, 10.0f, 0.f);
+ float_4 out_falling = ifelse(falling, 10.0f, 0.f);
+
+ float_4 pulse = endOfCyclePulse[part][c / 4].process(args.sampleTime);
+ float_4 out_EOC = ifelse(pulse, 10.f, 0.f);
+
+ outputs[OUT_A_OUTPUT + part].setVoltageSimd(out[part][c / 4], c);
+ outputs[RISING_A_OUTPUT + part].setVoltageSimd(out_rising, c);
+ outputs[FALLING_A_OUTPUT + part].setVoltageSimd(out_falling, c);
+ outputs[EOC_A_OUTPUT + part].setVoltageSimd(out_EOC, c);
+
+ } // for(int c, ...)
+
+ if (channels[part] == 1) {
+ lights[RISING_A_LIGHT + 3 * part ].setSmoothBrightness(outputs[RISING_A_OUTPUT + part].getVoltage() / 10.f, args.sampleTime);
+ lights[RISING_A_LIGHT + 3 * part + 1].setBrightness(0.0f);
+ lights[RISING_A_LIGHT + 3 * part + 2].setBrightness(0.0f);
+ lights[FALLING_A_LIGHT + 3 * part ].setSmoothBrightness(outputs[FALLING_A_OUTPUT + part].getVoltage() / 10.f, args.sampleTime);
+ lights[FALLING_A_LIGHT + 3 * part + 1].setBrightness(0.0f);
+ lights[FALLING_A_LIGHT + 3 * part + 2].setBrightness(0.0f);
+ lights[OUT_A_LIGHT + 3 * part ].setSmoothBrightness(out[part][0].s[0] / 10.0, args.sampleTime);
+ lights[OUT_A_LIGHT + 3 * part + 1].setBrightness(0.0f);
+ lights[OUT_A_LIGHT + 3 * part + 2].setBrightness(0.0f);
}
else {
- gate[c] = false;
+ lights[RISING_A_LIGHT + 3 * part ].setBrightness(0.0f);
+ lights[RISING_A_LIGHT + 3 * part + 1].setBrightness(0.0f);
+ lights[RISING_A_LIGHT + 3 * part + 2].setBrightness(10.0f);
+ lights[FALLING_A_LIGHT + 3 * part ].setBrightness(0.0f);
+ lights[FALLING_A_LIGHT + 3 * part + 1].setBrightness(0.0f);
+ lights[FALLING_A_LIGHT + 3 * part + 2].setBrightness(10.0f);
+ lights[OUT_A_LIGHT + 3 * part ].setBrightness(0.0f);
+ lights[OUT_A_LIGHT + 3 * part + 1].setBrightness(0.0f);
+ lights[OUT_A_LIGHT + 3 * part + 2].setBrightness(10.0f);
}
- if (!rising && !falling) {
- out[c] = in;
- }
+ } // for (int part, ... )
- outputs[RISING_A_OUTPUT + c].setVoltage((rising ? 10.0 : 0.0));
- outputs[FALLING_A_OUTPUT + c].setVoltage((falling ? 10.0 : 0.0));
- lights[RISING_A_LIGHT + c].setSmoothBrightness(rising ? 1.0 : 0.0, args.sampleTime);
- lights[FALLING_A_LIGHT + c].setSmoothBrightness(falling ? 1.0 : 0.0, args.sampleTime);
- outputs[EOC_A_OUTPUT + c].setVoltage((endOfCyclePulse[c].process(args.sampleTime) ? 10.0 : 0.0));
- outputs[OUT_A_OUTPUT + c].setVoltage(out[c]);
- lights[OUT_A_LIGHT + c].setSmoothBrightness(out[c] / 10.0, args.sampleTime);
- }
// Logic
float balance = params[BALANCE_PARAM].getValue();
- float a = out[0];
- float b = out[1];
- if (balance < 0.5)
- b *= 2.0 * balance;
- else if (balance > 0.5)
- a *= 2.0 * (1.0 - balance);
- outputs[COMPARATOR_OUTPUT].setVoltage((b > a ? 10.0 : 0.0));
- outputs[MIN_OUTPUT].setVoltage(std::min(a, b));
- outputs[MAX_OUTPUT].setVoltage(std::max(a, b));
+
+ for (int c = 0; c < channels_max; c += 4) {
+
+ float_4 a = out[0][c / 4];
+ float_4 b = out[1][c / 4];
+
+ if (balance < 0.5)
+ b *= 2.0f * balance;
+ else if (balance > 0.5)
+ a *= 2.0f * (1.0 - balance);
+
+ float_4 comp = ifelse(b > a, 10.0f, 0.f);
+ float_4 out_min = simd::fmin(a, b);
+ float_4 out_max = simd::fmax(a, b);
+
+ outputs[COMPARATOR_OUTPUT].setVoltageSimd(comp, c);
+ outputs[MIN_OUTPUT].setVoltageSimd(out_min, c);
+ outputs[MAX_OUTPUT].setVoltageSimd(out_max, c);
+ }
// Lights
- lights[COMPARATOR_LIGHT].setSmoothBrightness(outputs[COMPARATOR_OUTPUT].value / 10.0, args.sampleTime);
- lights[MIN_LIGHT].setSmoothBrightness(outputs[MIN_OUTPUT].value / 10.0, args.sampleTime);
- lights[MAX_LIGHT].setSmoothBrightness(outputs[MAX_OUTPUT].value / 10.0, args.sampleTime);
+ if (channels_max == 1) {
+ lights[COMPARATOR_LIGHT ].setSmoothBrightness(outputs[COMPARATOR_OUTPUT].getVoltage(), args.sampleTime);
+ lights[COMPARATOR_LIGHT + 1].setBrightness(0.0f);
+ lights[COMPARATOR_LIGHT + 2].setBrightness(0.0f);
+ lights[MIN_LIGHT ].setSmoothBrightness(outputs[MIN_OUTPUT].getVoltage(), args.sampleTime);
+ lights[MIN_LIGHT + 1].setBrightness(0.0f);
+ lights[MIN_LIGHT + 2].setBrightness(0.0f);
+ lights[MAX_LIGHT ].setSmoothBrightness(outputs[MAX_OUTPUT].getVoltage(), args.sampleTime);
+ lights[MAX_LIGHT + 1].setBrightness(0.0f);
+ lights[MAX_LIGHT + 2].setBrightness(0.0f);
+ }
+ else {
+ lights[COMPARATOR_LIGHT ].setBrightness(0.0f);
+ lights[COMPARATOR_LIGHT + 1].setBrightness(0.0f);
+ lights[COMPARATOR_LIGHT + 2].setBrightness(10.0f);
+ lights[MIN_LIGHT ].setBrightness(0.0f);
+ lights[MIN_LIGHT + 1].setBrightness(0.0f);
+ lights[MIN_LIGHT + 2].setBrightness(10.0f);
+ lights[MAX_LIGHT ].setBrightness(0.0f);
+ lights[MAX_LIGHT + 1].setBrightness(0.0f);
+ lights[MAX_LIGHT + 2].setBrightness(10.0f);
+ }
}
};
-
-
struct RampageWidget : ModuleWidget {
- RampageWidget(Rampage *module) {
+ RampageWidget(Rampage* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Rampage.svg")));
addChild(createWidget(Vec(15, 0)));
- addChild(createWidget(Vec(box.size.x-30, 0)));
+ addChild(createWidget(Vec(box.size.x - 30, 0)));
addChild(createWidget(Vec(15, 365)));
- addChild(createWidget