//*********************************************************************************************** //Chain-able keyboard module for VCV Rack by Marc Boulé // //Based on code from the Fundamental and Audible Instruments 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 // //Module inspired by: // * the Autodafe keyboard by Antonio Grazioli // * the cf mixer by Clément Foulc // * Twisted Electrons' KeyChain // //*********************************************************************************************** #include "ImpromptuModular.hpp" namespace rack_plugin_ImpromptuModular { struct TwelveKey : Module { enum ParamIds { OCTINC_PARAM, OCTDEC_PARAM, ENUMS(KEY_PARAMS, 12), NUM_PARAMS }; enum InputIds { GATE_INPUT, CV_INPUT, OCT_INPUT, NUM_INPUTS }; enum OutputIds { GATE_OUTPUT, CV_OUTPUT, OCT_OUTPUT, NUM_OUTPUTS }; enum LightIds { PRESS_LIGHT,// no longer used ENUMS(KEY_LIGHTS, 12), NUM_LIGHTS }; // Need to save int panelTheme = 0; int octaveNum;// 0 to 9 float cv; bool stateInternal;// false when pass through CV and Gate, true when CV and gate from this module // No need to save //float gateLight = 0.0f; unsigned long noteLightCounter;// 0 when no key to light, downward step counter timer when key lit int lastKeyPressed;// 0 to 11 int lightRefreshCounter; SchmittTrigger keyTriggers[12]; SchmittTrigger gateInputTrigger; SchmittTrigger octIncTrigger; SchmittTrigger octDecTrigger; TwelveKey() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { onReset(); } void onReset() override { octaveNum = 4; cv = 0.0f; stateInternal = inputs[GATE_INPUT].active ? false : true; noteLightCounter = 0ul; lastKeyPressed = 0; lightRefreshCounter = 0; } void onRandomize() override { octaveNum = randomu32() % 10; cv = ((float)(octaveNum - 4)) + ((float)(randomu32() % 12)) / 12.0f; stateInternal = inputs[GATE_INPUT].active ? false : true; noteLightCounter = 0ul; lastKeyPressed = 0; } json_t *toJson() override { json_t *rootJ = json_object(); // panelTheme json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme)); // cv json_object_set_new(rootJ, "cv", json_real(cv)); // octave json_object_set_new(rootJ, "octave", json_integer(octaveNum)); // stateInternal json_object_set_new(rootJ, "stateInternal", json_boolean(stateInternal)); return rootJ; } void fromJson(json_t *rootJ) override { // panelTheme json_t *panelThemeJ = json_object_get(rootJ, "panelTheme"); if (panelThemeJ) panelTheme = json_integer_value(panelThemeJ); // cv json_t *cvJ = json_object_get(rootJ, "cv"); if (cvJ) cv = json_real_value(cvJ); // octave json_t *octaveJ = json_object_get(rootJ, "octave"); if (octaveJ) octaveNum = json_integer_value(octaveJ); // stateInternal json_t *stateInternalJ = json_object_get(rootJ, "stateInternal"); if (stateInternalJ) stateInternal = json_is_true(stateInternalJ); } // Advances the module by 1 audio frame with duration 1.0 / engineGetSampleRate() void step() override { static const float noteLightTime = 0.5f;// seconds //********** Buttons, knobs, switches and inputs ********** // Octave buttons and input if (octIncTrigger.process(params[OCTINC_PARAM].value)) octaveNum++; if (octDecTrigger.process(params[OCTDEC_PARAM].value)) octaveNum--; if (inputs[OCT_INPUT].active) octaveNum = ((int) floor(inputs[OCT_INPUT].value)); if (octaveNum > 9) octaveNum = 9; if (octaveNum < 0) octaveNum = 0; // Keyboard buttons and gate input for (int i = 0; i < 12; i++) { if (keyTriggers[i].process(params[KEY_PARAMS + i].value)) { cv = ((float)(octaveNum - 4)) + ((float) i) / 12.0f; stateInternal = true; noteLightCounter = (unsigned long) (noteLightTime * engineGetSampleRate() / displayRefreshStepSkips); lastKeyPressed = i; } } if (gateInputTrigger.process(inputs[GATE_INPUT].value)) { cv = inputs[CV_INPUT].value; stateInternal = false; } //********** Outputs and lights ********** // Gate light (with fade) int pressed = 0; for (int i = 0; i < 12; i++) if (params[KEY_PARAMS + i].value > 0.5f) pressed++; /*if (pressed != 0) gateLight = 1.0f; else gateLight -= (gateLight / lightLambda) * engineGetSampleTime(); lights[PRESS_LIGHT].value = gateLight;*/ // cv output outputs[CV_OUTPUT].value = cv; // gate output if (stateInternal == false) {// if receiving a key from left chain outputs[GATE_OUTPUT].value = inputs[GATE_INPUT].value; } else {// key from this outputs[GATE_OUTPUT].value = (pressed != 0 ? 10.0f : 0.0f); } // Octave output outputs[OCT_OUTPUT].value = round( (float)(octaveNum + 1) ); lightRefreshCounter++; if (lightRefreshCounter > displayRefreshStepSkips) { lightRefreshCounter = 0; // Key lights for (int i = 0; i < 12; i++) lights[KEY_LIGHTS + i].value = (( i == lastKeyPressed && (noteLightCounter > 0ul || params[KEY_PARAMS + i].value > 0.5f)) ? 1.0f : 0.0f); if (noteLightCounter > 0ul) noteLightCounter--; } } }; struct TwelveKeyWidget : ModuleWidget { struct OctaveNumDisplayWidget : TransparentWidget { int *octaveNum; std::shared_ptr font; OctaveNumDisplayWidget() { font = Font::load(assetPlugin(plugin, "res/fonts/Segment14.ttf")); } void draw(NVGcontext *vg) override { NVGcolor textColor = prepareDisplay(vg, &box); nvgFontFaceId(vg, font->handle); //nvgTextLetterSpacing(vg, 2.5); Vec textPos = Vec(6, 24); nvgFillColor(vg, nvgTransRGBA(textColor, 16)); nvgText(vg, textPos.x, textPos.y, "~", NULL); nvgFillColor(vg, textColor); char displayStr[2]; displayStr[0] = 0x30 + (char) *octaveNum; displayStr[1] = 0; nvgText(vg, textPos.x, textPos.y, displayStr, NULL); } }; struct PanelThemeItem : MenuItem { TwelveKey *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); TwelveKey *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;// ImpromptuModular.hpp lightItem->module = module; lightItem->theme = 0; menu->addChild(lightItem); PanelThemeItem *darkItem = new PanelThemeItem(); darkItem->text = darkPanelID;// ImpromptuModular.hpp darkItem->module = module; darkItem->theme = 1; menu->addChild(darkItem); return menu; } TwelveKeyWidget(TwelveKey *module) : ModuleWidget(module) { // Main panel from Inkscape DynamicSVGPanel *panel = new DynamicSVGPanel(); panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/TwelveKey.svg"))); panel->addPanel(SVG::load(assetPlugin(plugin, "res/dark/TwelveKey_dark.svg"))); box.size = panel->box.size; panel->mode = &module->panelTheme; addChild(panel); // Screws addChild(createDynamicScrew(Vec(15, 0), &module->panelTheme)); addChild(createDynamicScrew(Vec(box.size.x-30, 0), &module->panelTheme)); addChild(createDynamicScrew(Vec(15, 365), &module->panelTheme)); addChild(createDynamicScrew(Vec(box.size.x-30, 365), &module->panelTheme)); // ****** Top portion (keys) ****** static const int offsetKeyLEDx = 12; static const int offsetKeyLEDy = 41; // Black keys addParam(ParamWidget::create(Vec(30, 40), module, TwelveKey::KEY_PARAMS + 1, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(30+offsetKeyLEDx, 40+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 1)); addParam(ParamWidget::create(Vec(71, 40), module, TwelveKey::KEY_PARAMS + 3, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(71+offsetKeyLEDx, 40+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 3)); addParam(ParamWidget::create(Vec(154, 40), module, TwelveKey::KEY_PARAMS + 6, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(154+offsetKeyLEDx, 40+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 6)); addParam(ParamWidget::create(Vec(195, 40), module, TwelveKey::KEY_PARAMS + 8, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(195+offsetKeyLEDx, 40+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 8)); addParam(ParamWidget::create(Vec(236, 40), module, TwelveKey::KEY_PARAMS + 10, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(236+offsetKeyLEDx, 40+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 10)); // White keys addParam(ParamWidget::create(Vec(10, 112), module, TwelveKey::KEY_PARAMS + 0, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(10+offsetKeyLEDx, 112+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 0)); addParam(ParamWidget::create(Vec(51, 112), module, TwelveKey::KEY_PARAMS + 2, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(51+offsetKeyLEDx, 112+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 2)); addParam(ParamWidget::create(Vec(92, 112), module, TwelveKey::KEY_PARAMS + 4, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(92+offsetKeyLEDx, 112+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 4)); addParam(ParamWidget::create(Vec(133, 112), module, TwelveKey::KEY_PARAMS + 5, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(133+offsetKeyLEDx, 112+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 5)); addParam(ParamWidget::create(Vec(174, 112), module, TwelveKey::KEY_PARAMS + 7, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(174+offsetKeyLEDx, 112+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 7)); addParam(ParamWidget::create(Vec(215, 112), module, TwelveKey::KEY_PARAMS + 9, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(215+offsetKeyLEDx, 112+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 9)); addParam(ParamWidget::create(Vec(256, 112), module, TwelveKey::KEY_PARAMS + 11, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(256+offsetKeyLEDx, 112+offsetKeyLEDy), module, TwelveKey::KEY_LIGHTS + 11)); // ****** Bottom portion ****** // Column rulers (horizontal positions) static const int columnRulerL = 30; static const int columnRulerR = box.size.x - 25 - columnRulerL; static const int columnRulerM = box.size.x / 2 - 14; // Row rulers (vertical positions) static const int rowRuler0 = 220; static const int rowRulerStep = 49; static const int rowRuler1 = rowRuler0 + rowRulerStep; static const int rowRuler2 = rowRuler1 + rowRulerStep; // Left side inputs addInput(createDynamicPort(Vec(columnRulerL, rowRuler0), Port::INPUT, module, TwelveKey::CV_INPUT, &module->panelTheme)); addInput(createDynamicPort(Vec(columnRulerL, rowRuler1), Port::INPUT, module, TwelveKey::GATE_INPUT, &module->panelTheme)); addInput(createDynamicPort(Vec(columnRulerL, rowRuler2), Port::INPUT, module, TwelveKey::OCT_INPUT, &module->panelTheme)); // Middle // Press LED (moved other controls below up by 16 px when removed, to better center) //addChild(ModuleLightWidget::create>(Vec(columnRulerM + offsetMediumLight, rowRuler0 - 31 + offsetMediumLight), module, TwelveKey::PRESS_LIGHT)); // Octave display OctaveNumDisplayWidget *octaveNumDisplay = new OctaveNumDisplayWidget(); octaveNumDisplay->box.pos = Vec(columnRulerM + 2, rowRuler1 - 27 + vOffsetDisplay); octaveNumDisplay->box.size = Vec(24, 30);// 1 character octaveNumDisplay->octaveNum = &module->octaveNum; addChild(octaveNumDisplay); // Octave buttons addParam(createDynamicParam(Vec(columnRulerM - 20 + offsetCKD6b, rowRuler2 - 26 + offsetCKD6b), module, TwelveKey::OCTDEC_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(columnRulerM + 22 + offsetCKD6b, rowRuler2 - 26 + offsetCKD6b), module, TwelveKey::OCTINC_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Right side outputs addOutput(createDynamicPort(Vec(columnRulerR, rowRuler0), Port::OUTPUT, module, TwelveKey::CV_OUTPUT, &module->panelTheme)); addOutput(createDynamicPort(Vec(columnRulerR, rowRuler1), Port::OUTPUT, module, TwelveKey::GATE_OUTPUT, &module->panelTheme)); addOutput(createDynamicPort(Vec(columnRulerR, rowRuler2), Port::OUTPUT, module, TwelveKey::OCT_OUTPUT, &module->panelTheme)); } }; } // namespace rack_plugin_ImpromptuModular using namespace rack_plugin_ImpromptuModular; RACK_PLUGIN_MODEL_INIT(ImpromptuModular, TwelveKey) { Model *modelTwelveKey = Model::create("Impromptu Modular", "Twelve-Key", "CTRL - Twelve-Key", CONTROLLER_TAG); return modelTwelveKey; }