| @@ -341,6 +341,20 @@ | |||||
| "tags": [ | "tags": [ | ||||
| "Hardware clone", | "Hardware clone", | ||||
| "Mixer", | "Mixer", | ||||
| "Polyphonic", | |||||
| "Utility" | |||||
| ] | |||||
| }, | |||||
| { | |||||
| "slug": "Bandit", | |||||
| "name": "Bandit", | |||||
| "description": "A spectral processing playground.", | |||||
| "tags": [ | |||||
| "Equalizer", | |||||
| "Filter", | |||||
| "Hardware clone", | |||||
| "Mixer", | |||||
| "Polyphonic", | |||||
| "Utility" | "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,208 @@ | |||||
| #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 | |||||
| }; | |||||
| dsp::TBiquadFilter<float_4> filterLow[4], filterLowMid[4], filterHighMid[4], filterHigh[4]; | |||||
| Bandit() { | |||||
| config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); | |||||
| configParam(LOW_GAIN_PARAM, -1.f, 1.f, 0.f, "Low gain"); | |||||
| configParam(LOW_MID_GAIN_PARAM, -1.f, 1.f, 0.f, "Low mid gain"); | |||||
| configParam(HIGH_MID_GAIN_PARAM, -1.f, 1.f, 0.f, "High mid gain"); | |||||
| configParam(HIGH_GAIN_PARAM, -1.f, 1.f, 0.f, "High gain"); | |||||
| configInput(LOW_INPUT, "Low"); | |||||
| configInput(LOW_MID_INPUT, "Low mid"); | |||||
| configInput(HIGH_MID_INPUT, "High mid"); | |||||
| configInput(HIGH_INPUT, "High"); | |||||
| 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"); | |||||
| 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"); | |||||
| configInput(ALL_CV_INPUT, "All CV"); | |||||
| configOutput(LOW_OUTPUT, "Low"); | |||||
| configOutput(LOW_MID_OUTPUT, "Low mid"); | |||||
| configOutput(HIGH_MID_OUTPUT, "High mid"); | |||||
| configOutput(HIGH_OUTPUT, "High"); | |||||
| configOutput(MIX_OUTPUT, "Mix"); | |||||
| onSampleRateChange(); | |||||
| } | |||||
| 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 = 5000.f / sr; | |||||
| const float Q = 1.f, V = 1.f; | |||||
| for (int i = 0; i < 4; ++i) { | |||||
| filterLow[i].setParameters(dsp::TBiquadFilter<float_4>::Type::LOWPASS, lowFc, Q, V); | |||||
| filterLowMid[i].setParameters(dsp::TBiquadFilter<float_4>::Type::BANDPASS, lowMidFc, Q, V); | |||||
| filterHighMid[i].setParameters(dsp::TBiquadFilter<float_4>::Type::BANDPASS, highMidFc, Q, V); | |||||
| filterHigh[i].setParameters(dsp::TBiquadFilter<float_4>::Type::HIGHPASS, highFc, Q, 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()}); | |||||
| 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() * inputs[LOW_CV_INPUT].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f; | |||||
| const float_4 outLow = filterLow[c / 4].process((inLow + inAll) * lowGain); | |||||
| outputs[LOW_OUTPUT].setVoltageSimd<float_4>(outLow, c); | |||||
| const float_4 lowMidGain = params[LOW_MID_GAIN_PARAM].getValue() * inputs[LOW_MID_CV_INPUT].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f; | |||||
| const float_4 outLowMid = filterLowMid[c / 4].process((inLowMid + inAll) * lowMidGain); | |||||
| outputs[LOW_MID_OUTPUT].setVoltageSimd<float_4>(outLowMid, c); | |||||
| const float_4 highMidGain = params[HIGH_MID_GAIN_PARAM].getValue() * inputs[HIGH_MID_CV_INPUT].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f; | |||||
| const float_4 outHighMid = filterHighMid[c / 4].process((inHighMid + inAll) * highMidGain); | |||||
| outputs[HIGH_MID_OUTPUT].setVoltageSimd<float_4>(outHighMid, c); | |||||
| const float_4 highGain = params[HIGH_GAIN_PARAM].getValue() * inputs[HIGH_CV_INPUT].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f; | |||||
| const float_4 outHigh = filterHigh[c / 4].process((inHigh + inAll) * highGain); | |||||
| outputs[HIGH_OUTPUT].setVoltageSimd<float_4>(outHigh, c); | |||||
| const float_4 fxReturnSum = inputs[LOW_RETURN_INPUT].getPolyVoltageSimd<float_4>(c) + | |||||
| inputs[LOW_MID_RETURN_INPUT].getPolyVoltageSimd<float_4>(c) + | |||||
| inputs[HIGH_MID_RETURN_INPUT].getPolyVoltageSimd<float_4>(c) + | |||||
| inputs[HIGH_RETURN_INPUT].getPolyVoltageSimd<float_4>(c); | |||||
| outputs[MIX_OUTPUT].setVoltageSimd<float_4>(fxReturnSum, c); | |||||
| } | |||||
| outputs[LOW_OUTPUT].setChannels(maxPolyphony); | |||||
| if (maxPolyphony == 1) { | |||||
| lights[MIX_LIGHT + 0].setBrightness(0.f); | |||||
| lights[MIX_LIGHT + 1].setBrightnessSmooth(outputs[MIX_OUTPUT].getVoltageRMS(), args.sampleTime); | |||||
| lights[MIX_LIGHT + 2].setBrightness(0.f); | |||||
| } | |||||
| else { | |||||
| lights[MIX_LIGHT + 0].setBrightness(0.f); | |||||
| lights[MIX_LIGHT + 1].setBrightness(0.f); | |||||
| lights[MIX_LIGHT + 2].setBrightnessSmooth(outputs[MIX_OUTPUT].getVoltageRMS(), args.sampleTime); | |||||
| } | |||||
| } | |||||
| }; | |||||
| 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)); | |||||
| } | |||||
| }; | |||||
| Model* modelBandit = createModel<Bandit, BanditWidget>("Bandit"); | |||||
| @@ -8,6 +8,7 @@ struct Bypass : Module { | |||||
| FX_GAIN_PARAM, | FX_GAIN_PARAM, | ||||
| LAUNCH_MODE_PARAM, | LAUNCH_MODE_PARAM, | ||||
| LAUNCH_BUTTON_PARAM, | LAUNCH_BUTTON_PARAM, | ||||
| SLEW_TIME_PARAM, | |||||
| PARAMS_LEN | PARAMS_LEN | ||||
| }; | }; | ||||
| enum InputId { | enum InputId { | ||||
| @@ -37,13 +38,13 @@ struct Bypass : Module { | |||||
| HARD_MODE, | HARD_MODE, | ||||
| SOFT_MODE | SOFT_MODE | ||||
| }; | }; | ||||
| LatchMode latchMode = LatchMode::MOMENTARY_MODE; | |||||
| ReturnMode returnMode = ReturnMode::HARD_MODE; | ReturnMode returnMode = ReturnMode::HARD_MODE; | ||||
| ParamQuantity* launchParam; | |||||
| ParamQuantity* launchParam, * slewTimeParam; | |||||
| dsp::SchmittTrigger launchCvTrigger; | dsp::SchmittTrigger launchCvTrigger; | ||||
| dsp::BooleanTrigger launchButtonTrigger; | dsp::BooleanTrigger launchButtonTrigger; | ||||
| dsp::BooleanTrigger latchTrigger; | dsp::BooleanTrigger latchTrigger; | ||||
| dsp::SlewLimiter clickFilter; | dsp::SlewLimiter clickFilter; | ||||
| bool launchButtonHeld = false; | |||||
| Bypass() { | Bypass() { | ||||
| config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); | config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); | ||||
| @@ -52,6 +53,9 @@ struct Bypass : Module { | |||||
| configSwitch(LAUNCH_MODE_PARAM, 0.f, 1.f, 0.f, "Launch Mode", {"Latch (Toggle)", "Gate (Momentary)"}); | configSwitch(LAUNCH_MODE_PARAM, 0.f, 1.f, 0.f, "Launch Mode", {"Latch (Toggle)", "Gate (Momentary)"}); | ||||
| launchParam = configButton(LAUNCH_BUTTON_PARAM, "Launch"); | 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_L_INPUT, "Left"); | ||||
| configInput(IN_R_INPUT, "Right"); | configInput(IN_R_INPUT, "Right"); | ||||
| configInput(FROM_FX_L_INPUT, "From FX L"); | configInput(FROM_FX_L_INPUT, "From FX L"); | ||||
| @@ -64,30 +68,32 @@ struct Bypass : Module { | |||||
| configOutput(OUT_R_OUTPUT, "Right"); | configOutput(OUT_R_OUTPUT, "Right"); | ||||
| configBypass(IN_L_INPUT, OUT_L_OUTPUT); | configBypass(IN_L_INPUT, OUT_L_OUTPUT); | ||||
| configBypass(IN_R_INPUT, OUT_R_OUTPUT); | |||||
| configBypass(IN_R_INPUT, OUT_R_OUTPUT); | |||||
| clickFilter.rise = 1 / 0.01f; // 0.01 ms | |||||
| clickFilter.fall = 1 / 0.01f; // 0.01 ms | |||||
| } | } | ||||
| bool active = false; | bool active = false; | ||||
| void process(const ProcessArgs& args) override { | void process(const ProcessArgs& args) override { | ||||
| const int maxChannels = std::max(inputs[IN_L_INPUT].getChannels(), inputs[IN_R_INPUT].getChannels()); | |||||
| // 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()}); | |||||
| latchMode = (LatchMode) params[LAUNCH_MODE_PARAM].getValue(); | |||||
| const LatchMode latchMode = (LatchMode) params[LAUNCH_MODE_PARAM].getValue(); | |||||
| const ReturnMode returnMode = (ReturnMode) params[MODE_PARAM].getValue(); | const ReturnMode returnMode = (ReturnMode) params[MODE_PARAM].getValue(); | ||||
| const bool launchCvTriggered = launchCvTrigger.process(inputs[LAUNCH_INPUT].getVoltage()); | const bool launchCvTriggered = launchCvTrigger.process(inputs[LAUNCH_INPUT].getVoltage()); | ||||
| const bool launchButtonPressed = launchButtonTrigger.process(params[LAUNCH_BUTTON_PARAM].getValue()); | |||||
| const bool launchButtonPressed = launchButtonTrigger.process(launchButtonHeld); | |||||
| // logical or (high if either high) | // logical or (high if either high) | ||||
| const float launchValue = std::max(launchCvTrigger.isHigh(), launchButtonTrigger.isHigh()); | const float launchValue = std::max(launchCvTrigger.isHigh(), launchButtonTrigger.isHigh()); | ||||
| if (latchMode == LatchMode::TOGGLE_MODE) { | if (latchMode == LatchMode::TOGGLE_MODE) { | ||||
| const bool risingEdge = launchCvTriggered || launchButtonPressed; | const bool risingEdge = launchCvTriggered || launchButtonPressed; | ||||
| // TODO: sometimes misses? | |||||
| if (risingEdge) { | if (risingEdge) { | ||||
| active = !active; | active = !active; | ||||
| } | } | ||||
| @@ -96,36 +102,91 @@ struct Bypass : Module { | |||||
| const float fxGain = std::pow(10, params[FX_GAIN_PARAM].getValue() / 20.0f); | const float fxGain = std::pow(10, params[FX_GAIN_PARAM].getValue() / 20.0f); | ||||
| const float sendActive = clickFilter.process(args.sampleTime, (latchMode == LatchMode::TOGGLE_MODE) ? active : launchValue); | const float sendActive = clickFilter.process(args.sampleTime, (latchMode == LatchMode::TOGGLE_MODE) ? active : launchValue); | ||||
| for (int c = 0; c < maxChannels; c += 4) { | |||||
| for (int c = 0; c < maxInputChannels; c += 4) { | |||||
| const float_4 inL = inputs[IN_L_INPUT].getPolyVoltageSimd<float_4>(c); | 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); | 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[TOFX_L_OUTPUT].setVoltageSimd<float_4>(inL * fxGain * sendActive, c); | outputs[TOFX_L_OUTPUT].setVoltageSimd<float_4>(inL * fxGain * sendActive, c); | ||||
| outputs[TOFX_R_OUTPUT].setVoltageSimd<float_4>(inR * fxGain * sendActive, c); | outputs[TOFX_R_OUTPUT].setVoltageSimd<float_4>(inR * fxGain * sendActive, c); | ||||
| } | |||||
| // fx send polyphony is set by input polyphony | |||||
| outputs[TOFX_L_OUTPUT].setChannels(maxInputChannels); | |||||
| outputs[TOFX_R_OUTPUT].setChannels(maxInputChannels); | |||||
| float_4 dryLeft, dryRight; | |||||
| 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 = inputs[FROM_FX_L_INPUT].getPolyVoltageSimd<float_4>(c); | const float_4 fxLeftReturn = inputs[FROM_FX_L_INPUT].getPolyVoltageSimd<float_4>(c); | ||||
| const float_4 fxRightReturn = inputs[FROM_FX_R_INPUT].getPolyVoltageSimd<float_4>(c); | const float_4 fxRightReturn = inputs[FROM_FX_R_INPUT].getPolyVoltageSimd<float_4>(c); | ||||
| if (returnMode == ReturnMode::HARD_MODE) { | if (returnMode == ReturnMode::HARD_MODE) { | ||||
| outputs[OUT_L_OUTPUT].setVoltageSimd<float_4>(inL * (1 - sendActive) + sendActive * fxLeftReturn, c); | |||||
| outputs[OUT_R_OUTPUT].setVoltageSimd<float_4>(inR * (1 - sendActive) + sendActive * fxRightReturn, c); | |||||
| outputs[OUT_L_OUTPUT].setVoltageSimd<float_4>(dryLeft * (1 - sendActive) + sendActive * fxLeftReturn, c); | |||||
| outputs[OUT_R_OUTPUT].setVoltageSimd<float_4>(dryRight * (1 - sendActive) + sendActive * fxRightReturn, c); | |||||
| } | } | ||||
| else { | else { | ||||
| outputs[OUT_L_OUTPUT].setVoltageSimd<float_4>(inL * (1 - sendActive) + fxLeftReturn, c); | |||||
| outputs[OUT_R_OUTPUT].setVoltageSimd<float_4>(inR * (1 - sendActive) + fxRightReturn, c); | |||||
| outputs[OUT_L_OUTPUT].setVoltageSimd<float_4>(dryLeft * (1 - sendActive) + fxLeftReturn, c); | |||||
| outputs[OUT_R_OUTPUT].setVoltageSimd<float_4>(dryRight * (1 - sendActive) + fxRightReturn, c); | |||||
| } | } | ||||
| } | } | ||||
| // output polyphony is set by fx return polyphony | |||||
| outputs[OUT_L_OUTPUT].setChannels(maxFxReturnChannels); | |||||
| outputs[OUT_R_OUTPUT].setChannels(maxFxReturnChannels); | |||||
| outputs[OUT_R_OUTPUT].setVoltage(sendActive); | |||||
| lights[LAUNCH_LED].setSmoothBrightness(sendActive, args.sampleTime); | |||||
| } | |||||
| }; | |||||
| lights[LAUNCH_LED].setBrightness(sendActive); | |||||
| /** 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(9, 9)); | |||||
| } | |||||
| }; | |||||
| using BefacoRedLightButton = LightButton<BefacoButton, VeryLargeSimpleLight<TRedLight<app::ModuleLightWidget>>>; | |||||
| 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 { | struct BypassWidget : ModuleWidget { | ||||
| @@ -142,7 +203,7 @@ struct BypassWidget : ModuleWidget { | |||||
| addParam(createParamCentered<BefacoTinyKnobWhite>(mm2px(Vec(10.0, 78.903)), module, Bypass::FX_GAIN_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)); | addParam(createParam<CKSSNarrow>(mm2px(Vec(13.8, 91.6)), module, Bypass::LAUNCH_MODE_PARAM)); | ||||
| launchParam = createLightParamCentered<BefacoRedLightButton>(mm2px(Vec(10.0, 111.287)), module, Bypass::LAUNCH_BUTTON_PARAM, Bypass::LAUNCH_LED); | |||||
| launchParam = createLightParamCentered<RecordButton>(mm2px(Vec(10.0, 111.287)), module, Bypass::LAUNCH_BUTTON_PARAM, Bypass::LAUNCH_LED); | |||||
| addParam(launchParam); | addParam(launchParam); | ||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.016, 15.03)), module, Bypass::IN_R_INPUT)); | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.016, 15.03)), module, Bypass::IN_R_INPUT)); | ||||
| @@ -157,16 +218,22 @@ struct BypassWidget : ModuleWidget { | |||||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(14.957, 53.824)), module, Bypass::OUT_R_OUTPUT)); | addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(14.957, 53.824)), module, Bypass::OUT_R_OUTPUT)); | ||||
| } | } | ||||
| void draw(const DrawArgs& args) override { | |||||
| // 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); | Bypass* module = dynamic_cast<Bypass*>(this->module); | ||||
| assert(module); | |||||
| if (module != nullptr) { | |||||
| launchParam->momentary = module->latchMode == Bypass::LatchMode::MOMENTARY_MODE; | |||||
| launchParam->latch = !launchParam->momentary; | |||||
| } | |||||
| menu->addChild(new MenuSeparator()); | |||||
| menu->addChild(new SlewTimeSider(module->slewTimeParam)); | |||||
| ModuleWidget::draw(args); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -32,4 +32,5 @@ void init(rack::Plugin *p) { | |||||
| p->addModel(modelVoltio); | p->addModel(modelVoltio); | ||||
| p->addModel(modelOctaves); | p->addModel(modelOctaves); | ||||
| p->addModel(modelBypass); | p->addModel(modelBypass); | ||||
| p->addModel(modelBandit); | |||||
| } | } | ||||
| @@ -33,6 +33,7 @@ extern Model* modelMidiThing; | |||||
| extern Model* modelVoltio; | extern Model* modelVoltio; | ||||
| extern Model* modelOctaves; | extern Model* modelOctaves; | ||||
| extern Model* modelBypass; | extern Model* modelBypass; | ||||
| extern Model* modelBandit; | |||||
| struct Knurlie : SvgScrew { | struct Knurlie : SvgScrew { | ||||
| Knurlie() { | Knurlie() { | ||||