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/) - -![EvenVCO](https://library.vcvrack.com/screenshots/Befaco/EvenVCO.m.png) - -### Rampage - -Based on [Rampage](http://www.befaco.org/rampage-2/), [Manual PDF](https://befaco.org/docs/Rampage/Rampage_User_Manual.pdf) - -![Rampage](https://library.vcvrack.com/screenshots/Befaco/Rampage.m.png) - -### 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) - -![A\*B+C](https://library.vcvrack.com/screenshots/Befaco/ABC.m.png) - -### Spring Reverb - -Based on [Spring Reverb](http://www.befaco.org/spring-reverb/) - -![Spring Reverb](https://library.vcvrack.com/screenshots/Befaco/SpringReverb.m.png) - -### Mixer - -Based on [Mixer](http://www.befaco.org/mixer-2/) - -![Mixer](https://library.vcvrack.com/screenshots/Befaco/Mixer.m.png) - -### Slew Limiter - -Based on [Slew Limiter](http://www.befaco.org/slew-limiter/) - -![Slew Limiter](https://library.vcvrack.com/screenshots/Befaco/SlewLimiter.m.png) - - -### Dual Atenuverter - -Based on [Dual Atenuverter](http://www.befaco.org/dual-atenuverter/) - -![Dual Atenuverter](https://library.vcvrack.com/screenshots/Befaco/DualAtenuverter.m.png) +[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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + 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 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + 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 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + +   +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lienzo 1 + + Capa 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + 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 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OUTs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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(Vec(box.size.x-30, 365))); + addChild(createWidget(Vec(box.size.x - 30, 365))); addParam(createParam(Vec(94, 32), module, Rampage::RANGE_A_PARAM)); addParam(createParam(Vec(27, 90), module, Rampage::SHAPE_A_PARAM)); @@ -207,42 +339,42 @@ struct RampageWidget : ModuleWidget { addParam(createParam(Vec(141, 238), module, Rampage::CYCLE_B_PARAM)); addParam(createParam(Vec(117, 76), module, Rampage::BALANCE_PARAM)); - addInput(createInput(Vec(14, 30), module, Rampage::IN_A_INPUT)); - addInput(createInput(Vec(52, 37), module, Rampage::TRIGG_A_INPUT)); - addInput(createInput(Vec(8, 268), module, Rampage::RISE_CV_A_INPUT)); - addInput(createInput(Vec(67, 268), module, Rampage::FALL_CV_A_INPUT)); - addInput(createInput(Vec(38, 297), module, Rampage::EXP_CV_A_INPUT)); - addInput(createInput(Vec(102, 290), module, Rampage::CYCLE_A_INPUT)); - addInput(createInput(Vec(229, 30), module, Rampage::IN_B_INPUT)); - addInput(createInput(Vec(192, 37), module, Rampage::TRIGG_B_INPUT)); - addInput(createInput(Vec(176, 268), module, Rampage::RISE_CV_B_INPUT)); - addInput(createInput(Vec(237, 268), module, Rampage::FALL_CV_B_INPUT)); - addInput(createInput(Vec(207, 297), module, Rampage::EXP_CV_B_INPUT)); - addInput(createInput(Vec(143, 290), module, Rampage::CYCLE_B_INPUT)); - - addOutput(createOutput(Vec(8, 326), module, Rampage::RISING_A_OUTPUT)); - addOutput(createOutput(Vec(68, 326), module, Rampage::FALLING_A_OUTPUT)); - addOutput(createOutput(Vec(104, 326), module, Rampage::EOC_A_OUTPUT)); - addOutput(createOutput(Vec(102, 195), module, Rampage::OUT_A_OUTPUT)); - addOutput(createOutput(Vec(177, 326), module, Rampage::RISING_B_OUTPUT)); - addOutput(createOutput(Vec(237, 326), module, Rampage::FALLING_B_OUTPUT)); - addOutput(createOutput(Vec(140, 326), module, Rampage::EOC_B_OUTPUT)); - addOutput(createOutput(Vec(142, 195), module, Rampage::OUT_B_OUTPUT)); - addOutput(createOutput(Vec(122, 133), module, Rampage::COMPARATOR_OUTPUT)); - addOutput(createOutput(Vec(89, 157), module, Rampage::MIN_OUTPUT)); - addOutput(createOutput(Vec(155, 157), module, Rampage::MAX_OUTPUT)); - - addChild(createLight>(Vec(132, 167), module, Rampage::COMPARATOR_LIGHT)); - addChild(createLight>(Vec(123, 174), module, Rampage::MIN_LIGHT)); - addChild(createLight>(Vec(141, 174), module, Rampage::MAX_LIGHT)); - addChild(createLight>(Vec(126, 185), module, Rampage::OUT_A_LIGHT)); - addChild(createLight>(Vec(138, 185), module, Rampage::OUT_B_LIGHT)); - addChild(createLight>(Vec(18, 312), module, Rampage::RISING_A_LIGHT)); - addChild(createLight>(Vec(78, 312), module, Rampage::FALLING_A_LIGHT)); - addChild(createLight>(Vec(187, 312), module, Rampage::RISING_B_LIGHT)); - addChild(createLight>(Vec(247, 312), module, Rampage::FALLING_B_LIGHT)); + addInput(createInput(Vec(14, 30), module, Rampage::IN_A_INPUT)); + addInput(createInput(Vec(52, 37), module, Rampage::TRIGG_A_INPUT)); + addInput(createInput(Vec(8, 268), module, Rampage::RISE_CV_A_INPUT)); + addInput(createInput(Vec(67, 268), module, Rampage::FALL_CV_A_INPUT)); + addInput(createInput(Vec(38, 297), module, Rampage::EXP_CV_A_INPUT)); + addInput(createInput(Vec(102, 290), module, Rampage::CYCLE_A_INPUT)); + addInput(createInput(Vec(229, 30), module, Rampage::IN_B_INPUT)); + addInput(createInput(Vec(192, 37), module, Rampage::TRIGG_B_INPUT)); + addInput(createInput(Vec(176, 268), module, Rampage::RISE_CV_B_INPUT)); + addInput(createInput(Vec(237, 268), module, Rampage::FALL_CV_B_INPUT)); + addInput(createInput(Vec(207, 297), module, Rampage::EXP_CV_B_INPUT)); + addInput(createInput(Vec(143, 290), module, Rampage::CYCLE_B_INPUT)); + + addOutput(createOutput(Vec(8, 326), module, Rampage::RISING_A_OUTPUT)); + addOutput(createOutput(Vec(68, 326), module, Rampage::FALLING_A_OUTPUT)); + addOutput(createOutput(Vec(104, 326), module, Rampage::EOC_A_OUTPUT)); + addOutput(createOutput(Vec(102, 195), module, Rampage::OUT_A_OUTPUT)); + addOutput(createOutput(Vec(177, 326), module, Rampage::RISING_B_OUTPUT)); + addOutput(createOutput(Vec(237, 326), module, Rampage::FALLING_B_OUTPUT)); + addOutput(createOutput(Vec(140, 326), module, Rampage::EOC_B_OUTPUT)); + addOutput(createOutput(Vec(142, 195), module, Rampage::OUT_B_OUTPUT)); + addOutput(createOutput(Vec(122, 133), module, Rampage::COMPARATOR_OUTPUT)); + addOutput(createOutput(Vec(89, 157), module, Rampage::MIN_OUTPUT)); + addOutput(createOutput(Vec(155, 157), module, Rampage::MAX_OUTPUT)); + + addChild(createLight>(Vec(132, 167), module, Rampage::COMPARATOR_LIGHT)); + addChild(createLight>(Vec(123, 174), module, Rampage::MIN_LIGHT)); + addChild(createLight>(Vec(141, 174), module, Rampage::MAX_LIGHT)); + addChild(createLight>(Vec(126, 185), module, Rampage::OUT_A_LIGHT)); + addChild(createLight>(Vec(138, 185), module, Rampage::OUT_B_LIGHT)); + addChild(createLight>(Vec(18, 312), module, Rampage::RISING_A_LIGHT)); + addChild(createLight>(Vec(78, 312), module, Rampage::FALLING_A_LIGHT)); + addChild(createLight>(Vec(187, 312), module, Rampage::RISING_B_LIGHT)); + addChild(createLight>(Vec(247, 312), module, Rampage::FALLING_B_LIGHT)); } }; -Model *modelRampage = createModel("Rampage"); +Model* modelRampage = createModel("Rampage"); diff --git a/src/SlewLimiter.cpp b/src/SlewLimiter.cpp index 8e1d159..ba6b718 100644 --- a/src/SlewLimiter.cpp +++ b/src/SlewLimiter.cpp @@ -1,5 +1,6 @@ #include "plugin.hpp" +using simd::float_4; struct SlewLimiter : Module { enum ParamIds { @@ -19,7 +20,7 @@ struct SlewLimiter : Module { NUM_OUTPUTS }; - float out = 0.0; + float_4 out[4] = {}; SlewLimiter() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS); @@ -28,40 +29,63 @@ struct SlewLimiter : Module { configParam(FALL_PARAM, 0.0, 1.0, 0.0, "Fall time"); } - void process(const ProcessArgs &args) override { - float in = inputs[IN_INPUT].getVoltage(); - float shape = params[SHAPE_PARAM].getValue(); + void process(const ProcessArgs& args) override { - // minimum and maximum slopes in volts per second + float_4 in[4] = {}; + float_4 riseCV[4] = {}; + float_4 fallCV[4] = {}; + + // this is the number of active polyphony engines, defined by the input + int numPolyphonyEngines = inputs[IN_INPUT].getChannels(); + + // minimum and std::maximum slopes in volts per second const float slewMin = 0.1; const float slewMax = 10000.f; // Amount of extra slew per voltage difference - const float shapeScale = 1/10.f; - - // Rise - if (in > out) { - float rise = inputs[RISE_INPUT].getVoltage() / 10.f + params[RISE_PARAM].getValue(); - float slew = slewMax * std::pow(slewMin / slewMax, rise); - out += slew * crossfade(1.f, shapeScale * (in - out), shape) * args.sampleTime; - if (out > in) - out = in; - } - // Fall - else if (in < out) { - float fall = inputs[FALL_INPUT].getVoltage() / 10.f + params[FALL_PARAM].getValue(); - float slew = slewMax * std::pow(slewMin / slewMax, fall); - out -= slew * crossfade(1.f, shapeScale * (out - in), shape) * args.sampleTime; - if (out < in) - out = in; - } + const float shapeScale = 1 / 10.f; + + const float_4 param_rise = params[RISE_PARAM].getValue() * 10.f; + const float_4 param_fall = params[FALL_PARAM].getValue() * 10.f; + + outputs[OUT_OUTPUT].setChannels(numPolyphonyEngines); + + for (int c = 0; c < numPolyphonyEngines; c += 4) { + in[c / 4] = inputs[IN_INPUT].getVoltageSimd(c); - outputs[OUT_OUTPUT].setVoltage(out); + if (inputs[RISE_INPUT].isConnected()) { + riseCV[c / 4] = inputs[RISE_INPUT].getPolyVoltageSimd(c); + } + if (inputs[FALL_INPUT].isConnected()) { + fallCV[c / 4] = inputs[FALL_INPUT].getPolyVoltageSimd(c); + } + + riseCV[c / 4] += param_rise; + fallCV[c / 4] += param_fall; + + float_4 delta = in[c / 4] - out[c / 4]; + float_4 delta_gt_0 = delta > 0.f; + float_4 delta_lt_0 = delta < 0.f; + + float_4 rateCV = {}; + rateCV = ifelse(delta_gt_0, riseCV[c / 4], 0.f); + rateCV = ifelse(delta_lt_0, fallCV[c / 4], rateCV) * 0.1f; + + float_4 pm_one = simd::sgn(delta); + float_4 slew = slewMax * simd::pow(slewMin / slewMax, rateCV); + + const float shape = params[SHAPE_PARAM].getValue(); + out[c / 4] += slew * simd::crossfade(pm_one, shapeScale * delta, shape) * args.sampleTime; + out[c / 4] = ifelse(delta_gt_0 & (out[c / 4] > in[c / 4]), in[c / 4], out[c / 4]); + out[c / 4] = ifelse(delta_lt_0 & (out[c / 4] < in[c / 4]), in[c / 4], out[c / 4]); + + outputs[OUT_OUTPUT].setVoltageSimd(out[c / 4], c); + } } }; struct SlewLimiterWidget : ModuleWidget { - SlewLimiterWidget(::SlewLimiter *module) { + SlewLimiterWidget(SlewLimiter* module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/SlewLimiter.svg"))); @@ -73,13 +97,13 @@ struct SlewLimiterWidget : ModuleWidget { addParam(createParam(Vec(15, 102), module, ::SlewLimiter::RISE_PARAM)); addParam(createParam(Vec(60, 102), module, ::SlewLimiter::FALL_PARAM)); - addInput(createInput(Vec(10, 273), module, ::SlewLimiter::RISE_INPUT)); - addInput(createInput(Vec(55, 273), module, ::SlewLimiter::FALL_INPUT)); + addInput(createInput(Vec(10, 273), module, ::SlewLimiter::RISE_INPUT)); + addInput(createInput(Vec(55, 273), module, ::SlewLimiter::FALL_INPUT)); - addInput(createInput(Vec(10, 323), module, ::SlewLimiter::IN_INPUT)); - addOutput(createOutput(Vec(55, 323), module, ::SlewLimiter::OUT_OUTPUT)); + addInput(createInput(Vec(10, 323), module, ::SlewLimiter::IN_INPUT)); + addOutput(createOutput(Vec(55, 323), module, ::SlewLimiter::OUT_OUTPUT)); } }; -Model *modelSlewLimiter = createModel<::SlewLimiter, SlewLimiterWidget>("SlewLimiter"); +Model* modelSlewLimiter = createModel<::SlewLimiter, SlewLimiterWidget>("SlewLimiter"); diff --git a/src/SpringReverb.cpp b/src/SpringReverb.cpp index 27ebe1d..8058ac8 100644 --- a/src/SpringReverb.cpp +++ b/src/SpringReverb.cpp @@ -1,7 +1,5 @@ -#include #include "plugin.hpp" -#include "pffft.h" - +#include BINARY(src_SpringReverbIR_pcm); @@ -32,19 +30,23 @@ struct SpringReverb : Module { }; enum LightIds { PEAK_LIGHT, - VU1_LIGHT, - NUM_LIGHTS = VU1_LIGHT + 7 + ENUMS(VU1_LIGHTS, 7), + NUM_LIGHTS }; - dsp::RealTimeConvolver *convolver = NULL; + dsp::RealTimeConvolver* convolver = NULL; dsp::SampleRateConverter<1> inputSrc; dsp::SampleRateConverter<1> outputSrc; - dsp::DoubleRingBuffer, 16*BLOCK_SIZE> inputBuffer; - dsp::DoubleRingBuffer, 16*BLOCK_SIZE> outputBuffer; + dsp::DoubleRingBuffer, 16 * BLOCK_SIZE> inputBuffer; + dsp::DoubleRingBuffer, 16 * BLOCK_SIZE> outputBuffer; dsp::RCFilter dryFilter; - dsp::PeakFilter vuFilter; - dsp::PeakFilter lightFilter; + + dsp::VuMeter2 vuFilter; + dsp::VuMeter2 lightFilter; + dsp::ClockDivider lightRefreshClock; + + const float brightnessIntervals[8] = {17.f, 14.f, 12.f, 9.f, 6.f, 0.f, -6.f, -12.f}; SpringReverb() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); @@ -55,18 +57,23 @@ struct SpringReverb : Module { convolver = new dsp::RealTimeConvolver(BLOCK_SIZE); - const float *kernel = (const float*) BINARY_START(src_SpringReverbIR_pcm); + const float* kernel = (const float*) BINARY_START(src_SpringReverbIR_pcm); size_t kernelLen = BINARY_SIZE(src_SpringReverbIR_pcm) / sizeof(float); convolver->setKernel(kernel, kernelLen); + + vuFilter.mode = dsp::VuMeter2::PEAK; + lightFilter.mode = dsp::VuMeter2::PEAK; + + lightRefreshClock.setDivision(32); } ~SpringReverb() { delete convolver; } - void process(const ProcessArgs &args) override { - float in1 = inputs[IN1_INPUT].getVoltage(); - float in2 = inputs[IN2_INPUT].getVoltage(); + void process(const ProcessArgs& args) override { + float in1 = inputs[IN1_INPUT].getVoltageSum(); + float in2 = inputs[IN2_INPUT].getVoltageSum(); const float levelScale = 0.030; const float levelBase = 25.0; float level1 = levelScale * dsp::exponentialBipolar(levelBase, params[LEVEL1_PARAM].getValue()) * inputs[CV1_INPUT].getNormalVoltage(10.0) / 10.0; @@ -114,6 +121,7 @@ struct SpringReverb : Module { // Set output if (outputBuffer.empty()) return; + float wet = outputBuffer.shift().samples[0]; float balance = clamp(params[WET_PARAM].getValue() + inputs[MIX_CV_INPUT].getVoltage() / 10.0f, 0.0f, 1.0f); float mix = crossfade(in1, wet, balance); @@ -121,32 +129,33 @@ struct SpringReverb : Module { outputs[WET_OUTPUT].setVoltage(clamp(wet, -10.0f, 10.0f)); outputs[MIX_OUTPUT].setVoltage(clamp(mix, -10.0f, 10.0f)); - // Set lights - float lightRate = 5.0 * args.sampleTime; - vuFilter.setRate(lightRate); - vuFilter.process(std::fabs(wet)); - lightFilter.setRate(lightRate); - lightFilter.process(std::fabs(dry*50.0)); - - float vuValue = vuFilter.peak(); - for (int i = 0; i < 7; i++) { - float light = std::pow(1.413, i) * vuValue / 10.0 - 1.0; - lights[VU1_LIGHT + i].value = clamp(light, 0.0f, 1.0f); + // process VU lights + vuFilter.process(args.sampleTime, wet); + // process peak light + lightFilter.process(args.sampleTime, dry * 50.0); + + if (lightRefreshClock.process()) { + + for (int i = 0; i < 7; i++) { + float brightness = vuFilter.getBrightness(brightnessIntervals[i + 1], brightnessIntervals[i]); + lights[VU1_LIGHTS + i].setBrightness(brightness); + } + + lights[PEAK_LIGHT].value = lightFilter.v; } - lights[PEAK_LIGHT].value = lightFilter.peak(); } }; struct SpringReverbWidget : ModuleWidget { - SpringReverbWidget(SpringReverb *module) { + SpringReverbWidget(SpringReverb* module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/SpringReverb.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, 29), module, SpringReverb::WET_PARAM)); @@ -155,25 +164,25 @@ struct SpringReverbWidget : ModuleWidget { addParam(createParam(Vec(42, 210), module, SpringReverb::HPF_PARAM)); - addInput(createInput(Vec(7, 243), module, SpringReverb::CV1_INPUT)); - addInput(createInput(Vec(88, 243), module, SpringReverb::CV2_INPUT)); - addInput(createInput(Vec(27, 281), module, SpringReverb::IN1_INPUT)); - addInput(createInput(Vec(67, 281), module, SpringReverb::IN2_INPUT)); + addInput(createInput(Vec(7, 243), module, SpringReverb::CV1_INPUT)); + addInput(createInput(Vec(88, 243), module, SpringReverb::CV2_INPUT)); + addInput(createInput(Vec(27, 281), module, SpringReverb::IN1_INPUT)); + addInput(createInput(Vec(67, 281), module, SpringReverb::IN2_INPUT)); - addOutput(createOutput(Vec(7, 317), module, SpringReverb::MIX_OUTPUT)); - addInput(createInput(Vec(47, 324), module, SpringReverb::MIX_CV_INPUT)); - addOutput(createOutput(Vec(88, 317), module, SpringReverb::WET_OUTPUT)); + addOutput(createOutput(Vec(7, 317), module, SpringReverb::MIX_OUTPUT)); + addInput(createInput(Vec(47, 324), module, SpringReverb::MIX_CV_INPUT)); + addOutput(createOutput(Vec(88, 317), module, SpringReverb::WET_OUTPUT)); addChild(createLight>(Vec(55, 269), module, SpringReverb::PEAK_LIGHT)); - addChild(createLight>(Vec(55, 113), module, SpringReverb::VU1_LIGHT + 0)); - addChild(createLight>(Vec(55, 126), module, SpringReverb::VU1_LIGHT + 1)); - addChild(createLight>(Vec(55, 138), module, SpringReverb::VU1_LIGHT + 2)); - addChild(createLight>(Vec(55, 150), module, SpringReverb::VU1_LIGHT + 3)); - addChild(createLight>(Vec(55, 163), module, SpringReverb::VU1_LIGHT + 4)); - addChild(createLight>(Vec(55, 175), module, SpringReverb::VU1_LIGHT + 5)); - addChild(createLight>(Vec(55, 188), module, SpringReverb::VU1_LIGHT + 6)); + addChild(createLight>(Vec(55, 113), module, SpringReverb::VU1_LIGHTS + 0)); + addChild(createLight>(Vec(55, 126), module, SpringReverb::VU1_LIGHTS + 1)); + addChild(createLight>(Vec(55, 138), module, SpringReverb::VU1_LIGHTS + 2)); + addChild(createLight>(Vec(55, 150), module, SpringReverb::VU1_LIGHTS + 3)); + addChild(createLight>(Vec(55, 163), module, SpringReverb::VU1_LIGHTS + 4)); + addChild(createLight>(Vec(55, 175), module, SpringReverb::VU1_LIGHTS + 5)); + addChild(createLight>(Vec(55, 188), module, SpringReverb::VU1_LIGHTS + 6)); } }; -Model *modelSpringReverb = createModel("SpringReverb"); +Model* modelSpringReverb = createModel("SpringReverb"); diff --git a/src/plugin.cpp b/src/plugin.cpp index e9d2840..ec6cdcf 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -13,4 +13,8 @@ void init(rack::Plugin *p) { p->addModel(modelMixer); p->addModel(modelSlewLimiter); p->addModel(modelDualAtenuverter); + p->addModel(modelPercall); + p->addModel(modelHexmixVCA); + p->addModel(modelChoppingKinky); + p->addModel(modelKickall); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 05674b8..a6b8d71 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -1,4 +1,4 @@ -#include "rack.hpp" +#include using namespace rack; @@ -13,12 +13,122 @@ extern Model *modelSpringReverb; extern Model *modelMixer; extern Model *modelSlewLimiter; extern Model *modelDualAtenuverter; +extern Model *modelPercall; +extern Model *modelHexmixVCA; +extern Model *modelChoppingKinky; +extern Model *modelKickall; -struct Knurlie : SVGScrew { +struct Knurlie : SvgScrew { Knurlie() { sw->svg = APP->window->loadSvg(asset::plugin(pluginInstance, "res/Knurlie.svg")); sw->wrap(); box.size = sw->box.size; } }; + +struct BefacoTinyKnobRed : app::SvgKnob { + BefacoTinyKnobRed() { + minAngle = -0.8 * M_PI; + maxAngle = 0.8 * M_PI; + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoTinyKnobRed.svg"))); + } +}; + +struct BefacoTinyKnobWhite : app::SvgKnob { + BefacoTinyKnobWhite() { + minAngle = -0.8 * M_PI; + maxAngle = 0.8 * M_PI; + setSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/BefacoTinyKnob.svg"))); + } +}; + +struct BefacoTinyKnobGrey : app::SvgKnob { + BefacoTinyKnobGrey() { + minAngle = -0.8 * M_PI; + maxAngle = 0.8 * M_PI; + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoTinyKnobGrey.svg"))); + } +}; + +struct Davies1900hLargeGreyKnob : Davies1900hKnob { + Davies1900hLargeGreyKnob() { + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Davies1900hLargeGrey.svg"))); + } +}; + +struct BefacoOutputPort : app::SvgPort { + BefacoOutputPort() { + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoOutputPort.svg"))); + } +}; + +struct BefacoInputPort : app::SvgPort { + BefacoInputPort() { + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoInputPort.svg"))); + } +}; + +template +T sin2pi_pade_05_5_4(T x) { + x -= 0.5f; + return (T(-6.283185307) * x + T(33.19863968) * simd::pow(x, 3) - T(32.44191367) * simd::pow(x, 5)) + / (1 + T(1.296008659) * simd::pow(x, 2) + T(0.7028072946) * simd::pow(x, 4)); +} + +template +T tanh_pade(T x) { + T x2 = x * x; + T q = 12.f + x2; + return 12.f * x * q / (36.f * x2 + q * q); +} + + +struct ADEnvelope { + enum Stage { + STAGE_OFF, + STAGE_ATTACK, + STAGE_DECAY + }; + + Stage stage = STAGE_OFF; + float env = 0.f; + float attackTime = 0.1, decayTime = 0.1; + float attackShape = 1.0, decayShape = 1.0; + + ADEnvelope() { }; + + void process(const float& sampleTime) { + + if (stage == STAGE_OFF) { + env = envLinear = 0.0f; + } + else if (stage == STAGE_ATTACK) { + envLinear += sampleTime / attackTime; + env = std::pow(envLinear, attackShape); + } + else if (stage == STAGE_DECAY) { + envLinear -= sampleTime / decayTime; + env = std::pow(envLinear, decayShape); + } + + if (envLinear >= 1.0f) { + stage = STAGE_DECAY; + env = envLinear = 1.0f; + } + else if (envLinear <= 0.0f) { + stage = STAGE_OFF; + env = envLinear = 0.0f; + } + } + + void trigger() { + stage = ADEnvelope::STAGE_ATTACK; + // non-linear envelopes won't retrigger at the correct starting point if + // attackShape != decayShape, so we advance the linear envelope + envLinear = std::pow(env, 1.0f / attackShape); + } + +private: + float envLinear = 0.f; +};