//************************************************************************************** // //BPM Clock module for VCV Rack by Alfredo Santamaria - AS - https://github.com/AScustomWorks/AS // //Based on code taken from Master Clock Module VCV Module Strum 2017 https://github.com/Strum/Strums_Mental_VCV_Modules //************************************************************************************** #include "AS.hpp" #include "dsp/digital.hpp" #include #include struct LFOGenerator { float phase = 0.0f; float pw = 0.5f; float freq = 1.0f; SchmittTrigger resetTrigger; void setFreq(float freq_to_set) { freq = freq_to_set; } void step(float dt) { float deltaPhase = fminf(freq * dt, 0.5f); phase += deltaPhase; if (phase >= 1.0f) phase -= 1.0f; } void setReset(float reset) { if (resetTrigger.process(reset)) { phase = 0.0f; } } float sqr() { float sqr = phase < pw ? 1.0f : -1.0f; return sqr; } }; struct BPMClock : Module { enum ParamIds { TEMPO_PARAM, MODE_PARAM, TIMESIGTOP_PARAM, TIMESIGBOTTOM_PARAM, RESET_SWITCH, RUN_SWITCH, NUM_PARAMS }; enum InputIds { RUN_CV, RESET_INPUT, NUM_INPUTS }; enum OutputIds { BEAT_OUT, EIGHTHS_OUT, SIXTEENTHS_OUT, BAR_OUT, RESET_OUTPUT, RUN_OUTPUT, NUM_OUTPUTS }; enum LightIds { RESET_LED, RUN_LED, NUM_LIGHTS }; LFOGenerator clock; SchmittTrigger eighths_trig; SchmittTrigger quarters_trig; SchmittTrigger bars_trig; SchmittTrigger run_button_trig; SchmittTrigger ext_run_trig; SchmittTrigger reset_btn_trig; SchmittTrigger reset_ext_trig; SchmittTrigger bpm_mode_trig; PulseGenerator resetPulse; bool reset_pulse = false; PulseGenerator runPulse; bool run_pulse = false; // PULSES FOR TRIGGER OUTPUTS INSTEAD OF GATES PulseGenerator clockPulse8s; bool pulse8s = false; PulseGenerator clockPulse4s; bool pulse4s = false; PulseGenerator clockPulse1s; bool pulse1s = false; PulseGenerator clockPulse16s; bool pulse16s = false; float trigger_length = 0.0001f; const float lightLambda = 0.075f; float resetLight = 0.0f; bool running = true; int eighths_count = 0; int quarters_count = 0; int bars_count = 0; float tempo =120.0f; int time_sig_top, time_sig_bottom = 0; int time_sig_bottom_old = 0; float frequency = 2.0f; int quarters_count_limit = 4; int eighths_count_limit = 2; int bars_count_limit = 16; BPMClock() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} void step() override; json_t *toJson() override{ json_t *rootJ = json_object(); json_t *button_statesJ = json_array(); json_t *button_stateJ = json_integer((int)running); json_array_append_new(button_statesJ, button_stateJ); json_object_set_new(rootJ, "run", button_statesJ); return rootJ; } void fromJson(json_t *rootJ) override{ json_t *button_statesJ = json_object_get(rootJ, "run"); if (button_statesJ){ json_t *button_stateJ = json_array_get(button_statesJ,0); if (button_stateJ) running = !!json_integer_value(button_stateJ); } } }; void BPMClock::step() { if (run_button_trig.process(params[RUN_SWITCH].value) || ext_run_trig.process(inputs[RUN_CV].value)){ running = !running; runPulse.trigger(0.01f); } lights[RUN_LED].value = running ? 1.0f : 0.0f; run_pulse = runPulse.process(1.0 / engineGetSampleRate()); outputs[RUN_OUTPUT].value = (run_pulse ? 10.0f : 0.0f); if (params[MODE_PARAM].value){ //regular 40 to 250 bpm mode tempo = std::round(params[TEMPO_PARAM].value); }else{ //extended 30 to 300 mode tempo = std::round(rescale(params[TEMPO_PARAM].value,40.0f,250.0f, 30.0f, 300.0f) ); } //tempo = std::round(params[TEMPO_PARAM].value); time_sig_top = std::round(params[TIMESIGTOP_PARAM].value); time_sig_bottom = std::round(params[TIMESIGBOTTOM_PARAM].value); time_sig_bottom = std::pow(2,time_sig_bottom+1); frequency = tempo/60.0f; //RESET TRIGGER if(reset_ext_trig.process(inputs[RESET_INPUT].value) || reset_btn_trig.process(params[RESET_SWITCH].value)) { clock.setReset(1.0f); eighths_count = 0; quarters_count = 0; bars_count = 0; resetLight = 1.0; resetPulse.trigger(0.01f); } resetLight -= resetLight / lightLambda / engineGetSampleRate(); lights[RESET_LED].value = resetLight; reset_pulse = resetPulse.process(1.0 / engineGetSampleRate()); outputs[RESET_OUTPUT].value = (reset_pulse ? 10.0f : 0.0f); if(!running){ eighths_count = 0; quarters_count = 0; bars_count = 0; outputs[BAR_OUT].value = 0.0f; outputs[BEAT_OUT].value = 0.0f; outputs[EIGHTHS_OUT].value = 0.0f; outputs[SIXTEENTHS_OUT].value = 0.0f; }else{ if (time_sig_top == time_sig_bottom){ quarters_count_limit = 4; eighths_count_limit = 2; bars_count_limit = 16; clock.setFreq(frequency*4); }else{ if(time_sig_bottom == 4){ quarters_count_limit = 4; eighths_count_limit = 2; bars_count_limit = time_sig_top * 4; clock.setFreq(frequency*4); } if(time_sig_bottom == 8){ quarters_count_limit = 4; eighths_count_limit = 2; bars_count_limit = time_sig_top * 2; clock.setFreq(frequency*4); } if((time_sig_top % 3) == 0){ quarters_count_limit = 6; eighths_count_limit = 2; bars_count_limit = (time_sig_top/3) * 6; clock.setFreq(frequency*6); } } } if(running){ clock.step(1.0 / engineGetSampleRate()); //16ths float clock16s = clamp(10.0f * clock.sqr(), 0.0f, 10.0f); if(clock16s>0){ clockPulse16s.trigger(trigger_length); } //8ths if (eighths_trig.process(clock.sqr()) && eighths_count <= eighths_count_limit){ eighths_count++; } if (eighths_count >= eighths_count_limit){ eighths_count = 0; } if(eighths_count == 0){ clockPulse8s.trigger(trigger_length); } //4ths if (quarters_trig.process(clock.sqr()) && quarters_count <= quarters_count_limit){ quarters_count++; } if (quarters_count >= quarters_count_limit){ quarters_count = 0; } if(quarters_count == 0){ clockPulse4s.trigger(trigger_length); } //bars if (bars_trig.process(clock.sqr()) && bars_count <= bars_count_limit){ bars_count++; } if (bars_count >= bars_count_limit){ bars_count = 0; } if(bars_count == 0){ clockPulse1s.trigger(trigger_length); } } pulse1s = clockPulse1s.process(1.0 / engineGetSampleRate()); pulse4s = clockPulse4s.process(1.0 / engineGetSampleRate()); pulse8s = clockPulse8s.process(1.0 / engineGetSampleRate()); pulse16s = clockPulse16s.process(1.0 / engineGetSampleRate()); outputs[BAR_OUT].value = (pulse1s ? 10.0f : 0.0f); outputs[BEAT_OUT].value = (pulse4s ? 10.0f : 0.0f); outputs[EIGHTHS_OUT].value = (pulse8s ? 10.0f : 0.0f); outputs[SIXTEENTHS_OUT].value = (pulse16s ? 10.0f : 0.0f); } //////////////////////////////////// struct BpmDisplayWidget : TransparentWidget { float *value; std::shared_ptr font; BpmDisplayWidget() { font = Font::load(assetPlugin(plugin, "res/Segment7Standard.ttf")); }; void draw(NVGcontext *vg) override { // Background //NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20); 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::setw(3) << *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 SigDisplayWidget : TransparentWidget { int *value; std::shared_ptr font; SigDisplayWidget() { font = Font::load(assetPlugin(plugin, "res/Segment7Standard.ttf")); }; void draw(NVGcontext *vg) override { // Background //NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20); 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.0); nvgStrokeColor(vg, borderColor); nvgStroke(vg); // text nvgFontSize(vg, 18); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, 2.5); std::stringstream to_display; to_display << std::setw(2) << *value; Vec textPos = Vec(3.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 BPMClockWidget : ModuleWidget { BPMClockWidget(BPMClock *module); }; BPMClockWidget::BPMClockWidget(BPMClock *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/BPMClock.svg"))); //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))); //BPM DISPLAY BpmDisplayWidget *display = new BpmDisplayWidget(); display->box.pos = Vec(23,45); display->box.size = Vec(45, 20); display->value = &module->tempo; addChild(display); //TEMPO KNOB addParam(ParamWidget::create(Vec(8, 69), module, BPMClock::TEMPO_PARAM, 40.0f, 250.0f, 120.0f)); //OLD/NEW SWITCH FROM 40-250 TO 30-300 addParam(ParamWidget::create(Vec(67, 77), module, BPMClock::MODE_PARAM, 0.0f, 1.0f, 1.0f)); //SIG TOP DISPLAY SigDisplayWidget *display2 = new SigDisplayWidget(); display2->box.pos = Vec(54,123); display2->box.size = Vec(30, 20); display2->value = &module->time_sig_top; addChild(display2); //SIG TOP KNOB addParam(ParamWidget::create(Vec(8, 110), module, BPMClock::TIMESIGTOP_PARAM,2.0f, 15.0f, 4.0f)); //SIG BOTTOM DISPLAY SigDisplayWidget *display3 = new SigDisplayWidget(); display3->box.pos = Vec(54,155); display3->box.size = Vec(30, 20); display3->value = &module->time_sig_bottom; addChild(display3); //SIG BOTTOM KNOB addParam(ParamWidget::create(Vec(8, 150), module, BPMClock::TIMESIGBOTTOM_PARAM,0.0f, 3.0f, 1.0f)); //RESET & RUN LEDS /* addParam(ParamWidget::create(Vec(60.5, 202), module, BPMClock::RUN_SWITCH , 0.0f, 1.0f, 0.0f)); addChild(ModuleLightWidget::create>(Vec(62.7, 204.3), module, BPMClock::RUN_LED)); */ addParam(ParamWidget::create(Vec(33.5, 202), module, BPMClock::RUN_SWITCH , 0.0f, 1.0f, 0.0f)); addChild(ModuleLightWidget::create>(Vec(35.7, 204.3), module, BPMClock::RUN_LED)); addParam(ParamWidget::create(Vec(33.5, 241), module, BPMClock::RESET_SWITCH , 0.0f, 1.0f, 0.0f)); addChild(ModuleLightWidget::create>(Vec(35.7, 243.2), module, BPMClock::RESET_LED)); //RESET INPUT addInput(Port::create(Vec(6, 240), Port::INPUT, module, BPMClock::RESET_INPUT)); //RESET OUTPUT addOutput(Port::create(Vec(59, 240), Port::OUTPUT, module, BPMClock::RESET_OUTPUT)); //TEMPO OUTPUTS addOutput(Port::create(Vec(6, 280), Port::OUTPUT, module, BPMClock::BAR_OUT)); addOutput(Port::create(Vec(59, 280), Port::OUTPUT, module, BPMClock::BEAT_OUT)); addOutput(Port::create(Vec(6, 320), Port::OUTPUT, module, BPMClock::EIGHTHS_OUT)); addOutput(Port::create(Vec(59, 320), Port::OUTPUT, module, BPMClock::SIXTEENTHS_OUT)); //RUN CV addInput(Port::create(Vec(6, 200), Port::INPUT, module, BPMClock::RUN_CV)); //RUN TRIGGER OUTPUT addOutput(Port::create(Vec(59, 200), Port::OUTPUT, module, BPMClock::RUN_OUTPUT)); } RACK_PLUGIN_MODEL_INIT(AS, BPMClock) { Model *modelBPMClock = Model::create("AS", "BPMClock", "BPM Clock", CLOCK_TAG); return modelBPMClock; }