//************************************************************************************** //Delay Plus module for VCV Rack by Alfredo Santamaria - AS - https://github.com/AScustomWorks/AS // //Code taken from the Fundamentals plugins by Andrew Belt http://www.vcvrack.com //************************************************************************************** #include "AS.hpp" #include "dsp/samplerate.hpp" #include "dsp/ringbuffer.hpp" #include "dsp/filter.hpp" #include "dsp/digital.hpp" #include #include namespace rack_plugin_AS { #define HISTORY_SIZE (1<<21) struct DelayPlusStereoFx : Module { enum ParamIds { TIME_PARAM_1, FEEDBACK_PARAM_1, COLOR_PARAM_1, TIME_PARAM_2, FEEDBACK_PARAM_2, COLOR_PARAM_2, FBK_LINK_PARAM, COLOR_LINK_PARAM, MIX_PARAM, BYPASS_SWITCH, NUM_PARAMS }; enum InputIds { TIME_CV_INPUT_1, FEEDBACK__CV_INPUT_1, COLOR_CV_INPUT_1, COLOR_RETURN_1, TIME_CV_INPUT_2, FEEDBACK__CV_INPUT_2, COLOR_CV_INPUT_2, COLOR_RETURN_2, MIX_CV_INPUT, SIGNAL_INPUT_1, SIGNAL_INPUT_2, BYPASS_CV_INPUT, NUM_INPUTS }; enum OutputIds { COLOR_SEND_1, COLOR_SEND_2, SIGNAL_OUTPUT_1, SIGNAL_OUTPUT_2, NUM_OUTPUTS }; enum LightIds { BYPASS_LED, NUM_LIGHTS }; RCFilter lowpassFilter1; RCFilter highpassFilter1; RCFilter lowpassFilter2; RCFilter highpassFilter2; DoubleRingBuffer historyBuffer1; DoubleRingBuffer outBuffer1; DoubleRingBuffer historyBuffer2; DoubleRingBuffer outBuffer2; SampleRateConverter<1> src1; SampleRateConverter<1> src2; SchmittTrigger bypass_button_trig; SchmittTrigger bypass_cv_trig; int lcd_tempo1 = 0; int lcd_tempo2 = 0; bool fx_bypass = false; float lastWet1 = 0.0f; float lastWet2 = 0.0f; float feedback1 = 0.0f; float feedback2 = 0.0f; float color1 = 0.0f; float color2 = 0.0f; float mix_value = 0.0f; float fade_in_fx = 0.0f; float fade_in_dry = 0.0f; float fade_out_fx = 1.0f; float fade_out_dry = 1.0f; const float fade_speed = 0.001f; float signal_input_1 = 0.0f; float signal_input_2 = 0.0f; DelayPlusStereoFx() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { } void step() override; json_t *toJson()override { json_t *rootJm = json_object(); json_t *statesJ = json_array(); json_t *bypassJ = json_boolean(fx_bypass); json_array_append_new(statesJ, bypassJ); json_object_set_new(rootJm, "as_FxBypass", statesJ); return rootJm; } void fromJson(json_t *rootJm)override { json_t *statesJ = json_object_get(rootJm, "as_FxBypass"); json_t *bypassJ = json_array_get(statesJ, 0); fx_bypass = !!json_boolean_value(bypassJ); } void resetFades(){ fade_in_fx = 0.0f; fade_in_dry = 0.0f; fade_out_fx = 1.0f; fade_out_dry = 1.0f; } }; void DelayPlusStereoFx::step() { if (bypass_button_trig.process(params[BYPASS_SWITCH].value) || bypass_cv_trig.process(inputs[BYPASS_CV_INPUT].value) ){ fx_bypass = !fx_bypass; resetFades(); } lights[BYPASS_LED].value = fx_bypass ? 1.0f : 0.0f; signal_input_1 = inputs[SIGNAL_INPUT_1].value; //check for MONO/STEREO CONNECTIONS if(!inputs[SIGNAL_INPUT_2].active){ signal_input_2 = inputs[SIGNAL_INPUT_1].value; }else{ signal_input_2 = inputs[SIGNAL_INPUT_2].value; } // DELAY L - Feed input to delay block feedback1 = clamp(params[FEEDBACK_PARAM_1].value + inputs[FEEDBACK__CV_INPUT_1].value / 10.0f, 0.0f, 1.0f); float dry1 = signal_input_1 + lastWet1 * feedback1; // Compute delay time in seconds //float delay = 1e-3 * powf(10.0 / 1e-3, clampf(params[TIME_PARAM_1].value + inputs[TIME_CV_INPUT_1].value / 10.0, 0.0, 1.0)); float delay1 = clamp(params[TIME_PARAM_1].value + inputs[TIME_CV_INPUT_1].value, 0.001f, 10.0f); //LCD display tempo - show value as ms lcd_tempo1 = std::round(delay1*1000); // Number of delay samples float index1 = delay1 * engineGetSampleRate(); // Push dry sample into history buffer if (!historyBuffer1.full()) { historyBuffer1.push(dry1); } // How many samples do we need consume to catch up? float consume1 = index1 - historyBuffer1.size(); if (outBuffer1.empty()) { double ratio1 = 1.0; if (consume1 <= -16) ratio1 = 0.5; else if (consume1 >= 16) ratio1 = 2.0; float inSR1 = engineGetSampleRate(); float outSR1 = ratio1 * inSR1; int inFrames1 = min(historyBuffer1.size(), 16); int outFrames1 = outBuffer1.capacity(); src1.setRates(inSR1, outSR1); src1.process((const Frame<1>*)historyBuffer1.startData(), &inFrames1, (Frame<1>*)outBuffer1.endData(), &outFrames1); historyBuffer1.startIncr(inFrames1); outBuffer1.endIncr(outFrames1); } float out1; float wet1 = 0.0f; if (!outBuffer1.empty()) { wet1 = outBuffer1.shift(); } if (!outputs[COLOR_SEND_1].active) { //internal color // Apply color to delay wet output color1 = clamp(params[COLOR_PARAM_1].value + inputs[COLOR_CV_INPUT_1].value / 10.0f, 0.0f, 1.0f); float lowpassFreq1 = 10000.0f * powf(10.0f, clamp(2.0*color1, 0.0f, 1.0f)); lowpassFilter1.setCutoff(lowpassFreq1 / engineGetSampleRate()); lowpassFilter1.process(wet1); wet1 = lowpassFilter1.lowpass(); float highpassFreq1 = 10.0f * powf(100.0f, clamp(2.0f*color1 - 1.0f, 0.0f, 1.0f)); highpassFilter1.setCutoff(highpassFreq1 / engineGetSampleRate()); highpassFilter1.process(wet1); wet1 = highpassFilter1.highpass(); }else { //external color, to filter the wet delay signal outside of the module, or to feed another module outputs[COLOR_SEND_1].value = wet1; wet1 = inputs[COLOR_RETURN_1].value; } lastWet1 = wet1; // end of delay 1 block // DELAY R - Feed input to delay block //CHECK IF LINK IS ENABLED FOR FEEDBACK KNOBS if(params[FBK_LINK_PARAM].value){ feedback2 = feedback1; }else{ feedback2 = clamp(params[FEEDBACK_PARAM_2].value + inputs[FEEDBACK__CV_INPUT_2].value / 10.0f, 0.0f, 1.0f); } float dry2 = signal_input_2 + lastWet2 * feedback2; // Compute delay time in seconds //float delay = 1e-3 * powf(10.0 / 1e-3, clampf(params[TIME_PARAM_1].value + inputs[TIME_CV_INPUT_1].value / 10.0, 0.0, 1.0)); float delay2 = clamp(params[TIME_PARAM_2].value + inputs[TIME_CV_INPUT_2].value, 0.001f, 10.0f); //LCD display tempo - show value as ms lcd_tempo2 = std::round(delay2*1000); // Number of delay samples float index2 = delay2 * engineGetSampleRate(); // Push dry sample into history buffer if (!historyBuffer2.full()) { historyBuffer2.push(dry2); } // How many samples do we need consume to catch up? float consume2 = index2 - historyBuffer2.size(); if (outBuffer2.empty()) { double ratio2 = 1.0; if (consume2 <= -16) ratio2 = 0.5; else if (consume2 >= 16) ratio2 = 2.0; float inSR2 = engineGetSampleRate(); float outSR2 = ratio2 * inSR2; int inFrames2 = min(historyBuffer2.size(), 16); int outFrames2 = outBuffer2.capacity(); src2.setRates(inSR2, outSR2); src2.process((const Frame<1>*)historyBuffer2.startData(), &inFrames2, (Frame<1>*)outBuffer2.endData(), &outFrames2); historyBuffer2.startIncr(inFrames2); outBuffer2.endIncr(outFrames2); } float out2; float wet2 = 0.0f; if (!outBuffer2.empty()) { wet2 = outBuffer2.shift(); } if (!outputs[COLOR_SEND_2].active) { //internal color // Apply color to delay wet output if ( params[COLOR_LINK_PARAM].value ) { color2 = color1; } else { color2 = clamp(params[COLOR_PARAM_2].value + inputs[COLOR_CV_INPUT_2].value / 10.0f, 0.0f, 1.0f); } float lowpassFreq2 = 10000.0f * powf(10.0f, clamp(2.0*color2, 0.0f, 1.0f)); lowpassFilter2.setCutoff(lowpassFreq2 / engineGetSampleRate()); lowpassFilter2.process(wet2); wet2 = lowpassFilter2.lowpass(); float highpassFreq2 = 10.0f * powf(100.0f, clamp(2.0f*color2 - 1.0f, 0.0f, 1.0f)); highpassFilter2.setCutoff(highpassFreq2 / engineGetSampleRate()); highpassFilter2.process(wet2); wet2 = highpassFilter2.highpass(); }else { //external color, to filter the wet delay signal outside of the module, or to feed another module outputs[COLOR_SEND_2].value = wet2; wet2 = inputs[COLOR_RETURN_2].value; } lastWet2 = wet2; //mix dry & wet signals mix_value = clamp(params[MIX_PARAM].value + inputs[MIX_CV_INPUT].value / 10.0f, 0.0f, 1.0f); out1 = crossfade(signal_input_1, wet1, mix_value); out2 = crossfade(signal_input_2, wet2, mix_value); //check bypass switch status if (fx_bypass){ fade_in_dry += fade_speed; if ( fade_in_dry > 1.0f ) { fade_in_dry = 1.0f; } fade_out_fx -= fade_speed; if ( fade_out_fx < 0.0f ) { fade_out_fx = 0.0f; } outputs[SIGNAL_OUTPUT_1].value = ( signal_input_1 * fade_in_dry ) + ( out1 * fade_out_fx ); outputs[SIGNAL_OUTPUT_2].value = ( signal_input_2 * fade_in_dry ) + ( out2 * fade_out_fx ); }else{ fade_in_fx += fade_speed; if ( fade_in_fx > 1.0f ) { fade_in_fx = 1.0f; } fade_out_dry -= fade_speed; if ( fade_out_dry < 0.0f ) { fade_out_dry = 0.0f; } outputs[SIGNAL_OUTPUT_1].value = ( signal_input_1 * fade_out_dry ) + ( out1 * fade_in_fx ); outputs[SIGNAL_OUTPUT_2].value = ( signal_input_2 * fade_out_dry ) + ( out2 * fade_in_fx ); } } /////////////////////////////////// struct MsDisplayWidget : TransparentWidget { int *value; std::shared_ptr font; MsDisplayWidget() { font = Font::load(assetPlugin(plugin, "res/Segment7Standard.ttf")); }; void draw(NVGcontext *vg) override { // Background NVGcolor backgroundColor = nvgRGB(0x20, 0x10, 0x10); NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10); nvgBeginPath(vg); nvgRoundedRect(vg, 0.0, 0.0, box.size.x, box.size.y, 4.0); nvgFillColor(vg, backgroundColor); nvgFill(vg); nvgStrokeWidth(vg, 1.5); nvgStrokeColor(vg, borderColor); nvgStroke(vg); // text nvgFontSize(vg, 18); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, 2.5); std::stringstream to_display; to_display << std::right << std::setw(5) << *value; Vec textPos = Vec(4.0f, 17.0f); NVGcolor textColor = nvgRGB(0xdf, 0xd2, 0x2c); nvgFillColor(vg, nvgTransRGBA(textColor, 16)); nvgText(vg, textPos.x, textPos.y, "~~~~~", NULL); textColor = nvgRGB(0xda, 0xe9, 0x29); nvgFillColor(vg, nvgTransRGBA(textColor, 16)); nvgText(vg, textPos.x, textPos.y, "\\\\\\\\\\", NULL); textColor = nvgRGB(0xf0, 0x00, 0x00); nvgFillColor(vg, textColor); nvgText(vg, textPos.x, textPos.y, to_display.str().c_str(), NULL); } }; //////////////////////////////////// struct DelayPlusStereoFxWidget : ModuleWidget { DelayPlusStereoFxWidget(DelayPlusStereoFx *module); }; DelayPlusStereoFxWidget::DelayPlusStereoFxWidget(DelayPlusStereoFx *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/DelayPlusStereo.svg"))); //MS DISPLAY L MsDisplayWidget *display1 = new MsDisplayWidget(); display1->box.pos = Vec(7,50); display1->box.size = Vec(70, 20); display1->value = &module->lcd_tempo1; addChild(display1); //MS DISPLAY R MsDisplayWidget *display2 = new MsDisplayWidget(); display2->box.pos = Vec(103,50); display2->box.size = Vec(70, 20); display2->value = &module->lcd_tempo2; addChild(display2); //SCREWS addChild(Widget::create(Vec(RACK_GRID_WIDTH, 0))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(Widget::create(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); //KNOBS L addParam(ParamWidget::create(Vec(37, 78), module, DelayPlusStereoFx::TIME_PARAM_1, 0.0f, 10.0f, 0.375f)); addParam(ParamWidget::create(Vec(37, 130), module, DelayPlusStereoFx::FEEDBACK_PARAM_1, 0.0f, 1.0f, 0.5f)); addParam(ParamWidget::create(Vec(37, 180), module, DelayPlusStereoFx::COLOR_PARAM_1, 0.0f, 1.0f, 0.5f)); //KNOBS R addParam(ParamWidget::create(Vec(106, 78), module, DelayPlusStereoFx::TIME_PARAM_2, 0.0f, 10.0f, 0.750f)); addParam(ParamWidget::create(Vec(106, 130), module, DelayPlusStereoFx::FEEDBACK_PARAM_2, 0.0f, 1.0f, 0.5f)); addParam(ParamWidget::create(Vec(106, 180), module, DelayPlusStereoFx::COLOR_PARAM_2, 0.0f, 1.0f, 0.5f)); //FEEDBACK LINK SWITCH addParam(ParamWidget::create(Vec(82, 145), module, DelayPlusStereoFx::FBK_LINK_PARAM, 0.0f, 1.0f, 0.0f)); //COLOR LINK SWITCH addParam(ParamWidget::create(Vec(82, 195), module, DelayPlusStereoFx::COLOR_LINK_PARAM, 0.0f, 1.0f, 0.0f)); //MIX KNOB addParam(ParamWidget::create(Vec(71, 251), module, DelayPlusStereoFx::MIX_PARAM, 0.0f, 1.0f, 0.5f)); //BYPASS SWITCH addParam(ParamWidget::create(Vec(79, 292), module, DelayPlusStereoFx::BYPASS_SWITCH , 0.0f, 1.0f, 0.0f)); addChild(ModuleLightWidget::create>(Vec(79+2.2, 294), module, DelayPlusStereoFx::BYPASS_LED)); //INPUTS CV L addInput(Port::create(Vec(7, 87), Port::INPUT, module, DelayPlusStereoFx::TIME_CV_INPUT_1)); addInput(Port::create(Vec(7, 137), Port::INPUT, module, DelayPlusStereoFx::FEEDBACK__CV_INPUT_1)); addInput(Port::create(Vec(7, 187), Port::INPUT, module, DelayPlusStereoFx::COLOR_CV_INPUT_1)); //INPUTS CV R addInput(Port::create(Vec(150, 87), Port::INPUT, module, DelayPlusStereoFx::TIME_CV_INPUT_2)); addInput(Port::create(Vec(150, 137), Port::INPUT, module, DelayPlusStereoFx::FEEDBACK__CV_INPUT_2)); addInput(Port::create(Vec(150, 187), Port::INPUT, module, DelayPlusStereoFx::COLOR_CV_INPUT_2)); //DELAY SIGNAL SEND L addOutput(Port::create(Vec(15, 224), Port::OUTPUT, module, DelayPlusStereoFx::COLOR_SEND_1)); //DELAY SIGNAL RETURN L addInput(Port::create(Vec(50, 224), Port::INPUT, module, DelayPlusStereoFx::COLOR_RETURN_1)); //DELAY SIGNAL SEND R addOutput(Port::create(Vec(105, 224), Port::OUTPUT, module, DelayPlusStereoFx::COLOR_SEND_2)); //DELAY SIGNAL RETURN R addInput(Port::create(Vec(140, 224), Port::INPUT, module, DelayPlusStereoFx::COLOR_RETURN_2)); //MIX CV INPUT addInput(Port::create(Vec(40, 258), Port::INPUT, module, DelayPlusStereoFx::MIX_CV_INPUT)); //SIGNAL INPUT L addInput(Port::create(Vec(20, 300), Port::INPUT, module, DelayPlusStereoFx::SIGNAL_INPUT_1)); //SIGNAL INPUT R addInput(Port::create(Vec(20, 330), Port::INPUT, module, DelayPlusStereoFx::SIGNAL_INPUT_2)); //SIGNAL OUTPUT L addOutput(Port::create(Vec(135, 300), Port::OUTPUT, module, DelayPlusStereoFx::SIGNAL_OUTPUT_1)); //SIGNAL OUTPUT R addOutput(Port::create(Vec(135, 330), Port::OUTPUT, module, DelayPlusStereoFx::SIGNAL_OUTPUT_2)); //BYPASS CV INPUT addInput(Port::create(Vec(78, 322), Port::INPUT, module, DelayPlusStereoFx::BYPASS_CV_INPUT)); } } // namespace rack_plugin_AS using namespace rack_plugin_AS; RACK_PLUGIN_MODEL_INIT(AS, DelayPlusStereoFx) { Model *modelDelayPlusStereoFx = Model::create("AS", "DelayPlusStereoFx", "DelayPlus Stereo Fx", DUAL_TAG, DELAY_TAG, EFFECT_TAG); return modelDelayPlusStereoFx; }