diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d07243..732bb17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Change Log
+## v2.8.0
+ * Molten Bypass
+ * Initial release
+ * EvenVCO
+ * Complete re-write for better FM performance
+ * Hard sync added
+ * Octaves
+ * Avoid allocation in the audio thread (thanks @danngreen)
+ * Noise Plethora
+ * Fix labels
+ * Avoid std::string allocations on audio thread (thanks @danngreen)
+
## v2.7.1
* Midi Thing 2
* Remove -10 to 0 V configuration
diff --git a/Makefile b/Makefile
index 5eb2b7a..7979d68 100644
--- a/Makefile
+++ b/Makefile
@@ -6,3 +6,5 @@ SOURCES += $(wildcard src/noise-plethora/*/*.cpp)
DISTRIBUTABLES += $(wildcard LICENSE*) res
include $(RACK_DIR)/plugin.mk
+
+CXXFLAGS += -std=c++17
\ No newline at end of file
diff --git a/docs/Oneiroi.md b/docs/Oneiroi.md
new file mode 100644
index 0000000..999ef51
--- /dev/null
+++ b/docs/Oneiroi.md
@@ -0,0 +1,16 @@
+# Befaco Oneiroi
+
+Based on [Befaco Oneiroi](http://www.befaco.org/oneiroi) Eurorack module. For the official manual, see [here](https://befaco.org/docs/Oneiroi/Oneiroi_User_Manual.pdf).
+
+
+## Differences with hardware
+
+* Randomisation can optionally be applied to every parameter using the built in VCV randomisation
+* Input gain switch (available on hardware) has been removed as this makes no sense in VCV
+* Undo/redo is natively handled by VCV Rack
+* .wav files can be loaded from the context menu (naive loading no sample rate conversion!)
+* Additional LED indicators have been added for filter type, filter position, modulation type and oscillator octave
+* Distinct virtual knobs are used for each parameter so parameter catch-up (used on hardware) is not needed.
+* As yet, slew of parameter values on randomize is not supported
+
+
diff --git a/docs/img/Oneiroi.png b/docs/img/Oneiroi.png
new file mode 100644
index 0000000..204cb9c
Binary files /dev/null and b/docs/img/Oneiroi.png differ
diff --git a/plugin.json b/plugin.json
index c7b430c..7fafb0b 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,6 +1,6 @@
{
"slug": "Befaco",
- "version": "2.7.1",
+ "version": "2.8.0",
"license": "GPL-3.0-or-later",
"name": "Befaco",
"brand": "Befaco",
@@ -314,7 +314,7 @@
"description": "An accurate voltage source and precision adder.",
"manualUrl": "https://www.befaco.org/voltio/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-voltio",
- "tags": [
+ "tags": [
"Hardware clone",
"Polyphonic",
"Utility"
@@ -331,6 +331,32 @@
"Oscillator",
"Polyphonic"
]
+ },
+ {
+ "slug": "Bypass",
+ "name": "Bypass",
+ "description": "A Stereo bypass module to gate control the send of your signals to your favorite effect!",
+ "manualUrl": "https://www.befaco.org/molten-bypass/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-molten-bypass",
+ "tags": [
+ "Hardware clone",
+ "Mixer",
+ "Polyphonic",
+ "Utility"
+ ]
+ },
+ {
+ "slug": "Bandit",
+ "name": "Bandit",
+ "description": "A spectral processing playground.",
+ "tags": [
+ "Equalizer",
+ "Filter",
+ "Hardware clone",
+ "Mixer",
+ "Polyphonic",
+ "Utility"
+ ]
}
]
-}
+}
\ No newline at end of file
diff --git a/res/components/VCVBezelBig.svg b/res/components/VCVBezelBig.svg
new file mode 100644
index 0000000..e74ade8
--- /dev/null
+++ b/res/components/VCVBezelBig.svg
@@ -0,0 +1,150 @@
+
+
+
+
diff --git a/res/panels/Bandit.svg b/res/panels/Bandit.svg
new file mode 100644
index 0000000..c93ed8c
--- /dev/null
+++ b/res/panels/Bandit.svg
@@ -0,0 +1,1739 @@
+
+
diff --git a/res/panels/Bypass.svg b/res/panels/Bypass.svg
new file mode 100644
index 0000000..49dc25b
--- /dev/null
+++ b/res/panels/Bypass.svg
@@ -0,0 +1,866 @@
+
+
diff --git a/src/Bandit.cpp b/src/Bandit.cpp
new file mode 100644
index 0000000..501c633
--- /dev/null
+++ b/src/Bandit.cpp
@@ -0,0 +1,322 @@
+#include "plugin.hpp"
+
+using namespace simd;
+
+struct Bandit : Module {
+ enum ParamId {
+ LOW_GAIN_PARAM,
+ LOW_MID_GAIN_PARAM,
+ HIGH_MID_GAIN_PARAM,
+ HIGH_GAIN_PARAM,
+ PARAMS_LEN
+ };
+ enum InputId {
+ LOW_INPUT,
+ LOW_MID_INPUT,
+ HIGH_MID_INPUT,
+ HIGH_INPUT,
+ LOW_RETURN_INPUT,
+ LOW_MID_RETURN_INPUT,
+ HIGH_MID_RETURN_INPUT,
+ HIGH_RETURN_INPUT,
+ LOW_CV_INPUT,
+ LOW_MID_CV_INPUT,
+ HIGH_MID_CV_INPUT,
+ HIGH_CV_INPUT,
+ ALL_INPUT,
+ ALL_CV_INPUT,
+ INPUTS_LEN
+ };
+ enum OutputId {
+ LOW_OUTPUT,
+ LOW_MID_OUTPUT,
+ HIGH_MID_OUTPUT,
+ HIGH_OUTPUT,
+ MIX_OUTPUT,
+ OUTPUTS_LEN
+ };
+ enum LightId {
+ ENUMS(MIX_CLIP_LIGHT, 3),
+ ENUMS(MIX_LIGHT, 3),
+ LIGHTS_LEN
+ };
+
+ // float_4 * [4] give 16 polyphony channels, [2] is for cascading biquads
+ dsp::TBiquadFilter filterLow[4][2], filterLowMid[4][2], filterHighMid[4][2], filterHigh[4][2];
+ float clipTimer = 0.f;
+ const float clipTime = 0.25f;
+ dsp::ClockDivider ledUpdateClock;
+ const int ledUpdateRate = 64;
+ bool applySaturation = true;
+
+ Bandit() {
+ config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
+ auto lowGainParam = configParam(LOW_GAIN_PARAM, 0.f, 1.f, 0.75f, "Low gain");
+ lowGainParam->description = "Lowpass <300 Hz";
+ auto lowMidGainParam = configParam(LOW_MID_GAIN_PARAM, 0.f, 1.f, 0.75f, "Low mid gain");
+ lowMidGainParam->description = "Bandpass ~750 Hz";
+ auto highMidGainParam = configParam(HIGH_MID_GAIN_PARAM, 0.f, 1.f, 0.75f, "High mid gain");
+ highMidGainParam->description = "Bandpass ~1.5 kHz";
+ auto highGainParam = configParam(HIGH_GAIN_PARAM, 0.f, 1.f, 0.75f, "High gain");
+ highGainParam->description = "Highpass >3 kHz";
+
+ // band inputs
+ configInput(LOW_INPUT, "Low");
+ configInput(LOW_MID_INPUT, "Low mid");
+ configInput(HIGH_MID_INPUT, "High mid");
+ configInput(HIGH_INPUT, "High");
+
+ // band send outputs
+ auto outLowSend = configOutput(LOW_OUTPUT, "Low");
+ outLowSend->description = "Normalled to Low band return";
+ auto outLowMidSend = configOutput(LOW_MID_OUTPUT, "Low mid");
+ outLowMidSend->description = "Normalled to Low Mid band return";
+ auto outHighMidSend = configOutput(HIGH_MID_OUTPUT, "High mid");
+ outHighMidSend->description = "Normalled to High Mid band return";
+ auto outHighSend = configOutput(HIGH_OUTPUT, "High");
+ outHighSend->description = "Normalled to High band return";
+
+ // band return inputs
+ configInput(LOW_RETURN_INPUT, "Low return");
+ configInput(LOW_MID_RETURN_INPUT, "Low mid return");
+ configInput(HIGH_MID_RETURN_INPUT, "High mid return");
+ configInput(HIGH_RETURN_INPUT, "High return");
+
+ // band gain CVs
+ configInput(LOW_CV_INPUT, "Low CV");
+ configInput(LOW_MID_CV_INPUT, "Low mid CV");
+ configInput(HIGH_MID_CV_INPUT, "High mid CV");
+ configInput(HIGH_CV_INPUT, "High CV");
+ configInput(ALL_INPUT, "All");
+ auto allCvInput = configInput(ALL_CV_INPUT, "All CV");
+ allCvInput->description = "Mix VCA, 10V to fully open";
+
+ // mix out
+ configOutput(MIX_OUTPUT, "Mix");
+
+ ledUpdateClock.setDivision(ledUpdateRate);
+ }
+
+ void onSampleRateChange() override {
+ const float sr = APP->engine->getSampleRate();
+ const float lowFc = 300.f / sr;
+ const float lowMidFc = 750.f / sr;
+ const float highMidFc = 1500.f / sr;
+ const float highFc = 3800.f / sr;
+ // Qs for cascaded biquads to get Butterworth response, see https://www.earlevel.com/main/2016/09/29/cascading-filters/
+ // technically only for LOWPASS and HIGHPASS, but seems to work well for BANDPASS too
+ const float Q[2] = {0.54119610f, 1.3065630f};
+ const float V = 1.f;
+
+ for (int i = 0; i < 4; ++i) {
+ for (int stage = 0; stage < 2; ++stage) {
+ filterLow[i][stage].setParameters(dsp::TBiquadFilter::Type::LOWPASS, lowFc, Q[stage], V);
+ filterLowMid[i][stage].setParameters(dsp::TBiquadFilter::Type::BANDPASS, lowMidFc, Q[stage], V);
+ filterHighMid[i][stage].setParameters(dsp::TBiquadFilter::Type::BANDPASS, highMidFc, Q[stage], V);
+ filterHigh[i][stage].setParameters(dsp::TBiquadFilter::Type::HIGHPASS, highFc, Q[stage], V);
+ }
+ }
+ }
+
+ void processBypass(const ProcessArgs& args) override {
+ const int maxPolyphony = std::max({1, inputs[ALL_INPUT].getChannels(), inputs[LOW_INPUT].getChannels(),
+ inputs[LOW_MID_INPUT].getChannels(), inputs[HIGH_MID_INPUT].getChannels(),
+ inputs[HIGH_INPUT].getChannels()});
+
+
+ for (int c = 0; c < maxPolyphony; c += 4) {
+ const float_4 inLow = inputs[LOW_INPUT].getPolyVoltageSimd(c);
+ const float_4 inLowMid = inputs[LOW_MID_INPUT].getPolyVoltageSimd(c);
+ const float_4 inHighMid = inputs[HIGH_MID_INPUT].getPolyVoltageSimd(c);
+ const float_4 inHigh = inputs[HIGH_INPUT].getPolyVoltageSimd(c);
+ const float_4 inAll = inputs[ALL_INPUT].getPolyVoltageSimd(c);
+
+ // bypass sums all inputs to the output
+ outputs[MIX_OUTPUT].setVoltageSimd(inLow + inLowMid + inHighMid + inHigh + inAll, c);
+ }
+
+ outputs[MIX_OUTPUT].setChannels(maxPolyphony);
+ }
+
+
+ void process(const ProcessArgs& args) override {
+
+ const int maxPolyphony = std::max({1, inputs[ALL_INPUT].getChannels(), inputs[LOW_INPUT].getChannels(),
+ inputs[LOW_MID_INPUT].getChannels(), inputs[HIGH_MID_INPUT].getChannels(),
+ inputs[HIGH_INPUT].getChannels()});
+
+ const bool allReturnsActiveAndMonophonic = inputs[LOW_RETURN_INPUT].isMonophonic() && inputs[LOW_MID_RETURN_INPUT].isMonophonic() &&
+ inputs[HIGH_MID_RETURN_INPUT].isMonophonic() && inputs[HIGH_RETURN_INPUT].isMonophonic();
+
+ float_4 mixOutput[4] = {};
+ for (int c = 0; c < maxPolyphony; c += 4) {
+
+ const float_4 inLow = inputs[LOW_INPUT].getPolyVoltageSimd(c);
+ const float_4 inLowMid = inputs[LOW_MID_INPUT].getPolyVoltageSimd(c);
+ const float_4 inHighMid = inputs[HIGH_MID_INPUT].getPolyVoltageSimd(c);
+ const float_4 inHigh = inputs[HIGH_INPUT].getPolyVoltageSimd(c);
+ const float_4 inAll = inputs[ALL_INPUT].getPolyVoltageSimd(c);
+
+ const float_4 lowGain = params[LOW_GAIN_PARAM].getValue() * clamp(inputs[LOW_CV_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f);
+ const float_4 outLow = 0.7 * 2 * filterLow[c / 4][1].process(filterLow[c / 4][0].process((inLow + inAll) * lowGain));
+ outputs[LOW_OUTPUT].setVoltageSimd(outLow, c);
+
+ const float_4 lowMidGain = params[LOW_MID_GAIN_PARAM].getValue() * clamp(inputs[LOW_MID_CV_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f);
+ const float_4 outLowMid = 2 * filterLowMid[c / 4][1].process(filterLowMid[c / 4][0].process((inLowMid + inAll) * lowMidGain));
+ outputs[LOW_MID_OUTPUT].setVoltageSimd(outLowMid, c);
+
+ const float_4 highMidGain = params[HIGH_MID_GAIN_PARAM].getValue() * clamp(inputs[HIGH_MID_CV_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f);
+ const float_4 outHighMid = 2 * filterHighMid[c / 4][1].process(filterHighMid[c / 4][0].process((inHighMid + inAll) * highMidGain));
+ outputs[HIGH_MID_OUTPUT].setVoltageSimd(outHighMid, c);
+
+ const float_4 highGain = params[HIGH_GAIN_PARAM].getValue() * clamp(inputs[HIGH_CV_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f);
+ const float_4 outHigh = 0.7 * 2 * filterHigh[c / 4][1].process(filterHigh[c / 4][0].process((inHigh + inAll) * highGain));
+ outputs[HIGH_OUTPUT].setVoltageSimd(outHigh, c);
+
+ // the fx return input is normalled to the fx send output
+ mixOutput[c / 4] = inputs[LOW_RETURN_INPUT].getNormalPolyVoltageSimd(outLow * !outputs[LOW_OUTPUT].isConnected(), c);
+ mixOutput[c / 4] += inputs[LOW_MID_RETURN_INPUT].getNormalPolyVoltageSimd(outLowMid * !outputs[LOW_MID_OUTPUT].isConnected(), c);
+ mixOutput[c / 4] += inputs[HIGH_MID_RETURN_INPUT].getNormalPolyVoltageSimd(outHighMid * !outputs[HIGH_MID_OUTPUT].isConnected(), c);
+ mixOutput[c / 4] += inputs[HIGH_RETURN_INPUT].getNormalPolyVoltageSimd(outHigh * !outputs[HIGH_OUTPUT].isConnected(), c);
+ mixOutput[c / 4] = mixOutput[c / 4] * clamp(inputs[ALL_CV_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f);
+
+ if (applySaturation) {
+ mixOutput[c / 4] = Saturator::process(mixOutput[c / 4] / 10.f) * 10.f;
+ }
+
+ outputs[MIX_OUTPUT].setVoltageSimd(mixOutput[c / 4], c);
+ }
+
+ outputs[LOW_OUTPUT].setChannels(maxPolyphony);
+ outputs[LOW_MID_OUTPUT].setChannels(maxPolyphony);
+ outputs[HIGH_MID_OUTPUT].setChannels(maxPolyphony);
+ outputs[HIGH_OUTPUT].setChannels(maxPolyphony);
+
+ if (allReturnsActiveAndMonophonic) {
+ // special case: if all return paths are connected and monophonic, then output mix should be monophonic
+ outputs[MIX_OUTPUT].setChannels(1);
+ }
+ else {
+ // however, if it's a mix (some normalled from input, maybe some polyphonic), then it should be polyphonic
+ outputs[MIX_OUTPUT].setChannels(maxPolyphony);
+ }
+
+ if (ledUpdateClock.process()) {
+ processLEDs(mixOutput, args.sampleTime * ledUpdateRate);
+ }
+ }
+
+ void processLEDs(const float_4* output, const float sampleTime) {
+
+ const int maxPolyphony = outputs[MIX_OUTPUT].getChannels();
+
+ if (maxPolyphony == 1) {
+ const float rmsOut = std::fabs(output[0][0]);
+ lights[MIX_LIGHT + 0].setBrightness(0.f);
+ lights[MIX_LIGHT + 1].setBrightnessSmooth(rmsOut / 5.f, sampleTime);
+ lights[MIX_LIGHT + 2].setBrightness(0.f);
+
+ if (rmsOut > 10.f) {
+ clipTimer = clipTime;
+ }
+
+ const bool clip = clipTimer > 0.f;
+ if (clip) {
+ clipTimer -= sampleTime;
+ }
+
+ lights[MIX_CLIP_LIGHT + 0].setBrightnessSmooth(clip, sampleTime);
+ lights[MIX_CLIP_LIGHT + 1].setBrightness(0.f);
+ lights[MIX_CLIP_LIGHT + 2].setBrightness(0.f);
+ }
+ else {
+
+ float maxRmsOut = 0.f;
+ for (int c = 0; c < maxPolyphony; c++) {
+ maxRmsOut = std::max(maxRmsOut, std::fabs(output[c / 4][c % 4]));
+ }
+
+ lights[MIX_LIGHT + 0].setBrightness(0.f);
+ lights[MIX_LIGHT + 1].setBrightness(0.f);
+ lights[MIX_LIGHT + 2].setBrightnessSmooth(maxRmsOut / 5.f, sampleTime);
+
+ // if any channel peaks above 10V, turn the clip light on for the next clipTime seconds
+ if (maxRmsOut > 10.f) {
+ clipTimer = clipTime;
+ }
+
+ const bool clip = clipTimer > 0.f;
+ if (clip) {
+ clipTimer -= sampleTime;
+ }
+ lights[MIX_CLIP_LIGHT + 0].setBrightnessSmooth(clip, sampleTime);
+ lights[MIX_CLIP_LIGHT + 1].setBrightness(0.f);
+ lights[MIX_CLIP_LIGHT + 2].setBrightness(0.f);
+ }
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+ json_t* applySaturationJ = json_object_get(rootJ, "applySaturation");
+ if (applySaturationJ) {
+ applySaturation = json_boolean_value(applySaturationJ);
+ }
+ }
+
+ json_t* dataToJson() override {
+ json_t* rootJ = json_object();
+ json_object_set_new(rootJ, "applySaturation", json_boolean(applySaturation));
+
+ return rootJ;
+ }
+};
+
+
+struct BanditWidget : ModuleWidget {
+ BanditWidget(Bandit* module) {
+ setModule(module);
+ setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Bandit.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(createParam(mm2px(Vec(3.062, 51.365)), module, Bandit::LOW_GAIN_PARAM));
+ addParam(createParam(mm2px(Vec(13.23, 51.365)), module, Bandit::LOW_MID_GAIN_PARAM));
+ addParam(createParam(mm2px(Vec(23.398, 51.365)), module, Bandit::HIGH_MID_GAIN_PARAM));
+ addParam(createParam(mm2px(Vec(33.566, 51.365)), module, Bandit::HIGH_GAIN_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(5.038, 14.5)), module, Bandit::LOW_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.178, 14.5)), module, Bandit::LOW_MID_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.253, 14.5)), module, Bandit::HIGH_MID_INPUT));
+ addInput(createInputCentered(mm2px(Vec(35.328, 14.5)), module, Bandit::HIGH_INPUT));
+ addInput(createInputCentered(mm2px(Vec(5.045, 40.34)), module, Bandit::LOW_RETURN_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.118, 40.34)), module, Bandit::LOW_MID_RETURN_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.19, 40.338)), module, Bandit::HIGH_MID_RETURN_INPUT));
+ addInput(createInputCentered(mm2px(Vec(35.263, 40.34)), module, Bandit::HIGH_RETURN_INPUT));
+ addInput(createInputCentered(mm2px(Vec(5.038, 101.229)), module, Bandit::LOW_CV_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.113, 101.229)), module, Bandit::LOW_MID_CV_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.187, 101.231)), module, Bandit::HIGH_MID_CV_INPUT));
+ addInput(createInputCentered(mm2px(Vec(35.263, 101.229)), module, Bandit::HIGH_CV_INPUT));
+ addInput(createInputCentered(mm2px(Vec(10.075, 113.502)), module, Bandit::ALL_INPUT));
+ addInput(createInputCentered(mm2px(Vec(20.15, 113.5)), module, Bandit::ALL_CV_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(5.045, 27.248)), module, Bandit::LOW_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(15.118, 27.256)), module, Bandit::LOW_MID_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(25.19, 27.256)), module, Bandit::HIGH_MID_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(35.263, 27.256)), module, Bandit::HIGH_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(30.225, 113.5)), module, Bandit::MIX_OUTPUT));
+
+ addChild(createLightCentered>(mm2px(Vec(37.781, 111.125)), module, Bandit::MIX_CLIP_LIGHT));
+ addChild(createLightCentered>(mm2px(Vec(37.781, 115.875)), module, Bandit::MIX_LIGHT));
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ Bandit* module = dynamic_cast(this->module);
+ assert(module);
+
+ menu->addChild(new MenuSeparator());
+ menu->addChild(createBoolPtrMenuItem("Soft clip at ±10V", "", &module->applySaturation));
+
+ }
+};
+
+Model* modelBandit = createModel("Bandit");
\ No newline at end of file
diff --git a/src/Bypass.cpp b/src/Bypass.cpp
new file mode 100644
index 0000000..fbb84a3
--- /dev/null
+++ b/src/Bypass.cpp
@@ -0,0 +1,283 @@
+#include "plugin.hpp"
+
+using namespace simd;
+
+struct Bypass : Module {
+ enum ParamId {
+ MODE_PARAM,
+ FX_GAIN_PARAM,
+ LAUNCH_MODE_PARAM,
+ LAUNCH_BUTTON_PARAM,
+ SLEW_TIME_PARAM,
+ PARAMS_LEN
+ };
+ enum InputId {
+ IN_R_INPUT,
+ FROM_FX_L_INPUT,
+ FROM_FX_R_INPUT,
+ LAUNCH_INPUT,
+ IN_L_INPUT,
+ INPUTS_LEN
+ };
+ enum OutputId {
+ TO_FX_L_OUTPUT,
+ TO_FX_R_OUTPUT,
+ OUT_L_OUTPUT,
+ OUT_R_OUTPUT,
+ OUTPUTS_LEN
+ };
+ enum LightId {
+ LAUNCH_LED,
+ LIGHTS_LEN
+ };
+ enum LatchMode {
+ TOGGLE_MODE, // i.e. latch
+ MOMENTARY_MODE // i.e. gate
+ };
+ enum ReturnMode {
+ HARD_MODE,
+ SOFT_MODE
+ };
+ ReturnMode returnMode = ReturnMode::HARD_MODE;
+ ParamQuantity* launchParam, * slewTimeParam;
+ dsp::SchmittTrigger launchCvTrigger;
+ dsp::BooleanTrigger launchButtonTrigger;
+ dsp::BooleanTrigger latchTrigger;
+ dsp::SlewLimiter clickFilter;
+ bool launchButtonHeld = false;
+ bool applySaturation = true;
+ bool active = false;
+
+ struct GainParamQuantity : ParamQuantity {
+ std::string getDisplayValueString() override {
+ if (getValue() < 0.f) {
+ return string::f("%g dB", 30 * getValue());
+ }
+ else {
+ return string::f("%g dB", 12 * getValue());
+ }
+ }
+ };
+
+ Bypass() {
+ config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
+ auto switchParam = configSwitch(MODE_PARAM, 0.f, 1.f, 0.f, "Return mode", {"Hard", "Soft"});
+ switchParam->description = "In hard mode, Bypass wil cut off any sound coming from the loop.\nWith soft mode, the FX return is still active giving you reverb tails, decaying delay taps etc.";
+ configParam(FX_GAIN_PARAM, -1.f, 1.f, 0.f, "FX return gain");
+ configSwitch(LAUNCH_MODE_PARAM, 0.f, 1.f, 0.f, "Launch Mode", {"Latch (Toggle)", "Gate (Momentary)"});
+ launchParam = configButton(LAUNCH_BUTTON_PARAM, "Launch");
+ slewTimeParam = configParam(SLEW_TIME_PARAM, .005f, 0.05f, 0.01f, "Slew time", "s");
+
+ configInput(IN_L_INPUT, "Left");
+ configInput(IN_R_INPUT, "Right");
+ configInput(FROM_FX_L_INPUT, "From FX L");
+ configInput(FROM_FX_R_INPUT, "From FX R");
+ configInput(LAUNCH_INPUT, "Launch");
+
+ configOutput(TO_FX_L_OUTPUT, "To FX L");
+ configOutput(TO_FX_R_OUTPUT, "To FX R");
+ configOutput(OUT_L_OUTPUT, "Left");
+ configOutput(OUT_R_OUTPUT, "Right");
+
+ configBypass(IN_L_INPUT, OUT_L_OUTPUT);
+ configBypass(IN_R_INPUT, OUT_R_OUTPUT);
+ }
+
+ void process(const ProcessArgs& args) override {
+
+ // slew time in secs (so take inverse for lambda)
+ clickFilter.rise = clickFilter.fall = 1.0 / params[SLEW_TIME_PARAM].getValue();
+
+ const int maxInputChannels = std::max({1, inputs[IN_L_INPUT].getChannels(), inputs[IN_R_INPUT].getChannels()});
+ const int maxFxReturnChannels = std::max({1, inputs[FROM_FX_L_INPUT].getChannels(), inputs[FROM_FX_R_INPUT].getChannels()});
+
+ const LatchMode latchMode = (LatchMode) params[LAUNCH_MODE_PARAM].getValue();
+ const ReturnMode returnMode = (ReturnMode) params[MODE_PARAM].getValue();
+
+
+ const bool launchCvTriggered = launchCvTrigger.process(inputs[LAUNCH_INPUT].getVoltage());
+ const bool launchButtonPressed = launchButtonTrigger.process(launchButtonHeld);
+
+ // logical or (high if either high)
+ const float launchValue = std::max(launchCvTrigger.isHigh(), launchButtonTrigger.isHigh());
+ if (latchMode == LatchMode::TOGGLE_MODE) {
+ const bool risingEdge = launchCvTriggered || launchButtonPressed;
+
+ if (risingEdge) {
+ active = !active;
+ }
+ }
+
+ // FX send section
+ const float sendActive = clickFilter.process(args.sampleTime, (latchMode == LatchMode::TOGGLE_MODE) ? active : launchValue);
+ for (int c = 0; c < maxInputChannels; c += 4) {
+ const float_4 inL = inputs[IN_L_INPUT].getPolyVoltageSimd(c);
+ const float_4 inR = inputs[IN_R_INPUT].getNormalPolyVoltageSimd(inL, c);
+
+ // we start be assuming that FXs can be polyphonic, but recognise that often they are not
+ outputs[TO_FX_L_OUTPUT].setVoltageSimd(inL * sendActive, c);
+ outputs[TO_FX_R_OUTPUT].setVoltageSimd(inR * sendActive, c);
+ }
+ // fx send polyphony is set by input polyphony
+ outputs[TO_FX_L_OUTPUT].setChannels(maxInputChannels);
+ outputs[TO_FX_R_OUTPUT].setChannels(maxInputChannels);
+
+
+ // FX return section
+ const float gainTaper = params[FX_GAIN_PARAM].getValue() < 0.f ? 30 * params[FX_GAIN_PARAM].getValue() : params[FX_GAIN_PARAM].getValue() * 12;
+ const float fxReturnGain = std::pow(10, gainTaper / 20.0f);
+ float_4 dryLeft, dryRight, outL, outR;
+ for (int c = 0; c < maxFxReturnChannels; c += 4) {
+
+ const bool fxMonophonic = (maxInputChannels == 1);
+ if (fxMonophonic) {
+ // if the return fx is monophonic, mix down dry inputs to monophonic also
+ dryLeft = inputs[IN_L_INPUT].getVoltageSum();
+ dryRight = inputs[IN_R_INPUT].isConnected() ? inputs[IN_R_INPUT].getVoltageSum() : inputs[IN_L_INPUT].getVoltageSum();
+ }
+ else {
+ // if the return fx is polyphonic, then we don't need to do anything special
+ dryLeft = inputs[IN_L_INPUT].getPolyVoltageSimd(c);
+ dryRight = inputs[IN_R_INPUT].getNormalPolyVoltageSimd(dryLeft, c);
+ }
+
+ const float_4 fxLeftReturn = fxReturnGain * inputs[FROM_FX_L_INPUT].getPolyVoltageSimd(c);
+ const float_4 fxRightReturn = fxReturnGain * inputs[FROM_FX_R_INPUT].getPolyVoltageSimd(c);
+
+ if (returnMode == ReturnMode::HARD_MODE) {
+ outL = dryLeft * (1 - sendActive) + sendActive * fxLeftReturn;
+ outR = dryRight * (1 - sendActive) + sendActive * fxRightReturn;
+ }
+ else {
+ outL = dryLeft * (1 - sendActive) + fxLeftReturn;
+ outR = dryRight * (1 - sendActive) + fxRightReturn;
+ }
+
+ if (applySaturation) {
+ outL = Saturator::process(outL / 10.f) * 10.f;
+ outR = Saturator::process(outR / 10.f) * 10.f;
+ }
+
+ outputs[OUT_L_OUTPUT].setVoltageSimd(outL, c);
+ outputs[OUT_R_OUTPUT].setVoltageSimd(outR, c);
+ }
+
+ // output polyphony is set by fx return polyphony
+ outputs[OUT_L_OUTPUT].setChannels(maxFxReturnChannels);
+ outputs[OUT_R_OUTPUT].setChannels(maxFxReturnChannels);
+
+ lights[LAUNCH_LED].setSmoothBrightness(sendActive, args.sampleTime);
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+ json_t* applySaturationJ = json_object_get(rootJ, "applySaturation");
+ if (applySaturationJ) {
+ applySaturation = json_boolean_value(applySaturationJ);
+ }
+
+ json_t* activeJ = json_object_get(rootJ, "active");
+ if (activeJ) {
+ active = json_boolean_value(activeJ);
+ }
+ }
+
+ json_t* dataToJson() override {
+ json_t* rootJ = json_object();
+
+ json_object_set_new(rootJ, "applySaturation", json_boolean(applySaturation));
+ json_object_set_new(rootJ, "active", json_boolean(active));
+
+ return rootJ;
+ }
+};
+
+/** From VCV Free */
+struct VCVBezelBig : app::SvgSwitch {
+ VCVBezelBig() {
+ addFrame(Svg::load(asset::plugin(pluginInstance, "res/components/VCVBezelBig.svg")));
+ }
+};
+
+template
+struct VCVBezelLightBig : TBase {
+ VCVBezelLightBig() {
+ this->borderColor = color::WHITE_TRANSPARENT;
+ this->bgColor = color::WHITE_TRANSPARENT;
+ this->box.size = mm2px(math::Vec(11, 11));
+ }
+};
+
+struct RecordButton : LightButton> {
+ // Instead of using onAction() which is called on mouse up, handle on mouse down
+ void onDragStart(const event::DragStart& e) override {
+ Bypass* module = dynamic_cast(this->module);
+ if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
+ if (module) {
+ module->launchButtonHeld = true;
+ }
+ }
+
+ LightButton::onDragStart(e);
+ }
+
+ void onDragEnd(const event::DragEnd& e) override {
+ Bypass* module = dynamic_cast(this->module);
+ if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
+ if (module) {
+ module->launchButtonHeld = false;
+ }
+ }
+ }
+};
+
+struct BypassWidget : ModuleWidget {
+
+ SvgSwitch* launchParam;
+
+ BypassWidget(Bypass* module) {
+ setModule(module);
+ setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Bypass.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(createParam(mm2px(Vec(6.7, 63.263)), module, Bypass::MODE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(10.0, 78.903)), module, Bypass::FX_GAIN_PARAM));
+ addParam(createParam(mm2px(Vec(13.8, 91.6)), module, Bypass::LAUNCH_MODE_PARAM));
+
+ launchParam = createLightParamCentered(mm2px(Vec(10.0, 111.287)), module, Bypass::LAUNCH_BUTTON_PARAM, Bypass::LAUNCH_LED);
+ addParam(launchParam);
+
+ addInput(createInputCentered(mm2px(Vec(15.016, 15.03)), module, Bypass::IN_R_INPUT));
+ addInput(createInputCentered(mm2px(Vec(4.947, 40.893)), module, Bypass::FROM_FX_L_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.001, 40.893)), module, Bypass::FROM_FX_R_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.648, 95.028)), module, Bypass::LAUNCH_INPUT));
+ addInput(createInputCentered(mm2px(Vec(4.947, 15.03)), module, Bypass::IN_L_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(4.957, 27.961)), module, Bypass::TO_FX_L_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(14.957, 27.961)), module, Bypass::TO_FX_R_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(4.947, 53.846)), module, Bypass::OUT_L_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(14.957, 53.824)), module, Bypass::OUT_R_OUTPUT));
+ }
+
+ // for context menu
+ struct SlewTimeSider : ui::Slider {
+ explicit SlewTimeSider(ParamQuantity* q_) {
+ quantity = q_;
+ this->box.size.x = 200.0f;
+ }
+ };
+
+ void appendContextMenu(Menu* menu) override {
+ Bypass* module = dynamic_cast(this->module);
+ assert(module);
+
+ menu->addChild(new MenuSeparator());
+ menu->addChild(createBoolPtrMenuItem("Soft clip at ±10V", "", &module->applySaturation));
+ menu->addChild(new SlewTimeSider(module->slewTimeParam));
+
+ }
+};
+
+
+Model* modelBypass = createModel("Bypass");
\ No newline at end of file
diff --git a/src/EvenVCO.cpp b/src/EvenVCO.cpp
index 67d3d5e..ae2bded 100644
--- a/src/EvenVCO.cpp
+++ b/src/EvenVCO.cpp
@@ -1,4 +1,5 @@
#include "plugin.hpp"
+#include "ChowDSP.hpp"
using simd::float_4;
@@ -26,20 +27,11 @@ struct EvenVCO : Module {
NUM_OUTPUTS
};
- float_4 phase[4] = {};
- float_4 tri[4] = {};
- /** The value of the last sync input */
- float sync = 0.0;
- /** The outputs */
- /** Whether we are past the pulse width already */
- bool halfPhase[PORT_MAX_CHANNELS] = {};
+ float_4 phase[4] = {};
+ dsp::TSchmittTrigger syncTrigger[4];
bool removePulseDC = true;
-
- dsp::MinBlepGenerator<16, 32> triSquareMinBlep[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];
+ bool limitPW = true;
EvenVCO() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS);
@@ -51,7 +43,7 @@ struct EvenVCO : Module {
configInput(PITCH1_INPUT, "Pitch 1");
configInput(PITCH2_INPUT, "Pitch 2");
configInput(FM_INPUT, "FM");
- configInput(SYNC_INPUT, "Sync (not implemented)");
+ configInput(SYNC_INPUT, "Sync");
configInput(PWM_INPUT, "Pulse Width Modulation");
configOutput(TRI_OUTPUT, "Triangle");
@@ -59,157 +51,191 @@ struct EvenVCO : Module {
configOutput(EVEN_OUTPUT, "Even");
configOutput(SAW_OUTPUT, "Sawtooth");
configOutput(SQUARE_OUTPUT, "Square");
+
+ // calculate up/downsampling rates
+ onSampleRateChange();
}
- void process(const ProcessArgs& args) override {
+ void onSampleRateChange() override {
+ float sampleRate = APP->engine->getSampleRate();
+ for (int i = 0; i < NUM_OUTPUTS; ++i) {
+ for (int c = 0; c < 4; c++) {
+ oversampler[i][c].setOversamplingIndex(oversamplingIndex);
+ oversampler[i][c].reset(sampleRate);
+ }
+ }
- int channels_pitch1 = inputs[PITCH1_INPUT].getChannels();
- int channels_pitch2 = inputs[PITCH2_INPUT].getChannels();
+ const float lowFreqRegime = oversampler[0][0].getOversamplingRatio() * 1e-3 * sampleRate;
+ DEBUG("Low freq regime: %g", lowFreqRegime);
+ }
- int channels = 1;
- channels = std::max(channels, channels_pitch1);
- channels = std::max(channels, channels_pitch2);
+ float_4 aliasSuppressedTri(float_4* phases) {
+ float_4 triBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0
+ float_4 s = 0.5 - simd::abs(p); // eq 30
+ triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29
+ }
+ return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]);
+ }
- float pitch_0 = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
+ float_4 aliasSuppressedSaw(float_4* phases) {
+ float_4 sawBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
+ sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11
+ }
- // Compute frequency, pitch is 1V/oct
- float_4 pitch[4] = {};
- for (int c = 0; c < channels; c += 4)
- pitch[c / 4] = pitch_0;
+ return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
+ }
- if (inputs[PITCH1_INPUT].isConnected()) {
- for (int c = 0; c < channels; c += 4)
- pitch[c / 4] += inputs[PITCH1_INPUT].getPolyVoltageSimd(c);
+ float_4 aliasSuppressedDoubleSaw(float_4* phases) {
+ float_4 sawBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float_4 p = 4.0 * simd::ifelse(phases[i] < 0.5, phases[i], phases[i] - 0.5) - 1.0;
+ sawBuffer[i] = (p * p * p - p) / 24.0; // eq 11 (modified for doubled freq)
}
- if (inputs[PITCH2_INPUT].isConnected()) {
- for (int c = 0; c < channels; c += 4)
- pitch[c / 4] += inputs[PITCH2_INPUT].getPolyVoltageSimd(c);
- }
+ return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
+ }
- 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 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) {
+ float_4 sawOffsetBuff[3];
- 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);
+ for (int i = 0; i < 3; ++i) {
+ float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
+ float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
+ pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1]
+ sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
}
+ return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]);
+ }
- // Pulse width
- float_4 pw[4] = {};
- for (int c = 0; c < channels; c += 4)
- pw[c / 4] = params[PWM_PARAM].getValue();
+ chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
+ int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling
- if (inputs[PWM_INPUT].isConnected()) {
- for (int c = 0; c < channels; c += 4)
- pw[c / 4] += inputs[PWM_INPUT].getPolyVoltageSimd(c) / 5.f;
- }
+ void process(const ProcessArgs& args) override {
+
+ // pitch inputs determine number of polyphony engines
+ const int channels = std::max({1, inputs[PITCH1_INPUT].getChannels(), inputs[PITCH2_INPUT].getChannels()});
+
+ const float pitchKnobs = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
+ const int oversamplingRatio = oversampler[0][0].getOversamplingRatio();
- 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);
+ float_4 pw = simd::clamp(params[PWM_PARAM].getValue() + inputs[PWM_INPUT].getPolyVoltageSimd(c) / 5.f, -1.f, 1.f);
+ if (limitPW) {
+ pw = simd::rescale(pw, -1, +1, 0.05f, 0.95f);
+ }
+ else {
+ pw = simd::rescale(pw, -1.f, +1.f, 0.f, 1.f);
+ }
- // 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];
- }
+ const float_4 fmVoltage = inputs[FM_INPUT].getPolyVoltageSimd(c) * 0.25f;
+ const float_4 pitch = inputs[PITCH1_INPUT].getPolyVoltageSimd(c) + inputs[PITCH2_INPUT].getPolyVoltageSimd(c);
+ const float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitchKnobs + pitch + fmVoltage);
+ const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, 1e-6, 0.5f);
+ // floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator
+ // becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't
+ // a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz.
+ const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3;
+ // 1 / denominator for the second-order FD
+ const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase);
- // the next block can't be done with SIMD instructions, but should at least be completed with
- // blocks of 4 (otherwise popping artfifacts are generated from invalid phase/oldPhase/deltaPhase)
- const int channelsRoundedUpNearestFour = (1 + (channels - 1) / 4) * 4;
- for (int c = 0; c < channelsRoundedUpNearestFour; c++) {
+ // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option
+ // for it to be added back in for hardware compatibility reasons
+ const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);
- 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);
- }
+ // hard sync
+ const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd(c));
+ phase[c / 4] = simd::ifelse(syncMask, 0.5f, phase[c / 4]);
- 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;
- }
+ float_4* osBufferTri = oversampler[TRI_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferSaw = oversampler[SAW_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferSin = oversampler[SINE_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferSquare = oversampler[SQUARE_OUTPUT][c / 4].getOSBuffer();
+ float_4* osBufferEven = oversampler[EVEN_OUTPUT][c / 4].getOSBuffer();
+ for (int i = 0; i < oversamplingRatio; ++i) {
- // 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;
- }
- }
+ phase[c / 4] += deltaBasePhase;
+ // ensure within [0, 1]
+ phase[c / 4] -= simd::floor(phase[c / 4]);
- 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 < channelsRoundedUpNearestFour; 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();
- }
+ float_4 phases[3]; // phase as extrapolated to the current and two previous samples
- for (int c = 0; c < channels; c += 4) {
+ phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f);
+ phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f);
+ phases[2] = phase[c / 4];
- triSquare[c / 4] = simd::ifelse((phase[c / 4] < 0.5f), -1.f, +1.f);
- triSquare[c / 4] += triSquareMinBlepOut[c / 4];
+ if (outputs[SINE_OUTPUT].isConnected() || outputs[EVEN_OUTPUT].isConnected()) {
+ // sin doesn't need PDW
+ osBufferSin[i] = -simd::cos(M_PI + 2.0 * M_PI * phase[c / 4]);
+ }
- // Integrate square for triangle
+ if (outputs[TRI_OUTPUT].isConnected()) {
+ const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0);
+ const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv;
- 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];
+ osBufferTri[i] = -simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ }
- sine[c / 4] = 5.f * simd::cos(2 * M_PI * phase[c / 4]);
+ if (outputs[SAW_OUTPUT].isConnected()) {
+ const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0;
+ const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv;
- // minBlep adds a small amount of DC that becomes significant at higher frequencies,
- // this subtracts DC based on empirical observvations about the scaling relationship
- const float sawCorrect = -5.7;
- const float_4 sawDCComp = deltaPhase[c / 4] * sawCorrect;
+ osBufferSaw[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ }
- doubleSaw[c / 4] = simd::ifelse((phase[c / 4] < 0.5), (-1.f + 4.f * phase[c / 4]), (-1.f + 4.f * (phase[c / 4] - 0.5f)));
- doubleSaw[c / 4] += doubleSawMinBlepOut[c / 4];
- doubleSaw[c / 4] += 2.f * sawDCComp;
- doubleSaw[c / 4] *= 5.f;
+ if (outputs[SQUARE_OUTPUT].isConnected()) {
- even[c / 4] = 0.55 * (doubleSaw[c / 4] + 1.27 * sine[c / 4]);
- saw[c / 4] = -1.f + 2.f * phase[c / 4];
- saw[c / 4] += sawMinBlepOut[c / 4];
- saw[c / 4] += sawDCComp;
- saw[c / 4] *= 5.f;
+ float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < pw, -1.0, +1.0);
+ dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f;
- square[c / 4] = simd::ifelse((phase[c / 4] < pw[c / 4]), -1.f, +1.f);
- square[c / 4] += squareMinBlepOut[c / 4];
- square[c / 4] += removePulseDC * 2.f * (pw[c / 4] - 0.5f);
- square[c / 4] *= 5.f;
+ float_4 saw = aliasSuppressedSaw(phases);
+ float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw);
+ float_4 dpwOrder3 = (saw - sawOffset) * denominatorInv + pulseDCOffset;
- // 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);
- }
+ osBufferSquare[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ }
+
+ if (outputs[EVEN_OUTPUT].isConnected()) {
+
+ float_4 dpwOrder1 = 4.0 * simd::ifelse(phase[c / 4] < 0.5, phase[c / 4], phase[c / 4] - 0.5) - 1.0;
+ float_4 dpwOrder3 = aliasSuppressedDoubleSaw(phases) * denominatorInv;
+ float_4 doubleSaw = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
+ osBufferEven[i] = 0.55 * (doubleSaw + 1.27 * osBufferSin[i]);
+ }
+
+
+ } // end of oversampling loop
+
+ // downsample (if required)
+ if (outputs[SINE_OUTPUT].isConnected()) {
+ const float_4 outSin = (oversamplingRatio > 1) ? oversampler[SINE_OUTPUT][c / 4].downsample() : osBufferSin[0];
+ outputs[SINE_OUTPUT].setVoltageSimd(5.f * outSin, c);
+ }
+
+ if (outputs[TRI_OUTPUT].isConnected()) {
+ const float_4 outTri = (oversamplingRatio > 1) ? oversampler[TRI_OUTPUT][c / 4].downsample() : osBufferTri[0];
+ outputs[TRI_OUTPUT].setVoltageSimd(5.f * outTri, c);
+ }
+
+ if (outputs[SAW_OUTPUT].isConnected()) {
+ const float_4 outSaw = (oversamplingRatio > 1) ? oversampler[SAW_OUTPUT][c / 4].downsample() : osBufferSaw[0];
+ outputs[SAW_OUTPUT].setVoltageSimd(5.f * outSaw, c);
+ }
+
+ if (outputs[SQUARE_OUTPUT].isConnected()) {
+ const float_4 outSquare = (oversamplingRatio > 1) ? oversampler[SQUARE_OUTPUT][c / 4].downsample() : osBufferSquare[0];
+ outputs[SQUARE_OUTPUT].setVoltageSimd(5.f * outSquare, c);
+ }
+
+ if (outputs[EVEN_OUTPUT].isConnected()) {
+ const float_4 outEven = (oversamplingRatio > 1) ? oversampler[EVEN_OUTPUT][c / 4].downsample() : osBufferEven[0];
+ outputs[EVEN_OUTPUT].setVoltageSimd(5.f * outEven, c);
+ }
+
+ } // end of channels loop
// Outputs
outputs[TRI_OUTPUT].setChannels(channels);
@@ -223,6 +249,8 @@ struct EvenVCO : Module {
json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC));
+ json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
+ json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0][0].getOversamplingIndex()));
return rootJ;
}
@@ -231,6 +259,17 @@ struct EvenVCO : Module {
if (pulseDCJ) {
removePulseDC = json_boolean_value(pulseDCJ);
}
+
+ json_t* limitPWJ = json_object_get(rootJ, "limitPW");
+ if (limitPWJ) {
+ limitPW = json_boolean_value(limitPWJ);
+ }
+
+ json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
+ if (oversamplingIndexJ) {
+ oversamplingIndex = json_integer_value(oversamplingIndexJ);
+ onSampleRateChange();
+ }
}
};
@@ -269,10 +308,22 @@ struct EvenVCOWidget : ModuleWidget {
menu->addChild(new MenuSeparator());
menu->addChild(createSubmenuItem("Hardware compatibility", "",
- [ = ](Menu * menu) {
+ [ = ](Menu * menu) {
menu->addChild(createBoolPtrMenuItem("Remove DC from pulse", "", &module->removePulseDC));
- }
- ));
+ menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW));
+ }
+ ));
+
+ menu->addChild(createIndexSubmenuItem("Oversampling",
+ {"Off", "x2", "x4", "x8"},
+ [ = ]() {
+ return module->oversamplingIndex;
+ },
+ [ = ](int mode) {
+ module->oversamplingIndex = mode;
+ module->onSampleRateChange();
+ }
+ ));
}
};
diff --git a/src/NoisePlethora.cpp b/src/NoisePlethora.cpp
index e1f408c..6bdb306 100644
--- a/src/NoisePlethora.cpp
+++ b/src/NoisePlethora.cpp
@@ -160,8 +160,10 @@ struct NoisePlethora : Module {
// section A/B
bool bypassFilters = false;
- std::shared_ptr algorithm[2]; // pointer to actual algorithm
- std::string algorithmName[2]; // variable to cache which algorithm is active (after program CV applied)
+ std::shared_ptr algorithm[2]{nullptr, nullptr}; // pointer to actual algorithm
+ std::string_view algorithmName[2]{"", ""}; // variable to cache which algorithm is active (after program CV applied)
+ std::map> A_algorithms{};
+ std::map> B_algorithms{};
// filters for A/B
StateVariableFilter2ndOrder svfFilter[2];
@@ -195,11 +197,11 @@ struct NoisePlethora : Module {
configParam(Y_A_PARAM, 0.f, 1.f, 0.5f, "YA");
configParam(CUTOFF_CV_A_PARAM, 0.f, 1.f, 0.f, "Cutoff CV A");
configSwitch(FILTER_TYPE_A_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
- configParam(PROGRAM_PARAM, -INFINITY, +INFINITY, 0.f, "Program/Bank selection");
+ configParam(PROGRAM_PARAM, 0, 1, 0.f, "Program/Bank selection");
configSwitch(FILTER_TYPE_B_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
- configParam(CUTOFF_CV_B_PARAM, 0.f, 1.f, 0.f, "Cutoff B");
+ configParam(CUTOFF_CV_B_PARAM, 0.f, 1.f, 0.f, "Cutoff CV B");
configParam(X_B_PARAM, 0.f, 1.f, 0.5f, "XB");
- configParam(CUTOFF_B_PARAM, 0.f, 1.f, 1.f, "Cutoff CV B");
+ configParam(CUTOFF_B_PARAM, 0.f, 1.f, 1.f, "Cutoff B");
configParam(RES_B_PARAM, 0.f, 1.f, 0.f, "Resonance B");
configParam(Y_B_PARAM, 0.f, 1.f, 0.5f, "YB");
configSwitch(FILTER_TYPE_C_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
@@ -231,6 +233,11 @@ struct NoisePlethora : Module {
getInputInfo(PROG_A_INPUT)->description = "CV sums with active program (0.5V increments)";
getInputInfo(PROG_B_INPUT)->description = "CV sums with active program (0.5V increments)";
+ for (auto const &entry : MyFactory::Instance()->factoryFunctionRegistry) {
+ A_algorithms[entry.first] = MyFactory::Instance()->Create(entry.first);
+ B_algorithms[entry.first] = MyFactory::Instance()->Create(entry.first);
+ }
+
setAlgorithm(SECTION_B, "radioOhNo");
setAlgorithm(SECTION_A, "radioOhNo");
onSampleRateChange();
@@ -298,19 +305,19 @@ struct NoisePlethora : Module {
programSelectorWithCV.getSection(SECTION).setBank(bank);
programSelectorWithCV.getSection(SECTION).setProgram(programWithCV);
- const std::string newAlgorithmName = programSelectorWithCV.getSection(SECTION).getCurrentProgramName();
+ std::string_view newAlgorithmName = programSelectorWithCV.getSection(SECTION).getCurrentProgramName();
// this is just a caching check to avoid constantly re-initialisating the algorithms
if (newAlgorithmName != algorithmName[SECTION]) {
- algorithm[SECTION] = MyFactory::Instance()->Create(newAlgorithmName);
+ algorithm[SECTION] = SECTION == Section::SECTION_A ? A_algorithms[newAlgorithmName] : B_algorithms[newAlgorithmName];
algorithmName[SECTION] = newAlgorithmName;
if (algorithm[SECTION]) {
algorithm[SECTION]->init();
}
else {
- DEBUG("WARNING: Failed to initialise %s in programSelector", newAlgorithmName.c_str());
+ DEBUG("WARNING: Failed to initialise %s in programSelector", newAlgorithmName.data());
}
}
}
@@ -433,25 +440,23 @@ struct NoisePlethora : Module {
void processProgramBankKnobLogic(const ProcessArgs& args) {
// program knob will either change program for current bank...
- if (programButtonDragged) {
- // work out the change (in discrete increments) since the program/bank knob started being dragged
- const int delta = (int)(dialResolution * (params[PROGRAM_PARAM].getValue() - programKnobReferenceState));
+ {
if (programKnobMode == PROGRAM_MODE) {
const int numProgramsForCurrentBank = getBankForIndex(programSelector.getCurrent().getBank()).getSize();
+ const int currentProgram = programSelector.getCurrent().getProgram();
+ const int newProgramFromKnob = (int) std::round((numProgramsForCurrentBank - 1) * params[PROGRAM_PARAM].getValue());
- if (delta != 0) {
- const int newProgramFromKnob = unsigned_modulo(programSelector.getCurrent().getProgram() + delta, numProgramsForCurrentBank);
- programKnobReferenceState = params[PROGRAM_PARAM].getValue();
+ if (newProgramFromKnob != currentProgram) {
setAlgorithmViaProgram(newProgramFromKnob);
}
}
// ...or change bank, (trying to) keep program the same
else {
+ const int currentBank = programSelector.getCurrent().getBank();
+ const int newBankFromKnob = (int) std::round((numBanks - 1) * params[PROGRAM_PARAM].getValue());
- if (delta != 0) {
- const int newBankFromKnob = unsigned_modulo(programSelector.getCurrent().getBank() + delta, numBanks);
- programKnobReferenceState = params[PROGRAM_PARAM].getValue();
+ if (currentBank != newBankFromKnob) {
setAlgorithmViaBank(newBankFromKnob);
}
}
@@ -502,7 +507,7 @@ struct NoisePlethora : Module {
void setAlgorithmViaProgram(int newProgram) {
const int currentBank = programSelector.getCurrent().getBank();
- const std::string algorithmName = getBankForIndex(currentBank).getProgramName(newProgram);
+ std::string_view algorithmName = getBankForIndex(currentBank).getProgramName(newProgram);
const int section = programSelector.getMode();
setAlgorithm(section, algorithmName);
@@ -513,13 +518,13 @@ struct NoisePlethora : Module {
const int currentProgram = programSelector.getCurrent().getProgram();
// the new bank may not have as many algorithms
const int currentProgramInNewBank = clamp(currentProgram, 0, getBankForIndex(newBank).getSize() - 1);
- const std::string algorithmName = getBankForIndex(newBank).getProgramName(currentProgramInNewBank);
+ const std::string_view algorithmName = getBankForIndex(newBank).getProgramName(currentProgramInNewBank);
const int section = programSelector.getMode();
setAlgorithm(section, algorithmName);
}
- void setAlgorithm(int section, std::string algorithmName) {
+ void setAlgorithm(int section, std::string_view algorithmName) {
if (section > 1) {
return;
@@ -537,7 +542,7 @@ struct NoisePlethora : Module {
}
}
- DEBUG("WARNING: Didn't find %s in programSelector", algorithmName.c_str());
+ DEBUG("WARNING: Didn't find %s in programSelector", algorithmName.data());
}
void dataFromJson(json_t* rootJ) override {
@@ -565,8 +570,8 @@ struct NoisePlethora : Module {
json_t* dataToJson() override {
json_t* rootJ = json_object();
- json_object_set_new(rootJ, "algorithmA", json_string(programSelector.getA().getCurrentProgramName().c_str()));
- json_object_set_new(rootJ, "algorithmB", json_string(programSelector.getB().getCurrentProgramName().c_str()));
+ json_object_set_new(rootJ, "algorithmA", json_string(programSelector.getA().getCurrentProgramName().data()));
+ json_object_set_new(rootJ, "algorithmB", json_string(programSelector.getB().getCurrentProgramName().data()));
json_object_set_new(rootJ, "bypassFilters", json_boolean(bypassFilters));
json_object_set_new(rootJ, "blockDC", json_boolean(blockDC));
@@ -648,7 +653,7 @@ struct NoisePlethoraLEDDisplay : LightWidget {
}
void setTooltip() {
- std::string activeName = module->programSelector.getSection(section).getCurrentProgramName();
+ std::string_view activeName = module->programSelector.getSection(section).getCurrentProgramName();
tooltip = new ui::Tooltip;
tooltip->text = activeName;
APP->scene->addChild(tooltip);
@@ -839,7 +844,7 @@ struct NoisePlethoraWidget : ModuleWidget {
menu->addChild(createSubmenuItem(string::f("Bank %d: %s", i + 1, bankAliases[i].c_str()), currentBank == i ? CHECKMARK_STRING : "", [ = ](Menu * menu) {
for (int j = 0; j < getBankForIndex(i).getSize(); ++j) {
const bool currentProgramAndBank = (currentProgram == j) && (currentBank == i);
- const std::string algorithmName = getBankForIndex(i).getProgramName(j);
+ std::string_view algorithmName = getBankForIndex(i).getProgramName(j);
bool implemented = false;
for (auto item : MyFactory::Instance()->factoryFunctionRegistry) {
@@ -850,14 +855,14 @@ struct NoisePlethoraWidget : ModuleWidget {
}
if (implemented) {
- menu->addChild(createMenuItem(algorithmName, currentProgramAndBank ? CHECKMARK_STRING : "",
+ menu->addChild(createMenuItem(algorithmName.data(), currentProgramAndBank ? CHECKMARK_STRING : "",
[ = ]() {
module->setAlgorithm(sectionId, algorithmName);
}));
}
else {
// placeholder text (greyed out)
- menu->addChild(createMenuLabel(algorithmName));
+ menu->addChild(createMenuLabel(algorithmName.data()));
}
}
}));
@@ -874,4 +879,4 @@ struct NoisePlethoraWidget : ModuleWidget {
};
-Model* modelNoisePlethora = createModel("NoisePlethora");
\ No newline at end of file
+Model* modelNoisePlethora = createModel("NoisePlethora");
diff --git a/src/Octaves.cpp b/src/Octaves.cpp
index e783eb2..1994271 100644
--- a/src/Octaves.cpp
+++ b/src/Octaves.cpp
@@ -80,12 +80,12 @@ struct Octaves : Module {
configInput(VOCT2_INPUT, "V/Octave 2");
configInput(SYNC_INPUT, "Sync");
configInput(PWM_INPUT, "PWM");
- configInput(GAIN_01F_INPUT, "Gain x1F CV");
- configInput(GAIN_02F_INPUT, "Gain x1F CV");
- configInput(GAIN_04F_INPUT, "Gain x1F CV");
- configInput(GAIN_08F_INPUT, "Gain x1F CV");
- configInput(GAIN_16F_INPUT, "Gain x1F CV");
- configInput(GAIN_32F_INPUT, "Gain x1F CV");
+ configInput(GAIN_01F_INPUT, "Gain Fundamental CV");
+ configInput(GAIN_02F_INPUT, "Gain x2F CV");
+ configInput(GAIN_04F_INPUT, "Gain x4F CV");
+ configInput(GAIN_08F_INPUT, "Gain x8F CV");
+ configInput(GAIN_16F_INPUT, "Gain x16F CV");
+ configInput(GAIN_32F_INPUT, "Gain x32F CV");
configOutput(OUT_01F_OUTPUT, "x1F");
configOutput(OUT_02F_OUTPUT, "x2F");
@@ -115,12 +115,10 @@ struct Octaves : Module {
const int numActivePolyphonyEngines = getNumActivePolyphonyEngines();
// work out active outputs
- const std::vector connectedOutputs = getConnectedOutputs();
- if (connectedOutputs.size() == 0) {
+ const int highestOutput = getMaxConnectedOutput();
+ if (highestOutput == -1) {
return;
}
- // only process up to highest active channel
- const int highestOutput = *std::max_element(connectedOutputs.begin(), connectedOutputs.end());
for (int c = 0; c < numActivePolyphonyEngines; c += 4) {
@@ -200,8 +198,10 @@ struct Octaves : Module {
}
} // end of polyphony loop
- for (int connectedOutput : connectedOutputs) {
- outputs[OUT_01F_OUTPUT + connectedOutput].setChannels(numActivePolyphonyEngines);
+ for (int c = 0; c < NUM_OUTPUTS; c++) {
+ if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
+ outputs[OUT_01F_OUTPUT + c].setChannels(numActivePolyphonyEngines);
+ }
}
}
@@ -219,14 +219,14 @@ struct Octaves : Module {
return activePolyphonyEngines;
}
- std::vector getConnectedOutputs() {
- std::vector connectedOutputs;
+ int getMaxConnectedOutput() {
+ int maxChans = -1;
for (int c = 0; c < NUM_OUTPUTS; c++) {
if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
- connectedOutputs.push_back(c);
+ maxChans = c;
}
}
- return connectedOutputs;
+ return maxChans;
}
json_t* dataToJson() override {
@@ -333,4 +333,4 @@ struct OctavesWidget : ModuleWidget {
}
};
-Model* modelOctaves = createModel("Octaves");
\ No newline at end of file
+Model* modelOctaves = createModel("Octaves");
diff --git a/src/noise-plethora/plugins/Banks.cpp b/src/noise-plethora/plugins/Banks.cpp
index 30f1a17..70558b6 100644
--- a/src/noise-plethora/plugins/Banks.cpp
+++ b/src/noise-plethora/plugins/Banks.cpp
@@ -14,7 +14,7 @@ Bank::Bank(const BankElem& p1, const BankElem& p2, const BankElem& p3,
: programs{p1, p2, p3, p4, p5, p6, p7, p8, p9, p10}
{ }
-const std::string Bank::getProgramName(int i) {
+std::string_view Bank::getProgramName(int i) {
if (i >= 0 && i < programsPerBank) {
return programs[i].name;
}
diff --git a/src/noise-plethora/plugins/Banks.hpp b/src/noise-plethora/plugins/Banks.hpp
index 4896a0c..c7a703b 100644
--- a/src/noise-plethora/plugins/Banks.hpp
+++ b/src/noise-plethora/plugins/Banks.hpp
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
#include
#include
@@ -30,7 +31,7 @@ struct Bank {
const BankElem& p7 = defaultElem, const BankElem& p8 = defaultElem,
const BankElem& p9 = defaultElem, const BankElem& p10 = defaultElem);
- const std::string getProgramName(int i);
+ std::string_view getProgramName(int i);
float getProgramGain(int i);
int getSize();
diff --git a/src/noise-plethora/plugins/ProgramSelector.hpp b/src/noise-plethora/plugins/ProgramSelector.hpp
index 9765a06..14bcc68 100644
--- a/src/noise-plethora/plugins/ProgramSelector.hpp
+++ b/src/noise-plethora/plugins/ProgramSelector.hpp
@@ -68,7 +68,7 @@ public:
return program.setValue(p, getBankForIndex(getBank()).getSize());
}
- const std::string getCurrentProgramName() {
+ const std::string_view getCurrentProgramName() {
return getBankForIndex(getBank()).getProgramName(getProgram());
}
diff --git a/src/plugin.cpp b/src/plugin.cpp
index 90d4b02..1b48a6e 100644
--- a/src/plugin.cpp
+++ b/src/plugin.cpp
@@ -31,4 +31,6 @@ void init(rack::Plugin *p) {
p->addModel(modelMidiThing);
p->addModel(modelVoltio);
p->addModel(modelOctaves);
+ p->addModel(modelBypass);
+ p->addModel(modelBandit);
}
diff --git a/src/plugin.hpp b/src/plugin.hpp
index 8d79940..70ad26f 100644
--- a/src/plugin.hpp
+++ b/src/plugin.hpp
@@ -32,6 +32,8 @@ extern Model* modelBurst;
extern Model* modelMidiThing;
extern Model* modelVoltio;
extern Model* modelOctaves;
+extern Model* modelBypass;
+extern Model* modelBandit;
struct Knurlie : SvgScrew {
Knurlie() {
@@ -240,6 +242,13 @@ struct Davies1900hWhiteKnobEndless : Davies1900hKnob {
}
};
+template
+struct VeryLargeSimpleLight : TBase {
+ VeryLargeSimpleLight() {
+ this->box.size = mm2px(math::Vec(7, 7));
+ }
+};
+
inline int unsigned_modulo(int a, int b) {
return ((a % b) + b) % b;
}