//#include #include "FrozenWasteland.hpp" #include "dsp/digital.hpp" #define DIVISIONS 27 namespace rack_plugin_FrozenWasteland { struct BPMLFO2 : Module { enum ParamIds { DIVISION_PARAM, DIVISION_CV_ATTENUVERTER_PARAM, SKEW_PARAM, SKEW_CV_ATTENUVERTER_PARAM, OFFSET_PARAM, WAVESHAPE_PARAM, HOLD_CLOCK_BEHAVIOR_PARAM, HOLD_MODE_PARAM, NUM_PARAMS }; enum InputIds { CLOCK_INPUT, DIVISION_INPUT, SKEW_INPUT, RESET_INPUT, HOLD_INPUT, NUM_INPUTS }; enum OutputIds { LFO_OUTPUT, NUM_OUTPUTS }; enum LightIds { CLOCK_LIGHT, HOLD_LIGHT, NUM_LIGHTS }; enum Waveshapes { SKEWSAW_WAV, SQUARE_WAV }; struct LowFrequencyOscillator { float phase = 0.0; float freq = 1.0; float pw = 0.5; float skew = 0.5; // Triangle bool offset = false; void setPitch(float pitch) { pitch = fminf(pitch, 8.0); freq = powf(2.0, pitch); } void setFrequency(float frequency) { freq = frequency; } void setPulseWidth(float pw_) { const float pwMin = 0.01; pw = clamp(pw_, pwMin, 1.0f - pwMin); } void hardReset() { phase = 0.0; } void step(float dt) { float deltaPhase = fminf(freq * dt, 0.5); phase += deltaPhase; if (phase >= 1.0) phase -= 1.0; } float skewsaw(float x) { x = eucmod(x,1.0); float inverseSkew = 1 - skew; float result; if (skew == 0 && x == 0) //Avoid /0 error return 2; if (x <= skew) result = 2.0 * (1- (-1 / skew * x + 1)); else result = 2.0 * (1-(1 / inverseSkew * (x - skew))); return result; } float skewsaw() { if (offset) return skewsaw(phase); else return skewsaw(phase) - 1; //Going to keep same phase for now //return skewsaw(phase - .5) - 1; } float sqr() { float sqr = (phase < pw) ? 1.0 : -1.0; return offset ? sqr + 1.0 : sqr; } float progress() { return phase; } }; LowFrequencyOscillator oscillator; SchmittTrigger clockTrigger,resetTrigger,holdTrigger; float divisions[DIVISIONS] = {1/64.0f,1/32.0f,1/16.0f,1/13.0f,1/11.0f,1/8.0f,1/7.0f,1/6.0f,1/5.0f,1/4.0f,1/3.0f,1/2.0f,1/1.5f,1,1.5f,2,3,4,5,6,7,8,11,13,16,32,64}; const char* divisionNames[DIVISIONS] = {"/64","/32","/16","/13","/11","/8","/7","/6","/5","/4","/3","/2","/1.5","x 1","x 1.5","x 2","x 3","x 4","x 5","x 6","x 7","x 8","x 11","x 13","x 16","x 32","x 64"}; int division = 0; float time = 0.0; float duration = 0; float waveshape = 0; float skew = 0.5; bool holding = false; bool secondClockReceived = false; float lfoOutputValue = 0.0; BPMLFO2() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} void step() override; void reset() override { division = 0; } // For more advanced Module features, read Rack's engine.hpp header file // - toJson, fromJson: serialization of internal data // - onSampleRateChange: event triggered by a change of sample rate // - onReset, onRandomize, onCreate, onDelete: implements special behavior when user clicks these from the context menu }; void BPMLFO2::step() { time += 1.0 / engineGetSampleRate(); if(inputs[CLOCK_INPUT].active) { if(clockTrigger.process(inputs[CLOCK_INPUT].value)) { if(secondClockReceived) { duration = time; } time = 0; secondClockReceived = true; } lights[CLOCK_LIGHT].value = time > (duration/2.0); } float divisionf = params[DIVISION_PARAM].value; if(inputs[DIVISION_INPUT].active) { divisionf +=(inputs[DIVISION_INPUT].value * params[DIVISION_CV_ATTENUVERTER_PARAM].value * (DIVISIONS / 10.0)); } divisionf = clamp(divisionf,0.0f,26.0f); division = int(divisionf); waveshape = params[WAVESHAPE_PARAM].value; skew = params[SKEW_PARAM].value; if(inputs[SKEW_INPUT].active) { skew +=inputs[SKEW_INPUT].value / 10 * params[SKEW_CV_ATTENUVERTER_PARAM].value; } skew = clamp(skew,0.0f,1.0f); oscillator.offset = (params[OFFSET_PARAM].value > 0.0); oscillator.skew = skew; oscillator.setPulseWidth(skew); if(duration != 0) { oscillator.setFrequency(1.0 / (duration / divisions[division])); } else { oscillator.setFrequency(0); } if(inputs[RESET_INPUT].active) { if(resetTrigger.process(inputs[RESET_INPUT].value)) { oscillator.hardReset(); } } if(inputs[HOLD_INPUT].active) { if(params[HOLD_MODE_PARAM].value == 1.0) { //Latched is default if(holdTrigger.process(inputs[HOLD_INPUT].value)) { holding = !holding; } } else { holding = inputs[HOLD_INPUT].value >= 1; } lights[HOLD_LIGHT].value = holding; } if(!holding || (holding && params[HOLD_CLOCK_BEHAVIOR_PARAM].value == 0.0)) { oscillator.step(1.0 / engineGetSampleRate()); } if(!holding) { if(waveshape == SKEWSAW_WAV) lfoOutputValue = 5.0 * oscillator.skewsaw(); else lfoOutputValue = 5.0 * oscillator.sqr(); } outputs[LFO_OUTPUT].value = lfoOutputValue; } struct BPMLFO2ProgressDisplay : TransparentWidget { BPMLFO2 *module; int frame = 0; std::shared_ptr font; BPMLFO2ProgressDisplay() { font = Font::load(assetPlugin(plugin, "res/fonts/01 Digit.ttf")); } void drawProgress(NVGcontext *vg, int waveshape, float skew, float phase) { float inverseSkew = 1 - skew; float y; if (skew == 0 && phase == 0) //Avoid /0 error y = 72.0; if (phase <= skew) y = 72.0 * (1- (-1 / skew * phase + 1)); else y = 72.0 * (1-(1 / inverseSkew * (phase - skew))); // Draw indicator nvgFillColor(vg, nvgRGBA(0xff, 0xff, 0x20, 0xff)); if(waveshape == BPMLFO2::SKEWSAW_WAV) { nvgBeginPath(vg); nvgMoveTo(vg,68,213); if(phase > skew) { nvgLineTo(vg,68 + (skew * 68),141); } nvgLineTo(vg,68 + (phase * 68),213-y); nvgLineTo(vg,68 + (phase * 68),213); nvgClosePath(vg); nvgFill(vg); } else { float endpoint = min(phase,skew); nvgBeginPath(vg); nvgMoveTo(vg,68,177); nvgRect(vg,68,177,endpoint*68,36.0); //nvgLineTo(vg,68 + (endpoint * 68),213); //nvgLineTo(vg,68 + (endpoint * 68),177); //nvgLineTo(vg,68,177); nvgClosePath(vg); nvgFill(vg); if(phase > skew) { nvgBeginPath(vg); nvgMoveTo(vg,68 + (skew * 68),141); nvgRect(vg,68 + (skew * 68),141,(phase-skew)*68,36.0); //nvgLineTo(vg,68 + (phase * 68),177); //nvgLineTo(vg,68 + (phase * 68),141); //nvgMoveTo(vg,68 + (skew * 68),141); nvgClosePath(vg); nvgFill(vg); } } } void drawWaveShape(NVGcontext *vg, int waveshape, float skew) { // Draw wave shape nvgStrokeColor(vg, nvgRGBA(0xff, 0xff, 0x20, 0xff)); nvgStrokeWidth(vg, 2.0); if(waveshape == BPMLFO2::SKEWSAW_WAV) { nvgBeginPath(vg); nvgMoveTo(vg,68,213); nvgLineTo(vg,68 + (skew * 68),141); nvgLineTo(vg,136,213); nvgClosePath(vg); } else { nvgBeginPath(vg); nvgMoveTo(vg,68,213); nvgLineTo(vg,68 + (skew * 68),213); nvgLineTo(vg,68 + (skew * 68),141); nvgLineTo(vg,136,141); //nvgClosePath(vg); } nvgStroke(vg); } void drawDivision(NVGcontext *vg, Vec pos, int division) { nvgFontSize(vg, 28); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -2); nvgFillColor(vg, nvgRGBA(0x00, 0xff, 0x00, 0xff)); char text[128]; snprintf(text, sizeof(text), "%s", module->divisionNames[division]); nvgText(vg, pos.x + 52, pos.y, text, NULL); } void draw(NVGcontext *vg) override { drawWaveShape(vg,module->waveshape, module->skew); drawProgress(vg,module->waveshape, module->skew, module->oscillator.progress()); drawDivision(vg, Vec(0, box.size.y - 153), module->division); } }; struct BPMLFO2Widget : ModuleWidget { BPMLFO2Widget(BPMLFO2 *module); }; BPMLFO2Widget::BPMLFO2Widget(BPMLFO2 *module) : ModuleWidget(module) { box.size = Vec(15*10, RACK_GRID_HEIGHT); { SVGPanel *panel = new SVGPanel(); panel->box.size = box.size; panel->setBackground(SVG::load(assetPlugin(plugin, "res/BPMLFO2.svg"))); addChild(panel); } addChild(Widget::create(Vec(RACK_GRID_WIDTH - 12, 0))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH + 12, 0))); addChild(Widget::create(Vec(RACK_GRID_WIDTH - 12, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH + 12, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); { BPMLFO2ProgressDisplay *display = new BPMLFO2ProgressDisplay(); display->module = module; display->box.pos = Vec(0, 0); display->box.size = Vec(box.size.x, 220); addChild(display); } addParam(ParamWidget::create(Vec(75, 78), module, BPMLFO2::DIVISION_PARAM, 0.0, 26.5, 13.0)); addParam(ParamWidget::create(Vec(40, 109), module, BPMLFO2::DIVISION_CV_ATTENUVERTER_PARAM, -1.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(14, 140), module, BPMLFO2::SKEW_PARAM, 0.0, 1.0, 0.5)); addParam(ParamWidget::create(Vec(17, 200), module, BPMLFO2::SKEW_CV_ATTENUVERTER_PARAM, -1.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(12, 240), module, BPMLFO2::OFFSET_PARAM, 0.0, 1.0, 1.0)); addParam(ParamWidget::create(Vec(42, 240), module, BPMLFO2::WAVESHAPE_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(82, 240), module, BPMLFO2::HOLD_CLOCK_BEHAVIOR_PARAM, 0.0, 1.0, 1.0)); addParam(ParamWidget::create(Vec(125, 240), module, BPMLFO2::HOLD_MODE_PARAM, 0.0, 1.0, 1.0)); addInput(Port::create(Vec(40, 81), Port::INPUT, module, BPMLFO2::DIVISION_INPUT)); addInput(Port::create(Vec(16, 172), Port::INPUT, module, BPMLFO2::SKEW_INPUT)); addInput(Port::create(Vec(31, 290), Port::INPUT, module, BPMLFO2::CLOCK_INPUT)); addInput(Port::create(Vec(62, 290), Port::INPUT, module, BPMLFO2::RESET_INPUT)); addInput(Port::create(Vec(94, 290), Port::INPUT, module, BPMLFO2::HOLD_INPUT)); addOutput(Port::create(Vec(63, 336), Port::OUTPUT, module, BPMLFO2::LFO_OUTPUT)); addChild(ModuleLightWidget::create>(Vec(12, 294), module, BPMLFO2::CLOCK_LIGHT)); addChild(ModuleLightWidget::create>(Vec(122, 294), module, BPMLFO2::HOLD_LIGHT)); } } // namespace rack_plugin_FrozenWasteland using namespace rack_plugin_FrozenWasteland; RACK_PLUGIN_MODEL_INIT(FrozenWasteland, BPMLFO2) { Model *modelBPMLFO2 = Model::create("Frozen Wasteland", "BPMLFO2", "BPM LFO 2", LFO_TAG); return modelBPMLFO2; }