/* COMPAIR Dual Window Comparator (inspired by Joranalogue Compare2) checks if an input voltage is between two values. compare window is set by two parameters: position and width. top knob on each channel (position) sets a center voltage of +/-5V. the other one changes the width around the center from near zero to 10V. so with both knobs at center the window is set at [-2.5..2.5]. both parameters are also controllable via cv input. cv and knob values are added together when computing the window. whenever the input signal is within that window the GATE output on that channel will go from 0V to +5V and the NOT output will do the opposite. logic outputs at the bottom compare the output values of both channels. AND output goes high if A is high and B is high OR output goes high if A or B is high XOR output goes high if either A or B ar high and the other one is low. FLIP output changes whenever the XOR out goes high. channel B inputs are normalized to channel A inputs. channel output to the logic section can be inverted. all outputs are 0V-5V. TODO: -proper RGB Light... -code clean-up, optimization, simplification *///////////////////////////////////////////////////////////////////////////// #include "pvc.hpp" #include "dsp/digital.hpp" namespace rack_plugin_PvC { struct Compair : Module { enum ParamIds { POS_A_PARAM, WIDTH_A_PARAM, POS_B_PARAM, WIDTH_B_PARAM, INVERT_A_PARAM, INVERT_B_PARAM, NUM_PARAMS }; enum InputIds { AUDIO_A_IN, POS_A_IN, WIDTH_A_IN, AUDIO_B_IN, POS_B_IN, WIDTH_B_IN, NUM_INPUTS }; enum OutputIds { GATE_A_OUT, NOT_A_OUT, GATE_B_OUT, NOT_B_OUT, AND_OUT, OR_OUT, XOR_OUT, FLIP_OUT, NUM_OUTPUTS }; enum LightIds { GATE_A_LED, GATE_B_LED, AND_LED, OR_LED, XOR_LED, FLIP_LED, OVER_A_LED, BELOW_A_LED, OVER_B_LED, BELOW_B_LED, NUM_LIGHTS }; bool outA = false; bool outB = false; bool flip = false; SchmittTrigger flipTrigger; enum OutputMode { ORIGINAL, BIPOLAR // INV_BIPOLAR, // INV_ORIGINAL }; OutputMode outputMode = ORIGINAL; Compair() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { // reset(); } void step() override; void reset() override { outA = false; outB = false; flip = false; outputMode = ORIGINAL; } // void randomize() override; json_t *toJson() override { json_t *rootJ = json_object(); // outputMode json_t *outputModeJ = json_integer((int) outputMode); json_object_set_new(rootJ, "outputMode", outputModeJ); return rootJ; } void fromJson(json_t *rootJ) override { // outputMode json_t *outputModeJ = json_object_get(rootJ, "outputMode"); if (outputModeJ) outputMode = (OutputMode)json_integer_value(outputModeJ); } }; void Compair::step(){ // get inputs and normalize B to A float inputA = inputs[AUDIO_A_IN].value; float inputB = inputs[AUDIO_B_IN].normalize(inputA); // get knob values float posA = params[POS_A_PARAM].value; float widthA = params[WIDTH_A_PARAM].value; float posB = params[POS_B_PARAM].value; float widthB = params[WIDTH_B_PARAM].value;; // testing bi-polar outputs float highVal = 5.0f; //float lowVal = 0.0f; float lowVal = outputMode == BIPOLAR ? -5.0f : 0.0f; // channel A CV inputs to knob values if (inputs[POS_A_IN].active) posA = (params[POS_A_PARAM].value + inputs[POS_A_IN].value); if (inputs[WIDTH_A_IN].active) widthA = (params[WIDTH_A_PARAM].value + inputs[WIDTH_A_IN].value); // compute window A float upperThreshA = posA + widthA*0.5f; float lowerThreshA = posA - widthA*0.5f; // check if input A is in window A outA = (inputA <= upperThreshA && inputA >= lowerThreshA) ? true : false; // channel B CV inputs to knob values and normalization if (inputs[POS_B_IN].active || inputs[POS_A_IN].active) posB = (params[POS_B_PARAM].value + inputs[POS_B_IN].normalize(inputs[POS_A_IN].value)); if (inputs[WIDTH_B_IN].active || inputs[WIDTH_B_IN].active) widthB = (params[WIDTH_B_PARAM].value + inputs[WIDTH_B_IN].normalize(inputs[WIDTH_A_IN].value)); // compute window B float upperThreshB = posB + widthB*0.5f; float lowerThreshB = posB - widthB*0.5f; // check if input B is in window B outB = (inputB <= upperThreshB && inputB >= lowerThreshB) ? true : false; // Gate/Not outputs and lights outputs[GATE_A_OUT].value = outA ? highVal : lowVal; outputs[NOT_A_OUT].value = !outA ? highVal : lowVal; lights[GATE_A_LED].setBrightness( outA ? 0.9f : (0.25f - clamp( fabsf(inputA-posA) * 0.025f, 0.0f, 0.4f) ) ); lights[OVER_A_LED].setBrightness( (inputA > upperThreshA) ? (inputA - upperThreshA)*0.1f + 0.4f : 0.0f ); lights[BELOW_A_LED].setBrightness( (inputA < lowerThreshA) ? (lowerThreshA - inputA)*0.1f + 0.4f : 0.0f ); outputs[GATE_B_OUT].value = outB ? highVal : lowVal; outputs[NOT_B_OUT].value = !outB ? highVal : lowVal; lights[GATE_B_LED].setBrightness( outB ? 0.9f : (0.25f - clamp( fabsf(inputB-posB) * 0.025f, 0.0f, 0.4f) ) ); lights[OVER_B_LED].setBrightness( (inputB > upperThreshB) ? (inputB - upperThreshB)*0.1f + 0.4f : 0.0f ); lights[BELOW_B_LED].setBrightness( (inputB < lowerThreshB) ? (lowerThreshB - inputB)*0.1f + 0.4f : 0.0f ); // logic input inverts if (params[INVERT_A_PARAM].value) outA = !outA; if (params[INVERT_B_PARAM].value) outB = !outB; // logic outputs and lights outputs[AND_OUT].value = (outA && outB) ? highVal : lowVal; lights[AND_LED].setBrightness( (outA && outB) ); outputs[OR_OUT].value = (outA || outB) ? highVal : lowVal; lights[OR_LED].setBrightness( (outA || outB) ); outputs[XOR_OUT].value = (outA != outB) ? highVal : lowVal; lights[XOR_LED].setBrightness( (outA != outB) ); if (flipTrigger.process(outputs[XOR_OUT].value)) // trigger the FlipFlop flip = !flip; outputs[FLIP_OUT].value = flip ? highVal : lowVal; lights[FLIP_LED].setBrightness( flip ); } // struct CompairToggle : SVGSwitch, ToggleSwitch { CompairToggle() { addFrame(SVG::load(assetPlugin(plugin, "res/components/CompairToggleUp.svg"))); addFrame(SVG::load(assetPlugin(plugin, "res/components/CompairToggleDn.svg"))); } }; // backdrop for the compare LEDs struct CompairLightBg : SVGScrew { CompairLightBg() { sw->svg = SVG::load(assetPlugin(plugin, "res/components/CompairLightBg.svg")); sw->wrap(); box.size = Vec(22,22); } }; // LEDs template struct CompairLight : BASE { CompairLight() { this->box.size = Vec(22, 22); this->bgColor = nvgRGBA(0x00, 0x00, 0x00, 0x00); } }; struct CompairWidget : ModuleWidget { CompairWidget(Compair *module); Menu *createContextMenu() override; }; CompairWidget::CompairWidget(Compair *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/panels/Compair.svg"))); // SCREWS addChild(Widget::create(Vec(15, 0))); //addChild(Widget::create(Vec(box.size.x - 15, 0))); addChild(Widget::create(Vec(15, 365))); //addChild(Widget::create(Vec(box.size.x - 30, 365))); // A Side addInput(Port::create(Vec(4,22), Port::INPUT, module,Compair::AUDIO_A_IN)); addInput(Port::create(Vec(34,22), Port::INPUT, module,Compair::AUDIO_B_IN)); addParam(ParamWidget::create(Vec(4,64), module, Compair::POS_A_PARAM, -5.0f , 5.0f, 0.0f)); addInput(Port::create(Vec(4,88), Port::INPUT, module,Compair::POS_A_IN)); addParam(ParamWidget::create(Vec(34,64), module, Compair::POS_B_PARAM, -5.0f, 5.0f, 0.0f)); addInput(Port::create(Vec(34,88), Port::INPUT, module,Compair::POS_B_IN)); addParam(ParamWidget::create(Vec(34,128), module, Compair::WIDTH_B_PARAM, 0.01f , 10.001f, 5.0f)); addInput(Port::create(Vec(34,152), Port::INPUT, module,Compair::WIDTH_B_IN)); addParam(ParamWidget::create(Vec(4,128), module, Compair::WIDTH_A_PARAM, 0.01f , 10.001f, 5.0f)); addInput(Port::create(Vec(4,152), Port::INPUT, module,Compair::WIDTH_A_IN)); addChild(Widget::create(Vec(4, 190))); addChild(ModuleLightWidget::create>(Vec(4,190),module,Compair::BELOW_A_LED)); addChild(ModuleLightWidget::create>(Vec(4,190),module,Compair::GATE_A_LED)); addChild(ModuleLightWidget::create>(Vec(4,190),module,Compair::OVER_A_LED)); addParam(ParamWidget::create(Vec(4,190),module,Compair::INVERT_A_PARAM, 0, 1, 0)); addChild(Widget::create(Vec(34, 190))); addChild(ModuleLightWidget::create>(Vec(34,190),module,Compair::BELOW_B_LED)); addChild(ModuleLightWidget::create>(Vec(34,190),module,Compair::GATE_B_LED)); addChild(ModuleLightWidget::create>(Vec(34,190),module,Compair::OVER_B_LED)); addParam(ParamWidget::create(Vec(34,190),module,Compair::INVERT_B_PARAM, 0, 1, 0)); addOutput(Port::create(Vec(4,230), Port::OUTPUT, module,Compair::GATE_A_OUT)); addOutput(Port::create(Vec(4,254), Port::OUTPUT, module,Compair::NOT_A_OUT)); addOutput(Port::create(Vec(34,230), Port::OUTPUT, module,Compair::GATE_B_OUT)); addOutput(Port::create(Vec(34,254), Port::OUTPUT, module,Compair::NOT_B_OUT)); addChild(ModuleLightWidget::create>(Vec(13,288),module,Compair::AND_LED)); addOutput(Port::create(Vec(4,294), Port::OUTPUT, module,Compair::AND_OUT)); addChild(ModuleLightWidget::create>(Vec(43,288),module,Compair::OR_LED)); addOutput(Port::create(Vec(34,294), Port::OUTPUT, module,Compair::OR_OUT)); addChild(ModuleLightWidget::create>(Vec(13,330),module,Compair::XOR_LED)); addOutput(Port::create(Vec(4,336), Port::OUTPUT, module,Compair::XOR_OUT)); addChild(ModuleLightWidget::create>(Vec(43,330),module,Compair::FLIP_LED)); addOutput(Port::create(Vec(34,336), Port::OUTPUT, module,Compair::FLIP_OUT)); } /*CompairWidget::CompairWidget(){ Compair *module = new Compair(); setModule(module); box.size = Vec(8 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT); { SVGPanel *panel = new SVGPanel(); panel->box.size = box.size; panel->setBackground(SVG::load(assetPlugin(plugin, "res/panels/Compair.svg"))); addChild(panel); } // SCREWS addChild(Widget::create(Vec(15, 0))); //addChild(Widget::create(Vec(box.size.x - 15, 0))); addChild(Widget::create(Vec(15, 365))); addChild(Widget::create(Vec(box.size.x - 30, 365))); // A addInput(Port::create(Vec(35,234), Port::INPUT, module,Compair::AUDIO_A_IN)); addParam(ParamWidget::create(Vec(10,60), module, Compair::POS_A_PARAM, -5.0f , 5.0f, 0.0f)); addInput(Port::create(Vec(7,190), Port::INPUT, module,Compair::POS_A_IN)); addParam(ParamWidget::create(Vec(10,128), module, Compair::WIDTH_A_PARAM, 0.01f , 10.001f, 5.0f)); addInput(Port::create(Vec(35,190), Port::INPUT, module,Compair::WIDTH_A_IN)); addOutput(Port::create(Vec(7,278), Port::OUTPUT, module,Compair::GATE_A_OUT)); addChild(Widget::create(Vec(7, 234))); addChild(ModuleLightWidget::create>(Vec(8,235),module,Compair::BELOW_A_LED)); addChild(ModuleLightWidget::create>(Vec(8,235),module,Compair::GATE_A_LED)); addChild(ModuleLightWidget::create>(Vec(8,235),module,Compair::OVER_A_LED)); addOutput(Port::create(Vec(35,278), Port::OUTPUT, module,Compair::NOT_A_OUT)); // B addInput(Port::create(Vec(63,234), Port::INPUT, module,Compair::AUDIO_B_IN)); addParam(ParamWidget::create(Vec(66,60), module, Compair::POS_B_PARAM, -5.0f, 5.0f, 0.0f)); addInput(Port::create(Vec(90,190), Port::INPUT, module,Compair::POS_B_IN)); addParam(ParamWidget::create(Vec(66,128), module, Compair::WIDTH_B_PARAM, 0.01f , 10.001f, 5.0f)); addInput(Port::create(Vec(63,190), Port::INPUT, module,Compair::WIDTH_B_IN)); addOutput(Port::create(Vec(90,278), Port::OUTPUT, module,Compair::GATE_B_OUT)); addChild(Widget::create(Vec(90, 234))); addChild(ModuleLightWidget::create>(Vec(91,235),module,Compair::BELOW_B_LED)); addChild(ModuleLightWidget::create>(Vec(91,235),module,Compair::GATE_B_LED)); addChild(ModuleLightWidget::create>(Vec(91,235),module,Compair::OVER_B_LED)); addOutput(Port::create(Vec(63,278), Port::OUTPUT, module,Compair::NOT_B_OUT)); // Invert toggles addParam(ParamWidget::create(Vec(11,238),module,Compair::INVERT_A_PARAM, 0, 1, 0)); addParam(ParamWidget::create(Vec(94,238),module,Compair::INVERT_B_PARAM, 0, 1, 0)); // LOGIC addOutput(Port::create(Vec(7,324), Port::OUTPUT, module,Compair::AND_OUT)); addChild(ModuleLightWidget::create>(Vec(16,318),module,Compair::AND_LED)); addOutput(Port::create(Vec(35,324), Port::OUTPUT, module,Compair::OR_OUT)); addChild(ModuleLightWidget::create>(Vec(44,318),module,Compair::OR_LED)); addOutput(Port::create(Vec(63,324), Port::OUTPUT, module,Compair::XOR_OUT)); addChild(ModuleLightWidget::create>(Vec(72,318),module,Compair::XOR_LED)); addOutput(Port::create(Vec(90,324), Port::OUTPUT, module,Compair::FLIP_OUT)); addChild(ModuleLightWidget::create>(Vec(99,318),module,Compair::FLIP_LED)); }*/ struct CompairOutputModeItem : MenuItem { Compair *compair; Compair::OutputMode outputMode; void onAction(EventAction &e) override { compair->outputMode = outputMode; } void step() override { rightText = (compair->outputMode == outputMode) ? "✔" : ""; } }; Menu *CompairWidget::createContextMenu() { Menu *menu = ModuleWidget::createContextMenu(); MenuLabel *spacerLabel = new MenuLabel(); menu->addChild(spacerLabel); Compair *compair = dynamic_cast(module); assert(compair); MenuLabel *modeLabel = new MenuLabel(); modeLabel->text = "Output Mode"; menu->addChild(modeLabel); CompairOutputModeItem *originalItem = new CompairOutputModeItem(); originalItem->text = "Original [0V..+5V]"; originalItem->compair = compair; originalItem->outputMode = Compair::ORIGINAL; menu->addChild(originalItem); CompairOutputModeItem *bipolarItem = new CompairOutputModeItem(); bipolarItem->text = "Bi-Polar [-5V..+5V]"; bipolarItem->compair = compair; bipolarItem->outputMode = Compair::BIPOLAR; menu->addChild(bipolarItem); return menu; } } // namespace rack_plugin_PvC using namespace rack_plugin_PvC; RACK_PLUGIN_MODEL_INIT(PvC, Compair) { Model *modelCompair = Model::create( "PvC", "Compair", "Compair", LOGIC_TAG, DUAL_TAG, CLOCK_MODULATOR_TAG); return modelCompair; }