//*********************************************************************************************** //Neutron Powered Rotating Crossfader 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 Pulsars : Module { enum ParamIds { ENUMS(VOID_PARAMS, 2),// push-button ENUMS(REV_PARAMS, 2),// push-button ENUMS(RND_PARAMS, 2),// push-button ENUMS(CVLEVEL_PARAMS, 2),// push-button NUM_PARAMS }; enum InputIds { ENUMS(INA_INPUTS, 8), INB_INPUT, ENUMS(LFO_INPUTS, 2), ENUMS(VOID_INPUTS, 2), ENUMS(REV_INPUTS, 2), NUM_INPUTS }; enum OutputIds { OUTA_OUTPUT, ENUMS(OUTB_OUTPUTS, 8), NUM_OUTPUTS }; enum LightIds { ENUMS(LFO_LIGHTS, 2), ENUMS(MIXA_LIGHTS, 8), ENUMS(MIXB_LIGHTS, 8), ENUMS(VOID_LIGHTS, 2), ENUMS(REV_LIGHTS, 2), ENUMS(RND_LIGHTS, 2), 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 epsilon = 0.0001f;// pulsar crossovers at epsilon and 1-epsilon in 0.0f to 1.0f space // Need to save, with reset bool isVoid[2]; bool isReverse[2]; bool isRandom[2]; int cvMode;// 0 is -5v to 5v, 1 is -10v to 10v; bit 0 is upper Pulsar, bit 1 is lower Pulsar // Need to save, no reset int panelTheme; // No need to save, with reset int posA;// always between 0 and 7 int posB;// always between 0 and 7 int posAnext;// always between 0 and 7 int posBnext;// always between 0 and 7 bool topCross[2]; // No need to save, no reset SchmittTrigger voidTriggers[2]; SchmittTrigger revTriggers[2]; SchmittTrigger rndTriggers[2]; SchmittTrigger cvLevelTriggers[2]; float lfoLights[2]; Pulsars() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { // Need to save, no reset panelTheme = 0; // No need to save, no reset for (int i = 0; i < 2; i++) { lfoLights[i] = 0.0f; voidTriggers[i].reset(); revTriggers[i].reset(); rndTriggers[i].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 = 0; for (int i = 0; i < 2; i++) { topCross[i] = false; isVoid[i] = false; isReverse[i] = false; isRandom[i] = false; } // No need to save, with reset posA = 0;// no need to check isVoid here, will be checked in step() posB = 0;// no need to check isVoid here, will be checked in step() posAnext = 1;// no need to check isVoid here, will be checked in step() posBnext = 1;// no need to check isVoid here, will be checked in step() } // 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++) { isVoid[i] = (randomu32() % 2) > 0; isReverse[i] = (randomu32() % 2) > 0; isRandom[i] = (randomu32() % 2) > 0; } // No need to save, with reset posA = randomu32() % 8;// no need to check isVoid here, will be checked in step() posB = randomu32() % 8;// no need to check isVoid here, will be checked in step() posAnext = (posA + (isReverse[0] ? 7 : 1)) % 8;// no need to check isVoid here, will be checked in step() posBnext = (posB + (isReverse[1] ? 7 : 1)) % 8;// no need to check isVoid here, will be checked in step() } // called by main thread json_t *toJson() override { json_t *rootJ = json_object(); // Need to save (reset or not) // isVoid json_object_set_new(rootJ, "isVoid0", json_real(isVoid[0])); json_object_set_new(rootJ, "isVoid1", json_real(isVoid[1])); // isReverse json_object_set_new(rootJ, "isReverse0", json_real(isReverse[0])); json_object_set_new(rootJ, "isReverse1", json_real(isReverse[1])); // isRandom json_object_set_new(rootJ, "isRandom0", json_real(isRandom[0])); json_object_set_new(rootJ, "isRandom1", json_real(isRandom[1])); // 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) // isVoid json_t *isVoid0J = json_object_get(rootJ, "isVoid0"); if (isVoid0J) isVoid[0] = json_real_value(isVoid0J); json_t *isVoid1J = json_object_get(rootJ, "isVoid1"); if (isVoid1J) isVoid[1] = json_real_value(isVoid1J); // isReverse json_t *isReverse0J = json_object_get(rootJ, "isReverse0"); if (isReverse0J) isReverse[0] = json_real_value(isReverse0J); json_t *isReverse1J = json_object_get(rootJ, "isReverse1"); if (isReverse1J) isReverse[1] = json_real_value(isReverse1J); // isRandom json_t *isRandom0J = json_object_get(rootJ, "isRandom0"); if (isRandom0J) isRandom[0] = json_real_value(isRandom0J); json_t *isRandom1J = json_object_get(rootJ, "isRandom1"); if (isRandom1J) isRandom[1] = json_real_value(isRandom1J); // 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 posA = 0;// no need to check isVoid here, will be checked in step() posB = 0;// no need to check isVoid here, will be checked in step() posAnext = (posA + (isReverse[0] ? 7 : 1)) % 8;// no need to check isVoid here, will be checked in step() posBnext = (posB + (isReverse[1] ? 7 : 1)) % 8;// no need to check isVoid here, will be checked in step() } // Advances the module by 1 audio frame with duration 1.0 / engineGetSampleRate() void step() override { // Void, Reverse and Random buttons for (int i = 0; i < 2; i++) { if (voidTriggers[i].process(params[VOID_PARAMS + i].value + inputs[VOID_INPUTS + i].value)) { isVoid[i] = !isVoid[i]; } if (revTriggers[i].process(params[REV_PARAMS + i].value + inputs[REV_INPUTS + i].value)) { isReverse[i] = !isReverse[i]; } if (rndTriggers[i].process(params[RND_PARAMS + i].value)) {// + inputs[RND_INPUTS + i].value)) { isRandom[i] = !isRandom[i]; } } // CV Level buttons for (int i = 0; i < 2; i++) { if (cvLevelTriggers[i].process(params[CVLEVEL_PARAMS + i].value)) cvMode ^= (0x1 << i); } // LFO values (normalized to 0.0f to 1.0f space, clamped and offset adjusted depending cvMode) float lfoVal[2]; lfoVal[0] = inputs[LFO_INPUTS + 0].value; lfoVal[1] = inputs[LFO_INPUTS + 1].active ? inputs[LFO_INPUTS + 1].value : lfoVal[0]; for (int i = 0; i < 2; i++) lfoVal[i] = clamp( (lfoVal[i] + ((cvMode & (0x1 << i)) == 0 ? 5.0f : 0.0f)) / 10.0f , 0.0f , 1.0f); // Pulsar A bool active8[8]; bool atLeastOneActive = false; for (int i = 0; i < 8; i++) { active8[i] = inputs[INA_INPUTS + i].active; if (active8[i]) atLeastOneActive = true; } if (atLeastOneActive) { if (!isVoid[0]) { if (!active8[posA])// ensure start on valid input when no void posA = getNextClosestActive(posA, active8, false, false, false); if (!active8[posAnext]) posAnext = getNextClosestActive(posA, active8, false, isReverse[0], isRandom[0]); } float posPercent = topCross[0] ? (1.0f - lfoVal[0]) : lfoVal[0]; float nextPosPercent = 1.0f - posPercent; outputs[OUTA_OUTPUT].value = posPercent * inputs[INA_INPUTS + posA].value + nextPosPercent * inputs[INA_INPUTS + posAnext].value; for (int i = 0; i < 8; i++) lights[MIXA_LIGHTS + i].setBrightness(0.0f + ((i == posA) ? posPercent : 0.0f) + ((i == posAnext) ? nextPosPercent : 0.0f)); // PulsarA crossover (LFO detection) if ( (topCross[0] && lfoVal[0] > (1.0f - epsilon)) || (!topCross[0] && lfoVal[0] < epsilon) ) { topCross[0] = !topCross[0];// switch to opposite detection posA = posAnext; posAnext = getNextClosestActive(posA, active8, isVoid[0], isReverse[0], isRandom[0]); lfoLights[0] = 1.0f; } } else { outputs[OUTA_OUTPUT].value = 0.0f; for (int i = 0; i < 8; i++) lights[MIXA_LIGHTS + i].value = 0.0f; } // Pulsar B atLeastOneActive = false; for (int i = 0; i < 8; i++) { active8[i] = outputs[OUTB_OUTPUTS + i].active; if (active8[i]) atLeastOneActive = true; } if (atLeastOneActive) { if (!isVoid[1]) { if (!active8[posB])// ensure start on valid output when no void posB = getNextClosestActive(posB, active8, false, false, false); if (!active8[posBnext]) posBnext = getNextClosestActive(posB, active8, false, isReverse[1], isRandom[1]); } float posPercent = topCross[1] ? (1.0f - lfoVal[1]) : lfoVal[1]; float nextPosPercent = 1.0f - posPercent; for (int i = 0; i < 8; i++) { if (inputs[INB_INPUT].active) outputs[OUTB_OUTPUTS + i].value = 0.0f + ((i == posB) ? (posPercent * inputs[INB_INPUT].value) : 0.0f) + ((i == posBnext) ? (nextPosPercent * inputs[INB_INPUT].value) : 0.0f); else// mutidimentional trick outputs[OUTB_OUTPUTS + i].value = 0.0f + ((i == posB) ? (posPercent * inputs[INA_INPUTS + i].value) : 0.0f) + ((i == posBnext) ? (nextPosPercent * inputs[INA_INPUTS + i].value) : 0.0f); lights[MIXB_LIGHTS + i].setBrightness(0.0f + ((i == posB) ? posPercent : 0.0f) + ((i == posBnext) ? nextPosPercent : 0.0f)); } // PulsarB crossover (LFO detection) if ( (topCross[1] && lfoVal[1] > (1.0f - epsilon)) || (!topCross[1] && lfoVal[1] < epsilon) ) { topCross[1] = !topCross[1];// switch to opposite detection posB = posBnext; posBnext = getNextClosestActive(posB, active8, isVoid[1], isReverse[1], isRandom[1]); lfoLights[1] = 1.0f; } } else { for (int i = 0; i < 8; i++) { outputs[OUTB_OUTPUTS + i].value = 0.0f; lights[MIXB_LIGHTS + i].value = 0.0f; } } // Void, Reverse and Random lights for (int i = 0; i < 2; i++) { lights[VOID_LIGHTS + i].value = isVoid[i] ? 1.0f : 0.0f; lights[REV_LIGHTS + i].value = isReverse[i] ? 1.0f : 0.0f; lights[RND_LIGHTS + i].value = isRandom[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; // LFO lights for (int i = 0; i < 2; i++) { lights[LFO_LIGHTS + i].value = lfoLights[i]; lfoLights[i] -= (lfoLights[i] / lightLambda) * (float)engineGetSampleTime(); } }// step() int getNextClosestActive(int pos, bool* active8, bool voidd, bool reverse, bool random) { // finds the next closest active position (excluding current if active) // assumes at least one active, but may not be given pos; will always return an active pos // scans all 8 positions int posNext = -1;// should never be returned if (random) { if (voidd) posNext = (pos + 1 + randomu32() % 7) % 8; else { posNext = pos; int activeIndexes[8];// room for all indexes of active positions except current if active(max size is guaranteed to be < 8) int activeIndexesI = 0; for (int i = 0; i < 8; i++) { if (active8[i] && i != pos) { activeIndexes[activeIndexesI] = i; activeIndexesI++; } } if (activeIndexesI > 0) posNext = activeIndexes[randomu32()%activeIndexesI]; } } else { posNext = (pos + (reverse ? 7 : 1)) % 8;// void approach by default (choose slot whether active of not) if (!voidd) { if (reverse) { for (int i = posNext + 8; i > posNext; i--) { if (active8[i % 8]) { posNext = i % 8; break; } } } else { for (int i = posNext; i < posNext + 8; i++) { if (active8[i % 8]) { posNext = i % 8; break; } } } } } return posNext; } }; struct PulsarsWidget : ModuleWidget { struct PanelThemeItem : MenuItem { Pulsars *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); Pulsars *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; } PulsarsWidget(Pulsars *module) : ModuleWidget(module) { // Main panel from Inkscape DynamicSVGPanel *panel = new DynamicSVGPanel(); panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/PulsarsBG-01.svg"))); //panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/PulsarsBG-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 rowRulerPulsarA = 127.5; static constexpr float rowRulerPulsarB = 261.5f; float rowRulerLFOlights = (rowRulerPulsarA + rowRulerPulsarB) / 2.0f; static constexpr float offsetLFOlightsX = 25.0f; static constexpr float radiusLeds = 23.0f; static constexpr float radiusJacks = 46.0f; static constexpr float offsetLeds = 17.0f; static constexpr float offsetJacks = 33.0f; static constexpr float offsetLFO = 24.0f;// used also for void and reverse CV input jacks static constexpr float offsetLedX = 13.0f;// adds/subs to offsetLFO for void and reverse lights static constexpr float offsetButtonX = 26.0f;// adds/subs to offsetLFO for void and reverse buttons static constexpr float offsetLedY = 11.0f;// adds/subs to offsetLFO for void and reverse lights static constexpr float offsetButtonY = 18.0f;// adds/subs to offsetLFO for void and reverse buttons static constexpr float offsetRndButtonX = 58.0f;// from center of pulsar static constexpr float offsetRndButtonY = 24.0f;// from center of pulsar static constexpr float offsetRndLedX = 63.0f;// from center of pulsar static constexpr float offsetRndLedY = 11.0f;// from center of pulsar static constexpr float offsetLedVsButBX = 8.0f;// BI static constexpr float offsetLedVsButBY = 10.0f; static constexpr float offsetLedVsButUX = 13.0f;// UNI static constexpr float offsetLedVsButUY = 1.0f; // PulsarA center output addOutput(createDynamicPort(Vec(colRulerCenter, rowRulerPulsarA), Port::OUTPUT, module, Pulsars::OUTA_OUTPUT, &module->panelTheme)); // PulsarA inputs addInput(createDynamicPort(Vec(colRulerCenter, rowRulerPulsarA - radiusJacks), Port::INPUT, module, Pulsars::INA_INPUTS + 0, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetJacks, rowRulerPulsarA - offsetJacks), Port::INPUT, module, Pulsars::INA_INPUTS + 1, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + radiusJacks, rowRulerPulsarA), Port::INPUT, module, Pulsars::INA_INPUTS + 2, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetJacks, rowRulerPulsarA + offsetJacks), Port::INPUT, module, Pulsars::INA_INPUTS + 3, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter, rowRulerPulsarA + radiusJacks), Port::INPUT, module, Pulsars::INA_INPUTS + 4, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - offsetJacks, rowRulerPulsarA + offsetJacks), Port::INPUT, module, Pulsars::INA_INPUTS + 5, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - radiusJacks, rowRulerPulsarA), Port::INPUT, module, Pulsars::INA_INPUTS + 6, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - offsetJacks, rowRulerPulsarA - offsetJacks), Port::INPUT, module, Pulsars::INA_INPUTS + 7, &module->panelTheme)); // PulsarA lights addChild(createLightCentered>(Vec(colRulerCenter, rowRulerPulsarA - radiusLeds), module, Pulsars::MIXA_LIGHTS + 0)); addChild(createLightCentered>(Vec(colRulerCenter + offsetLeds, rowRulerPulsarA - offsetLeds), module, Pulsars::MIXA_LIGHTS + 1)); addChild(createLightCentered>(Vec(colRulerCenter + radiusLeds, rowRulerPulsarA), module, Pulsars::MIXA_LIGHTS + 2)); addChild(createLightCentered>(Vec(colRulerCenter + offsetLeds, rowRulerPulsarA + offsetLeds), module, Pulsars::MIXA_LIGHTS + 3)); addChild(createLightCentered>(Vec(colRulerCenter, rowRulerPulsarA + radiusLeds), module, Pulsars::MIXA_LIGHTS + 4)); addChild(createLightCentered>(Vec(colRulerCenter - offsetLeds, rowRulerPulsarA + offsetLeds), module, Pulsars::MIXA_LIGHTS + 5)); addChild(createLightCentered>(Vec(colRulerCenter - radiusLeds, rowRulerPulsarA), module, Pulsars::MIXA_LIGHTS + 6)); addChild(createLightCentered>(Vec(colRulerCenter - offsetLeds, rowRulerPulsarA - offsetLeds), module, Pulsars::MIXA_LIGHTS + 7)); // PulsarA void (jack, light and button) addInput(createDynamicPort(Vec(colRulerCenter - offsetJacks - offsetLFO, rowRulerPulsarA - offsetJacks - offsetLFO), Port::INPUT, module, Pulsars::VOID_INPUTS + 0, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter - offsetJacks - offsetLFO + offsetLedX, rowRulerPulsarA - offsetJacks - offsetLFO - offsetLedY), module, Pulsars::VOID_LIGHTS + 0)); addParam(createDynamicParam(Vec(colRulerCenter - offsetJacks - offsetLFO + offsetButtonX, rowRulerPulsarA - offsetJacks - offsetLFO - offsetButtonY), module, Pulsars::VOID_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // PulsarA reverse (jack, light and button) addInput(createDynamicPort(Vec(colRulerCenter + offsetJacks + offsetLFO, rowRulerPulsarA - offsetJacks - offsetLFO), Port::INPUT, module, Pulsars::REV_INPUTS + 0, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter + offsetJacks + offsetLFO - offsetLedX, rowRulerPulsarA - offsetJacks - offsetLFO - offsetLedY), module, Pulsars::REV_LIGHTS + 0)); addParam(createDynamicParam(Vec(colRulerCenter + offsetJacks + offsetLFO - offsetButtonX, rowRulerPulsarA - offsetJacks - offsetLFO - offsetButtonY), module, Pulsars::REV_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // PulsarA random (light and button) addChild(createLightCentered>(Vec(colRulerCenter + offsetRndLedX, rowRulerPulsarA + offsetRndLedY), module, Pulsars::RND_LIGHTS + 0)); addParam(createDynamicParam(Vec(colRulerCenter + offsetRndButtonX, rowRulerPulsarA + offsetRndButtonY), module, Pulsars::RND_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // PulsarA CV level (lights and button) addParam(createDynamicParam(Vec(colRulerCenter - offsetRndButtonX, rowRulerPulsarA + offsetRndButtonY), module, Pulsars::CVLEVEL_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter - offsetRndButtonX - offsetLedVsButBX, rowRulerPulsarA + offsetRndButtonY + offsetLedVsButBY), module, Pulsars::CVALEVEL_LIGHTS + 0)); addChild(createLightCentered>(Vec(colRulerCenter - offsetRndButtonX - offsetLedVsButUX, rowRulerPulsarA + offsetRndButtonY - offsetLedVsButUY), module, Pulsars::CVALEVEL_LIGHTS + 1)); // PulsarA LFO input and light addInput(createDynamicPort(Vec(colRulerCenter - offsetJacks - offsetLFO, rowRulerPulsarA + offsetJacks + offsetLFO), Port::INPUT, module, Pulsars::LFO_INPUTS + 0, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter - offsetLFOlightsX, rowRulerLFOlights), module, Pulsars::LFO_LIGHTS + 0)); // PulsarB center input addInput(createDynamicPort(Vec(colRulerCenter, rowRulerPulsarB), Port::INPUT, module, Pulsars::INB_INPUT, &module->panelTheme)); // PulsarB outputs addOutput(createDynamicPort(Vec(colRulerCenter, rowRulerPulsarB - radiusJacks), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 0, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + offsetJacks, rowRulerPulsarB - offsetJacks), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 1, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + radiusJacks, rowRulerPulsarB), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 2, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + offsetJacks, rowRulerPulsarB + offsetJacks), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 3, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter, rowRulerPulsarB + radiusJacks), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 4, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetJacks, rowRulerPulsarB + offsetJacks), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 5, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - radiusJacks, rowRulerPulsarB), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 6, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetJacks, rowRulerPulsarB - offsetJacks), Port::OUTPUT, module, Pulsars::OUTB_OUTPUTS + 7, &module->panelTheme)); // PulsarB lights addChild(createLightCentered>(Vec(colRulerCenter, rowRulerPulsarB - radiusLeds), module, Pulsars::MIXB_LIGHTS + 0)); addChild(createLightCentered>(Vec(colRulerCenter + offsetLeds, rowRulerPulsarB - offsetLeds), module, Pulsars::MIXB_LIGHTS + 1)); addChild(createLightCentered>(Vec(colRulerCenter + radiusLeds, rowRulerPulsarB), module, Pulsars::MIXB_LIGHTS + 2)); addChild(createLightCentered>(Vec(colRulerCenter + offsetLeds, rowRulerPulsarB + offsetLeds), module, Pulsars::MIXB_LIGHTS + 3)); addChild(createLightCentered>(Vec(colRulerCenter, rowRulerPulsarB + radiusLeds), module, Pulsars::MIXB_LIGHTS + 4)); addChild(createLightCentered>(Vec(colRulerCenter - offsetLeds, rowRulerPulsarB + offsetLeds), module, Pulsars::MIXB_LIGHTS + 5)); addChild(createLightCentered>(Vec(colRulerCenter - radiusLeds, rowRulerPulsarB), module, Pulsars::MIXB_LIGHTS + 6)); addChild(createLightCentered>(Vec(colRulerCenter - offsetLeds, rowRulerPulsarB - offsetLeds), module, Pulsars::MIXB_LIGHTS + 7)); // PulsarB void (jack, light and button) addInput(createDynamicPort(Vec(colRulerCenter - offsetJacks - offsetLFO, rowRulerPulsarB + offsetJacks + offsetLFO), Port::INPUT, module, Pulsars::VOID_INPUTS + 1, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter - offsetJacks - offsetLFO + offsetLedX, rowRulerPulsarB + offsetJacks + offsetLFO + offsetLedY), module, Pulsars::VOID_LIGHTS + 1)); addParam(createDynamicParam(Vec(colRulerCenter - offsetJacks - offsetLFO + offsetButtonX, rowRulerPulsarB + offsetJacks + offsetLFO + offsetButtonY), module, Pulsars::VOID_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // PulsarB reverse (jack, light and button) addInput(createDynamicPort(Vec(colRulerCenter + offsetJacks + offsetLFO, rowRulerPulsarB + offsetJacks + offsetLFO), Port::INPUT, module, Pulsars::REV_INPUTS + 1, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter + offsetJacks + offsetLFO - offsetLedX, rowRulerPulsarB + offsetJacks + offsetLFO + offsetLedY), module, Pulsars::REV_LIGHTS + 1)); addParam(createDynamicParam(Vec(colRulerCenter + offsetJacks + offsetLFO - offsetButtonX, rowRulerPulsarB + offsetJacks + offsetLFO + offsetButtonY), module, Pulsars::REV_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // PulsarB random (light and button) addChild(createLightCentered>(Vec(colRulerCenter - offsetRndLedX, rowRulerPulsarB - offsetRndLedY), module, Pulsars::RND_LIGHTS + 1)); addParam(createDynamicParam(Vec(colRulerCenter - offsetRndButtonX, rowRulerPulsarB - offsetRndButtonY), module, Pulsars::RND_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // PulsarB CV level (lights and button) addParam(createDynamicParam(Vec(colRulerCenter + offsetRndButtonX, rowRulerPulsarB - offsetRndButtonY), module, Pulsars::CVLEVEL_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter + offsetRndButtonX + offsetLedVsButBX, rowRulerPulsarB - offsetRndButtonY - offsetLedVsButBY), module, Pulsars::CVBLEVEL_LIGHTS + 0)); addChild(createLightCentered>(Vec(colRulerCenter + offsetRndButtonX + offsetLedVsButUX, rowRulerPulsarB - offsetRndButtonY + offsetLedVsButUY), module, Pulsars::CVBLEVEL_LIGHTS + 1)); // PulsarA LFO input and light addInput(createDynamicPort(Vec(colRulerCenter + offsetJacks + offsetLFO, rowRulerPulsarB - offsetJacks - offsetLFO), Port::INPUT, module, Pulsars::LFO_INPUTS + 1, &module->panelTheme)); addChild(createLightCentered>(Vec(colRulerCenter + offsetLFOlightsX, rowRulerLFOlights), module, Pulsars::LFO_LIGHTS + 1)); } }; } // namespace rack_plugin_Geodesics using namespace rack_plugin_Geodesics; RACK_PLUGIN_MODEL_INIT(Geodesics, Pulsars) { Model *modelPulsars = Model::create("Geodesics", "Pulsars", "Pulsars", MIXER_TAG); return modelPulsars; } /*CHANGE LOG 0.6.1: add CV level modes buttons and lights, remove from right-click menu 0.6.0: created */