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 + +![Oneiroi](img/Oneiroi.png) 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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; }