//*********************************************************************************************** //Gravitational Voltage Controled Amplifiers module for VCV Rack by Pierre Collard and Marc Boulé // //Based on code from the Fundamental plugins by Andrew Belt and graphics // from the Component Library by Wes Milholen. //See ./LICENSE.txt for all licenses //See ./res/fonts/ for font licenses // //*********************************************************************************************** #include "Geodesics.hpp" namespace rack_plugin_Geodesics { struct BlackHoles : Module { enum ParamIds { ENUMS(LEVEL_PARAMS, 8),// -1.0f to 1.0f knob, set to default (0.0f) when using CV input ENUMS(EXP_PARAMS, 2),// push-button WORMHOLE_PARAM, ENUMS(CVLEVEL_PARAMS, 2),// push-button NUM_PARAMS }; enum InputIds { ENUMS(IN_INPUTS, 8),// -10 to 10 V ENUMS(LEVELCV_INPUTS, 8),// 0 to 10V CV or -5 to 5V depeding on cvMode NUM_INPUTS }; enum OutputIds { ENUMS(OUT_OUTPUTS, 8),// input * [-1;1] when input connected, else [-10;10] CV when input unconnected ENUMS(BLACKHOLE_OUTPUTS, 2), NUM_OUTPUTS }; enum LightIds { ENUMS(EXP_LIGHTS, 2), ENUMS(WORMHOLE_LIGHT, 2),// room for WhiteRed ENUMS(CVALEVEL_LIGHTS, 2),// White, but two lights (light 0 is cvMode bit = 0, light 1 is cvMode bit = 1) ENUMS(CVBLEVEL_LIGHTS, 2),// White, but two lights NUM_LIGHTS }; // Constants static constexpr float expBase = 50.0f; // Need to save, with reset bool isExponential[2]; bool wormhole; int cvMode;// 0 is -5v to 5v, 1 is -10v to 10v; bit 0 is upper BH, bit 1 is lower BH // Need to save, no reset int panelTheme; // No need to save, with reset // none // No need to save, no reset SchmittTrigger expTriggers[2]; SchmittTrigger cvLevelTriggers[2]; SchmittTrigger wormholeTrigger; BlackHoles() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { // Need to save, no reset panelTheme = 0; // No need to save, no reset expTriggers[0].reset(); expTriggers[1].reset(); onReset(); } // widgets are not yet created when module is created // even if widgets not created yet, can use params[] and should handle 0.0f value since step may call // this before widget creation anyways // called from the main thread if by constructor, called by engine thread if right-click initialization // when called by constructor, module is created before the first step() is called void onReset() override { // Need to save, with reset cvMode = 0x3; isExponential[0] = false; isExponential[1] = false; wormhole = true; // No need to save, with reset // none } // widgets randomized before onRandomize() is called // called by engine thread if right-click randomize void onRandomize() override { // Need to save, with reset for (int i = 0; i < 2; i++) { isExponential[i] = (randomu32() % 2) > 0; } wormhole = (randomu32() % 2) > 0; // No need to save, with reset // none } // called by main thread json_t *toJson() override { json_t *rootJ = json_object(); // Need to save (reset or not) // isExponential json_object_set_new(rootJ, "isExponential0", json_real(isExponential[0])); json_object_set_new(rootJ, "isExponential1", json_real(isExponential[1])); // wormhole json_object_set_new(rootJ, "wormhole", json_boolean(wormhole)); // panelTheme json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme)); // cvMode json_object_set_new(rootJ, "cvMode", json_integer(cvMode)); return rootJ; } // widgets have their fromJson() called before this fromJson() is called // called by main thread void fromJson(json_t *rootJ) override { // Need to save (reset or not) // isExponential json_t *isExponential0J = json_object_get(rootJ, "isExponential0"); if (isExponential0J) isExponential[0] = json_real_value(isExponential0J); json_t *isExponential1J = json_object_get(rootJ, "isExponential1"); if (isExponential1J) isExponential[1] = json_real_value(isExponential1J); // wormhole json_t *wormholeJ = json_object_get(rootJ, "wormhole"); if (wormholeJ) wormhole = json_is_true(wormholeJ); // panelTheme json_t *panelThemeJ = json_object_get(rootJ, "panelTheme"); if (panelThemeJ) panelTheme = json_integer_value(panelThemeJ); // cvMode json_t *cvModeJ = json_object_get(rootJ, "cvMode"); if (cvModeJ) cvMode = json_integer_value(cvModeJ); // No need to save, with reset // none } // Advances the module by 1 audio frame with duration 1.0 / engineGetSampleRate() void step() override { // Exponential buttons for (int i = 0; i < 2; i++) if (expTriggers[i].process(params[EXP_PARAMS + i].value)) { isExponential[i] = !isExponential[i]; } // Wormhole buttons if (wormholeTrigger.process(params[WORMHOLE_PARAM].value)) { wormhole = ! wormhole; } // CV Level buttons for (int i = 0; i < 2; i++) { if (cvLevelTriggers[i].process(params[CVLEVEL_PARAMS + i].value)) cvMode ^= (0x1 << i); } // BlackHole 0 all outputs float blackHole0 = 0.0f; float inputs0[4] = {10.0f, 10.0f, 10.0f, 10.0f};// default to generate CV when no input connected for (int i = 0; i < 4; i++) if (inputs[IN_INPUTS + i].active) inputs0[i] = inputs[IN_INPUTS + i].value; for (int i = 0; i < 4; i++) { float chanVal = calcChannel(inputs0[i], params[LEVEL_PARAMS + i], inputs[LEVELCV_INPUTS + i], isExponential[0], cvMode & 0x1); outputs[OUT_OUTPUTS + i].value = chanVal; blackHole0 += chanVal; } outputs[BLACKHOLE_OUTPUTS + 0].value = clamp(blackHole0, -10.0f, 10.0f); // BlackHole 1 all outputs float blackHole1 = 0.0f; float inputs1[4] = {10.0f, 10.0f, 10.0f, 10.0f};// default to generate CV when no input connected bool allUnconnected = true; for (int i = 0; i < 4; i++) if (inputs[IN_INPUTS + i + 4].active) { inputs1[i] = inputs[IN_INPUTS + i + 4].value; allUnconnected = false; } if (allUnconnected && wormhole) for (int i = 0; i < 4; i++) inputs1[i] = blackHole0; for (int i = 0; i < 4; i++) { float chanVal = calcChannel(inputs1[i], params[LEVEL_PARAMS + i + 4], inputs[LEVELCV_INPUTS + i + 4], isExponential[1], cvMode >> 1); outputs[OUT_OUTPUTS + i + 4].value = chanVal; blackHole1 += chanVal; } outputs[BLACKHOLE_OUTPUTS + 1].value = clamp(blackHole1, -10.0f, 10.0f); // Wormhole light lights[WORMHOLE_LIGHT + 0].value = ((wormhole && allUnconnected) ? 1.0f : 0.0f); lights[WORMHOLE_LIGHT + 1].value = ((wormhole && !allUnconnected) ? 1.0f : 0.0f); // isExponential lights for (int i = 0; i < 2; i++) lights[EXP_LIGHTS + i].value = isExponential[i] ? 1.0f : 0.0f; // CV Level lights lights[CVALEVEL_LIGHTS + 0].value = (cvMode & 0x1) == 0 ? 1.0f : 0.0f; lights[CVALEVEL_LIGHTS + 1].value = 1.0f - lights[CVALEVEL_LIGHTS + 0].value; lights[CVBLEVEL_LIGHTS + 0].value = (cvMode & 0x2) == 0 ? 1.0f : 0.0f; lights[CVBLEVEL_LIGHTS + 1].value = 1.0f - lights[CVBLEVEL_LIGHTS + 0].value; }// step() float calcChannel(float in, Param &level, Input &levelCV, bool isExp, int cvMode) { float levCv = levelCV.active ? (levelCV.value / (cvMode != 0 ? 10.0f : 5.0f)) : 0.0f; float lev = clamp(level.value + levCv, -1.0f, 1.0f); if (isExp) { float newlev = rescale(powf(expBase, fabs(lev)), 1.0f, expBase, 0.0f, 1.0f); if (lev < 0.0f) newlev *= -1.0f; lev = newlev; } float ret = lev * in; return ret; } }; struct BlackHolesWidget : ModuleWidget { struct PanelThemeItem : MenuItem { BlackHoles *module; int theme; void onAction(EventAction &e) override { module->panelTheme = theme; } void step() override { rightText = (module->panelTheme == theme) ? "✔" : ""; } }; Menu *createContextMenu() override { Menu *menu = ModuleWidget::createContextMenu(); MenuLabel *spacerLabel = new MenuLabel(); menu->addChild(spacerLabel); BlackHoles *module = dynamic_cast(this->module); assert(module); MenuLabel *themeLabel = new MenuLabel(); themeLabel->text = "Panel Theme"; menu->addChild(themeLabel); PanelThemeItem *lightItem = new PanelThemeItem(); lightItem->text = lightPanelID;// Geodesics.hpp lightItem->module = module; lightItem->theme = 0; menu->addChild(lightItem); PanelThemeItem *darkItem = new PanelThemeItem(); darkItem->text = darkPanelID;// Geodesics.hpp darkItem->module = module; darkItem->theme = 1; //menu->addChild(darkItem); return menu; } BlackHolesWidget(BlackHoles *module) : ModuleWidget(module) { // Main panel from Inkscape DynamicSVGPanel *panel = new DynamicSVGPanel(); panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/BlackHolesBG-01.svg"))); //panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/BlackHolesBG-02.svg")));// no dark pannel for now box.size = panel->box.size; panel->mode = &module->panelTheme; addChild(panel); // Screws // part of svg panel, no code required float colRulerCenter = box.size.x / 2.0f; static constexpr float rowRulerBlack0 = 108.5f; static constexpr float rowRulerBlack1 = 272.5f; static constexpr float radiusIn = 30.0f; static constexpr float radiusOut = 61.0f; static constexpr float offsetL = 53.0f; static constexpr float offsetS = 30.0f; // BlackHole0 knobs addParam(createDynamicParam(Vec(colRulerCenter, rowRulerBlack0 - radiusOut), module, BlackHoles::LEVEL_PARAMS + 0, -1.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerCenter + radiusOut, rowRulerBlack0), module, BlackHoles::LEVEL_PARAMS + 1, -1.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerCenter, rowRulerBlack0 + radiusOut), module, BlackHoles::LEVEL_PARAMS + 2, -1.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerCenter - radiusOut, rowRulerBlack0), module, BlackHoles::LEVEL_PARAMS + 3, -1.0f, 1.0f, 0.0f, &module->panelTheme)); // BlackHole0 level CV inputs addInput(createDynamicPort(Vec(colRulerCenter, rowRulerBlack0 - radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 0, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + radiusIn, rowRulerBlack0), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 1, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter, rowRulerBlack0 + radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 2, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - radiusIn, rowRulerBlack0), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 3, &module->panelTheme)); // BlackHole0 inputs addInput(createDynamicPort(Vec(colRulerCenter - offsetS, rowRulerBlack0 - offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 0, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetL, rowRulerBlack0 - offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 1, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetS, rowRulerBlack0 + offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 2, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - offsetL, rowRulerBlack0 + offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 3, &module->panelTheme)); // BlackHole0 outputs addOutput(createDynamicPort(Vec(colRulerCenter + offsetS, rowRulerBlack0 - offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 0, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + offsetL, rowRulerBlack0 + offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 1, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetS, rowRulerBlack0 + offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 2, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetL, rowRulerBlack0 - offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 3, &module->panelTheme)); // BlackHole0 center output addOutput(createDynamicPort(Vec(colRulerCenter, rowRulerBlack0), Port::OUTPUT, module, BlackHoles::BLACKHOLE_OUTPUTS + 0, &module->panelTheme)); // BlackHole1 knobs addParam(createDynamicParam(Vec(colRulerCenter, rowRulerBlack1 - radiusOut), module, BlackHoles::LEVEL_PARAMS + 4, -1.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerCenter + radiusOut, rowRulerBlack1), module, BlackHoles::LEVEL_PARAMS + 5, -1.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerCenter, rowRulerBlack1 + radiusOut), module, BlackHoles::LEVEL_PARAMS + 6, -1.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerCenter - radiusOut, rowRulerBlack1), module, BlackHoles::LEVEL_PARAMS + 7, -1.0f, 1.0f, 0.0f, &module->panelTheme)); // BlackHole1 level CV inputs addInput(createDynamicPort(Vec(colRulerCenter, rowRulerBlack1 - radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 4, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + radiusIn, rowRulerBlack1), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 5, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter, rowRulerBlack1 + radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 6, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - radiusIn, rowRulerBlack1), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 7, &module->panelTheme)); // BlackHole1 inputs addInput(createDynamicPort(Vec(colRulerCenter - offsetS, rowRulerBlack1 - offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 4, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetL, rowRulerBlack1 - offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 5, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetS, rowRulerBlack1 + offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 6, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - offsetL, rowRulerBlack1 + offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 7, &module->panelTheme)); // BlackHole1 outputs addOutput(createDynamicPort(Vec(colRulerCenter + offsetS, rowRulerBlack1 - offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 4, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + offsetL, rowRulerBlack1 + offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 5, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetS, rowRulerBlack1 + offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 6, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetL, rowRulerBlack1 - offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 7, &module->panelTheme)); // BlackHole1 center output addOutput(createDynamicPort(Vec(colRulerCenter, rowRulerBlack1), Port::OUTPUT, module, BlackHoles::BLACKHOLE_OUTPUTS + 1, &module->panelTheme)); static constexpr float offsetButtonsX = 62.0f; static constexpr float offsetButtonsY = 64.0f; static constexpr float offsetLedVsBut = 9.0f; static constexpr float offsetLedVsButS = 5.0f;// small static constexpr float offsetLedVsButL = 12.0f;// large // BlackHole0 Exp button and light addParam(createDynamicParam(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack0 + offsetButtonsY), module, BlackHoles::EXP_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack0 + offsetButtonsY - offsetLedVsBut - 1.0f), module, BlackHoles::EXP_LIGHTS + 0)); // BlackHole1 Exp button and light addParam(createDynamicParam(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack1 + offsetButtonsY), module, BlackHoles::EXP_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack1 + offsetButtonsY - offsetLedVsBut -1.0f), module, BlackHoles::EXP_LIGHTS + 1)); // Wormhole button and light addParam(createDynamicParam(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack1 - offsetButtonsY), module, BlackHoles::WORMHOLE_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack1 - offsetButtonsY + offsetLedVsBut), module, BlackHoles::WORMHOLE_LIGHT)); // CV Level A button and light addParam(createDynamicParam(Vec(colRulerCenter + offsetButtonsX, rowRulerBlack0 + offsetButtonsY), module, BlackHoles::CVLEVEL_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButL, rowRulerBlack0 + offsetButtonsY + offsetLedVsButS), module, BlackHoles::CVALEVEL_LIGHTS + 0)); addChild(createLightCentered>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButS, rowRulerBlack0 + offsetButtonsY + offsetLedVsButL), module, BlackHoles::CVALEVEL_LIGHTS + 1)); // CV Level B button and light addParam(createDynamicParam(Vec(colRulerCenter + offsetButtonsX, rowRulerBlack1 + offsetButtonsY), module, BlackHoles::CVLEVEL_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButL, rowRulerBlack1 + offsetButtonsY + offsetLedVsButS), module, BlackHoles::CVBLEVEL_LIGHTS + 0)); addChild(createLightCentered>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButS, rowRulerBlack1 + offsetButtonsY + offsetLedVsButL), module, BlackHoles::CVBLEVEL_LIGHTS + 1)); } }; } // namespace rack_plugin_Geodesics using namespace rack_plugin_Geodesics; RACK_PLUGIN_MODEL_INIT(Geodesics, BlackHoles) { Model *modelBlackHoles = Model::create("Geodesics", "BlackHoles", "BlackHoles", AMPLIFIER_TAG); return modelBlackHoles; } /*CHANGE LOG 0.6.1: add CV level modes buttons and lights change CV level behavior 0.6.0: created */