* 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)tags/v2.8.2
| @@ -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 | |||
| @@ -6,3 +6,5 @@ SOURCES += $(wildcard src/noise-plethora/*/*.cpp) | |||
| DISTRIBUTABLES += $(wildcard LICENSE*) res | |||
| include $(RACK_DIR)/plugin.mk | |||
| CXXFLAGS += -std=c++17 | |||
| @@ -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 | |||
|  | |||
| @@ -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" | |||
| ] | |||
| } | |||
| ] | |||
| } | |||
| } | |||
| @@ -0,0 +1,150 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
| <!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |||
| <svg | |||
| version="1.0" | |||
| id="svg64581" | |||
| x="0px" | |||
| y="0px" | |||
| width="34.015747" | |||
| height="34.015747" | |||
| viewBox="0 0 34.015747 34.015748" | |||
| enable-background="new 0 0 36.02325 36.0188" | |||
| xml:space="preserve" | |||
| sodipodi:docname="VCVBezelBig.svg" | |||
| inkscape:version="1.3.2 (091e20e, 2023-11-25)" | |||
| xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |||
| xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |||
| xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:svg="http://www.w3.org/2000/svg"><defs | |||
| id="defs7" /> | |||
| <sodipodi:namedview | |||
| bordercolor="#666666" | |||
| borderopacity="1.0" | |||
| fit-margin-bottom="0" | |||
| fit-margin-left="0" | |||
| fit-margin-right="0" | |||
| fit-margin-top="0" | |||
| id="base" | |||
| inkscape:current-layer="svg64581" | |||
| inkscape:cx="-101.01525" | |||
| inkscape:cy="10.101525" | |||
| inkscape:document-units="mm" | |||
| inkscape:pageopacity="0.0" | |||
| inkscape:pageshadow="2" | |||
| inkscape:window-height="1301" | |||
| inkscape:window-maximized="0" | |||
| inkscape:window-width="2560" | |||
| inkscape:window-x="0" | |||
| inkscape:window-y="25" | |||
| inkscape:zoom="1.979899" | |||
| pagecolor="#ffffff" | |||
| showgrid="false" | |||
| inkscape:showpageshadow="2" | |||
| inkscape:pagecheckerboard="0" | |||
| inkscape:deskcolor="#d1d1d1"> | |||
| </sodipodi:namedview> | |||
| <g | |||
| id="g7" | |||
| transform="matrix(0.94421282,0,0,0.94421046,3.5880087e-4,9.4421046e-5)"> | |||
| <g | |||
| id="g5959-5_106_" | |||
| transform="translate(301.93513,1189.951)"> | |||
| <path | |||
| id="path5961-3_112_" | |||
| inkscape:connector-curvature="0" | |||
| d="m -265.91,-1171.937 c 0,9.9474 -8.06683,18.0115 -18.01407,18.0115 -9.94595,0 -18.01144,-8.0641 -18.01144,-18.0115 0,-9.9513 8.06549,-18.0141 18.01141,-18.0141 9.94726,10e-5 18.0141,8.0628 18.0141,18.0141" /> | |||
| </g> | |||
| <g | |||
| id="g5959-5_105_" | |||
| transform="translate(301.93513,1189.951)"> | |||
| <linearGradient | |||
| id="path5961-3_1_" | |||
| gradientUnits="userSpaceOnUse" | |||
| x1="-590.1001" | |||
| y1="-2191.8545" | |||
| x2="-557.04907" | |||
| y2="-2191.8545" | |||
| gradientTransform="rotate(90,-938.70655,-1537.0705)"> | |||
| <stop | |||
| offset="0" | |||
| style="stop-color:#787878" | |||
| id="stop1" /> | |||
| <stop | |||
| offset="1" | |||
| style="stop-color:#474747" | |||
| id="stop2" /> | |||
| </linearGradient> | |||
| <path | |||
| id="path5961-3_111_" | |||
| inkscape:connector-curvature="0" | |||
| fill="url(#path5961-3_1_)" | |||
| d="m -300.44827,-1171.9395 c 0,-9.1261 7.40079,-16.5244 16.52676,-16.5244 9.12473,0 16.52429,7.3984 16.52429,16.5244 0,9.1297 -7.39957,16.5268 -16.52432,16.5268 -9.12595,0 -16.52673,-7.3971 -16.52673,-16.5268" | |||
| style="fill:url(#path5961-3_1_)" /> | |||
| </g> | |||
| <g | |||
| opacity="0.21" | |||
| id="g4"> | |||
| <g | |||
| id="g5959-5_104_" | |||
| transform="translate(301.93513,1189.951)"> | |||
| <linearGradient | |||
| id="path5961-3_2_" | |||
| gradientUnits="userSpaceOnUse" | |||
| x1="-299.58844" | |||
| y1="-1111.948" | |||
| x2="-268.70374" | |||
| y2="-1111.948" | |||
| gradientTransform="rotate(-90,-314.02952,-1142.0548)"> | |||
| <stop | |||
| offset="0.00559" | |||
| style="stop-color:#6B6B6B" | |||
| id="stop3" /> | |||
| <stop | |||
| offset="1" | |||
| style="stop-color:#DEDEDE" | |||
| id="stop4" /> | |||
| </linearGradient> | |||
| <path | |||
| id="path5961-3_110_" | |||
| inkscape:connector-curvature="0" | |||
| fill="url(#path5961-3_2_)" | |||
| d="m -283.92389,-1156.496 c -8.52792,0 -15.44122,-6.9156 -15.44122,-15.4434 0,-8.5267 6.9133,-15.4412 15.44122,-15.4412 8.53131,0 15.44348,6.9145 15.44348,15.4412 0,8.5278 -6.91214,15.4434 -15.44348,15.4434" | |||
| style="fill:url(#path5961-3_2_)" /> | |||
| </g> | |||
| </g> | |||
| <g | |||
| id="g6"> | |||
| <g | |||
| id="g5959-5_103_" | |||
| transform="translate(301.93513,1189.951)"> | |||
| <linearGradient | |||
| id="path5961-3_3_" | |||
| gradientUnits="userSpaceOnUse" | |||
| x1="-298.98605" | |||
| y1="-1111.948" | |||
| x2="-269.30612" | |||
| y2="-1111.948" | |||
| gradientTransform="rotate(-90,-314.02952,-1142.0548)"> | |||
| <stop | |||
| offset="0.00559" | |||
| style="stop-color:#5B5B5B" | |||
| id="stop5" /> | |||
| <stop | |||
| offset="1" | |||
| style="stop-color:#6C6C6C" | |||
| id="stop6" /> | |||
| </linearGradient> | |||
| <path | |||
| id="path5961-3_109_" | |||
| inkscape:connector-curvature="0" | |||
| fill="url(#path5961-3_3_)" | |||
| d="m -283.92386,-1157.0984 c -8.19525,0 -14.83887,-6.6459 -14.83887,-14.8409 0,-8.1941 6.64362,-14.839 14.83887,-14.839 8.19852,0 14.84106,6.6449 14.84106,14.8388 1e-5,8.1952 -6.64251,14.8411 -14.84106,14.8411" | |||
| style="fill:url(#path5961-3_3_)" /> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -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<float_4> 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<float_4>::Type::LOWPASS, lowFc, Q[stage], V); | |||
| filterLowMid[i][stage].setParameters(dsp::TBiquadFilter<float_4>::Type::BANDPASS, lowMidFc, Q[stage], V); | |||
| filterHighMid[i][stage].setParameters(dsp::TBiquadFilter<float_4>::Type::BANDPASS, highMidFc, Q[stage], V); | |||
| filterHigh[i][stage].setParameters(dsp::TBiquadFilter<float_4>::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<float_4>(c); | |||
| const float_4 inLowMid = inputs[LOW_MID_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 inHighMid = inputs[HIGH_MID_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 inHigh = inputs[HIGH_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 inAll = inputs[ALL_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| // bypass sums all inputs to the output | |||
| outputs[MIX_OUTPUT].setVoltageSimd<float_4>(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<float_4>(c); | |||
| const float_4 inLowMid = inputs[LOW_MID_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 inHighMid = inputs[HIGH_MID_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 inHigh = inputs[HIGH_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 inAll = inputs[ALL_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 lowGain = params[LOW_GAIN_PARAM].getValue() * clamp(inputs[LOW_CV_INPUT].getNormalPolyVoltageSimd<float_4>(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<float_4>(outLow, c); | |||
| const float_4 lowMidGain = params[LOW_MID_GAIN_PARAM].getValue() * clamp(inputs[LOW_MID_CV_INPUT].getNormalPolyVoltageSimd<float_4>(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<float_4>(outLowMid, c); | |||
| const float_4 highMidGain = params[HIGH_MID_GAIN_PARAM].getValue() * clamp(inputs[HIGH_MID_CV_INPUT].getNormalPolyVoltageSimd<float_4>(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<float_4>(outHighMid, c); | |||
| const float_4 highGain = params[HIGH_GAIN_PARAM].getValue() * clamp(inputs[HIGH_CV_INPUT].getNormalPolyVoltageSimd<float_4>(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<float_4>(outHigh, c); | |||
| // the fx return input is normalled to the fx send output | |||
| mixOutput[c / 4] = inputs[LOW_RETURN_INPUT].getNormalPolyVoltageSimd<float_4>(outLow * !outputs[LOW_OUTPUT].isConnected(), c); | |||
| mixOutput[c / 4] += inputs[LOW_MID_RETURN_INPUT].getNormalPolyVoltageSimd<float_4>(outLowMid * !outputs[LOW_MID_OUTPUT].isConnected(), c); | |||
| mixOutput[c / 4] += inputs[HIGH_MID_RETURN_INPUT].getNormalPolyVoltageSimd<float_4>(outHighMid * !outputs[HIGH_MID_OUTPUT].isConnected(), c); | |||
| mixOutput[c / 4] += inputs[HIGH_RETURN_INPUT].getNormalPolyVoltageSimd<float_4>(outHigh * !outputs[HIGH_OUTPUT].isConnected(), c); | |||
| mixOutput[c / 4] = mixOutput[c / 4] * clamp(inputs[ALL_CV_INPUT].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f, 0.f, 1.f); | |||
| if (applySaturation) { | |||
| mixOutput[c / 4] = Saturator<float_4>::process(mixOutput[c / 4] / 10.f) * 10.f; | |||
| } | |||
| outputs[MIX_OUTPUT].setVoltageSimd<float_4>(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<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); | |||
| addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||
| addParam(createParam<BefacoSlidePot>(mm2px(Vec(3.062, 51.365)), module, Bandit::LOW_GAIN_PARAM)); | |||
| addParam(createParam<BefacoSlidePot>(mm2px(Vec(13.23, 51.365)), module, Bandit::LOW_MID_GAIN_PARAM)); | |||
| addParam(createParam<BefacoSlidePot>(mm2px(Vec(23.398, 51.365)), module, Bandit::HIGH_MID_GAIN_PARAM)); | |||
| addParam(createParam<BefacoSlidePot>(mm2px(Vec(33.566, 51.365)), module, Bandit::HIGH_GAIN_PARAM)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.038, 14.5)), module, Bandit::LOW_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.178, 14.5)), module, Bandit::LOW_MID_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.253, 14.5)), module, Bandit::HIGH_MID_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.328, 14.5)), module, Bandit::HIGH_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.045, 40.34)), module, Bandit::LOW_RETURN_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.118, 40.34)), module, Bandit::LOW_MID_RETURN_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.19, 40.338)), module, Bandit::HIGH_MID_RETURN_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.263, 40.34)), module, Bandit::HIGH_RETURN_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.038, 101.229)), module, Bandit::LOW_CV_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.113, 101.229)), module, Bandit::LOW_MID_CV_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.187, 101.231)), module, Bandit::HIGH_MID_CV_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.263, 101.229)), module, Bandit::HIGH_CV_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(10.075, 113.502)), module, Bandit::ALL_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(20.15, 113.5)), module, Bandit::ALL_CV_INPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.045, 27.248)), module, Bandit::LOW_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(15.118, 27.256)), module, Bandit::LOW_MID_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.19, 27.256)), module, Bandit::HIGH_MID_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.263, 27.256)), module, Bandit::HIGH_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(30.225, 113.5)), module, Bandit::MIX_OUTPUT)); | |||
| addChild(createLightCentered<MediumLight<RedGreenBlueLight>>(mm2px(Vec(37.781, 111.125)), module, Bandit::MIX_CLIP_LIGHT)); | |||
| addChild(createLightCentered<MediumLight<RedGreenBlueLight>>(mm2px(Vec(37.781, 115.875)), module, Bandit::MIX_LIGHT)); | |||
| } | |||
| void appendContextMenu(Menu* menu) override { | |||
| Bandit* module = dynamic_cast<Bandit*>(this->module); | |||
| assert(module); | |||
| menu->addChild(new MenuSeparator()); | |||
| menu->addChild(createBoolPtrMenuItem("Soft clip at ±10V", "", &module->applySaturation)); | |||
| } | |||
| }; | |||
| Model* modelBandit = createModel<Bandit, BanditWidget>("Bandit"); | |||
| @@ -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<GainParamQuantity>(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<float_4>(c); | |||
| const float_4 inR = inputs[IN_R_INPUT].getNormalPolyVoltageSimd<float_4>(inL, c); | |||
| // we start be assuming that FXs can be polyphonic, but recognise that often they are not | |||
| outputs[TO_FX_L_OUTPUT].setVoltageSimd<float_4>(inL * sendActive, c); | |||
| outputs[TO_FX_R_OUTPUT].setVoltageSimd<float_4>(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<float_4>(c); | |||
| dryRight = inputs[IN_R_INPUT].getNormalPolyVoltageSimd<float_4>(dryLeft, c); | |||
| } | |||
| const float_4 fxLeftReturn = fxReturnGain * inputs[FROM_FX_L_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 fxRightReturn = fxReturnGain * inputs[FROM_FX_R_INPUT].getPolyVoltageSimd<float_4>(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<float_4>::process(outL / 10.f) * 10.f; | |||
| outR = Saturator<float_4>::process(outR / 10.f) * 10.f; | |||
| } | |||
| outputs[OUT_L_OUTPUT].setVoltageSimd<float_4>(outL, c); | |||
| outputs[OUT_R_OUTPUT].setVoltageSimd<float_4>(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 <typename TBase> | |||
| 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<VCVBezelBig, VCVBezelLightBig<RedLight>> { | |||
| // 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<Bypass*>(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<Bypass*>(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<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); | |||
| addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||
| addParam(createParam<CKSSHoriz2>(mm2px(Vec(6.7, 63.263)), module, Bypass::MODE_PARAM)); | |||
| addParam(createParamCentered<BefacoTinyKnobWhite>(mm2px(Vec(10.0, 78.903)), module, Bypass::FX_GAIN_PARAM)); | |||
| addParam(createParam<CKSSNarrow>(mm2px(Vec(13.8, 91.6)), module, Bypass::LAUNCH_MODE_PARAM)); | |||
| launchParam = createLightParamCentered<RecordButton>(mm2px(Vec(10.0, 111.287)), module, Bypass::LAUNCH_BUTTON_PARAM, Bypass::LAUNCH_LED); | |||
| addParam(launchParam); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.016, 15.03)), module, Bypass::IN_R_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(4.947, 40.893)), module, Bypass::FROM_FX_L_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.001, 40.893)), module, Bypass::FROM_FX_R_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(6.648, 95.028)), module, Bypass::LAUNCH_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(4.947, 15.03)), module, Bypass::IN_L_INPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(4.957, 27.961)), module, Bypass::TO_FX_L_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(14.957, 27.961)), module, Bypass::TO_FX_R_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(4.947, 53.846)), module, Bypass::OUT_L_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(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<Bypass*>(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, BypassWidget>("Bypass"); | |||
| @@ -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<float_4> 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<float_4>(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<float_4>(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<float_4>(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<float_4>(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<float_4>(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<float_4>(c) * 0.25f; | |||
| const float_4 pitch = inputs[PITCH1_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[PITCH2_INPUT].getPolyVoltageSimd<float_4>(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<float_4>(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(); | |||
| } | |||
| )); | |||
| } | |||
| }; | |||
| @@ -160,8 +160,10 @@ struct NoisePlethora : Module { | |||
| // section A/B | |||
| bool bypassFilters = false; | |||
| std::shared_ptr<NoisePlethoraPlugin> algorithm[2]; // pointer to actual algorithm | |||
| std::string algorithmName[2]; // variable to cache which algorithm is active (after program CV applied) | |||
| std::shared_ptr<NoisePlethoraPlugin> 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<std::string_view, std::shared_ptr<NoisePlethoraPlugin>> A_algorithms{}; | |||
| std::map<std::string_view, std::shared_ptr<NoisePlethoraPlugin>> 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, NoisePlethoraWidget>("NoisePlethora"); | |||
| Model* modelNoisePlethora = createModel<NoisePlethora, NoisePlethoraWidget>("NoisePlethora"); | |||
| @@ -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<int> 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<int> getConnectedOutputs() { | |||
| std::vector<int> 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, OctavesWidget>("Octaves"); | |||
| Model* modelOctaves = createModel<Octaves, OctavesWidget>("Octaves"); | |||
| @@ -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; | |||
| } | |||
| @@ -1,6 +1,7 @@ | |||
| #pragma once | |||
| #include <string> | |||
| #include <string_view> | |||
| #include <memory> | |||
| #include <array> | |||
| @@ -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(); | |||
| @@ -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()); | |||
| } | |||
| @@ -31,4 +31,6 @@ void init(rack::Plugin *p) { | |||
| p->addModel(modelMidiThing); | |||
| p->addModel(modelVoltio); | |||
| p->addModel(modelOctaves); | |||
| p->addModel(modelBypass); | |||
| p->addModel(modelBandit); | |||
| } | |||
| @@ -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 <typename TBase = WhiteLight> | |||
| 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; | |||
| } | |||