//*********************************************************************************************** //Colliding Sample and Hold 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. //Also based on code from Joel Robichaud's Nohmad Noise module //See ./LICENSE.txt for all licenses // //*********************************************************************************************** #include #include #include "Geodesics.hpp" namespace rack_plugin_Geodesics { // By Joel Robichaud - Nohmad Noise module struct NoiseGenerator { std::mt19937 rng; std::uniform_real_distribution uniform; NoiseGenerator() : uniform(-1.0f, 1.0f) { rng.seed(std::random_device()()); } float white() { return uniform(rng); } }; //***************************************************************************** // By Joel Robichaud - Nohmad Noise module struct PinkFilter { float b0, b1, b2, b3, b4, b5, b6; // Coefficients float y; // Out void process(float x) { b0 = 0.99886f * b0 + x * 0.0555179f; b1 = 0.99332f * b1 + x * 0.0750759f; b2 = 0.96900f * b2 + x * 0.1538520f; b3 = 0.86650f * b3 + x * 0.3104856f; b4 = 0.55000f * b4 + x * 0.5329522f; b5 = -0.7616f * b5 - x * 0.0168980f; y = b0 + b1 + b2 + b3 + b4 + b5 + b6 + x * 0.5362f; b6 = x * 0.115926f; } float pink() { return y; } }; //***************************************************************************** struct Branes : Module { enum ParamIds { ENUMS(TRIG_BYPASS_PARAMS, 2), NUM_PARAMS }; enum InputIds { ENUMS(IN_INPUTS, 14), ENUMS(TRIG_INPUTS, 2), ENUMS(TRIG_BYPASS_INPUTS, 2), NUM_INPUTS }; enum OutputIds { ENUMS(OUT_OUTPUTS, 14), NUM_OUTPUTS }; enum LightIds { ENUMS(BYPASS_CV_LIGHTS, 2 * 2),// room for white-red ENUMS(BYPASS_TRIG_LIGHTS, 2 * 2),// room for white-red NUM_LIGHTS }; // Constants // S&H are numbered 0 to 6 in BraneA from lower left to lower right // S&H are numbered 7 to 13 in BraneB from top right to top left enum NoiseId {NONE, WHITE, PINK, RED, BLUE};//use negative value for inv phase int noiseSources[14] = {PINK, RED, BLUE, WHITE, -BLUE, -RED, -PINK, -PINK, -RED, -BLUE, WHITE, BLUE, RED, PINK}; static constexpr float nullNoise = 100.0f;// when a noise has not been generated for the current step // Need to save, with reset bool trigBypass[2]; // Need to save, no reset int panelTheme; // No need to save, with reset float heldOuts[14]; // No need to save, no reset SchmittTrigger sampleTriggers[2]; SchmittTrigger trigBypassTriggers[2]; NoiseGenerator whiteNoise; PinkFilter pinkFilter; RCFilter redFilter; RCFilter blueFilter; float trigLights[2]; Branes() : 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++) { sampleTriggers[i].reset(); trigBypassTriggers[i].reset(); trigLights[i] = 0.0f; } redFilter.setCutoff(441.0f / engineGetSampleRate()); blueFilter.setCutoff(44100.0f / engineGetSampleRate()); 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 for (int i = 0; i < 2; i++) trigBypass[i] = false; // No need to save, with reset for (int i = 0; i < 14; i++) heldOuts[i] = 0.0f; } // 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++) trigBypass[i] = (randomu32() % 2) > 0; // No need to save, with reset for (int i = 0; i < 14; i++) heldOuts[i] = 0.0f; } // called by main thread json_t *toJson() override { json_t *rootJ = json_object(); // Need to save (reset or not) // trigBypass json_object_set_new(rootJ, "trigBypass0", json_real(trigBypass[0])); json_object_set_new(rootJ, "trigBypass1", json_real(trigBypass[1])); // panelTheme json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme)); 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) // trigBypass json_t *trigBypass0J = json_object_get(rootJ, "trigBypass0"); if (trigBypass0J) trigBypass[0] = json_real_value(trigBypass0J); json_t *trigBypass1J = json_object_get(rootJ, "trigBypass1"); if (trigBypass1J) trigBypass[1] = json_real_value(trigBypass1J); // panelTheme json_t *panelThemeJ = json_object_get(rootJ, "panelTheme"); if (panelThemeJ) panelTheme = json_integer_value(panelThemeJ); // No need to save, with reset for (int i = 0; i < 14; i++) heldOuts[i] = 0.0f; } // Advances the module by 1 audio frame with duration 1.0 / engineGetSampleRate() void step() override { float stepNoises[6] = {nullNoise, nullNoise, nullNoise, nullNoise, nullNoise, 0.0f};// order is whiteBase (-1 to 1), white, pink, red, blue, pink_processed (1.0f or 0.0f) // trigBypass buttons and cv inputs for (int i = 0; i < 2; i++) { if (trigBypassTriggers[i].process(params[TRIG_BYPASS_PARAMS + i].value + inputs[TRIG_BYPASS_INPUTS + i].value)) { trigBypass[i] = !trigBypass[i]; } } // trig inputs bool trigs[2]; bool trigInputsActive[2]; for (int i = 0; i < 2; i++) { trigs[i] = sampleTriggers[i].process(inputs[TRIG_INPUTS + i].value); if (trigs[i]) trigLights[i] = 1.0f; trigInputsActive[i] = trigBypass[i] ? false : inputs[TRIG_INPUTS + i].active; } // sample and hold outputs for (int sh = 0; sh < 14; sh++) { if (trigInputsActive[sh / 7] || (sh == 13 && trigInputsActive[0]) || (sh == 6 && trigInputsActive[1])) {// trig connected (with crosstrigger mechanism) if (trigs[sh / 7] || (sh == 13 && trigs[0]) || (sh == 6 && trigs[1])) { if (inputs[IN_INPUTS + sh].active)// if input cable heldOuts[sh] = inputs[IN_INPUTS + sh].value;// sample and hold input else { int noiseIndex = prepareNoise(stepNoises, sh);// sample and hold noise heldOuts[sh] = stepNoises[noiseIndex] * (noiseSources[sh] > 0 ? 1.0f : -1.0f); } } } else { // no trig connected if (inputs[IN_INPUTS + sh].active) { heldOuts[sh] = inputs[IN_INPUTS + sh].value;// copy of input if no trig and no input } else { heldOuts[sh] = 0.0f; if (outputs[OUT_OUTPUTS + sh].active) { int noiseIndex = prepareNoise(stepNoises, sh); heldOuts[sh] = stepNoises[noiseIndex] * (noiseSources[sh] > 0 ? 1.0f : -1.0f); } } } outputs[OUT_OUTPUTS + sh].value = heldOuts[sh]; } // Lights for (int i = 0; i < 2; i++) { float red = trigBypass[i] ? 1.0f : 0.0f; float white = !trigBypass[i] ? trigLights[i] : 0.0f; lights[BYPASS_CV_LIGHTS + i * 2 + 0].value = white; lights[BYPASS_CV_LIGHTS + i * 2 + 1].value = red; lights[BYPASS_TRIG_LIGHTS + i * 2 + 0].value = white; lights[BYPASS_TRIG_LIGHTS + i * 2 + 1].value = red; trigLights[i] -= (trigLights[i] / lightLambda) * (float)engineGetSampleTime(); } }// step() int prepareNoise(float* stepNoises, int sh) { int noiseIndex = abs( noiseSources[sh] ); if (stepNoises[noiseIndex] == nullNoise) { if (stepNoises[0] == nullNoise) stepNoises[0] = whiteNoise.white(); if ((noiseIndex == PINK || noiseIndex == BLUE) && stepNoises[5] == 0.0f) { pinkFilter.process(stepNoises[0]); stepNoises[5] = 1.0f; } switch (noiseIndex) { // most of the code in here is from Joel Robichaud - Nohmad Noise module case (PINK) : stepNoises[noiseIndex] = 5.0f * clamp(0.18f * pinkFilter.pink(), -1.0f, 1.0f); break; case (RED) : redFilter.process(stepNoises[0]); stepNoises[noiseIndex] = 5.0f * clamp(7.8f * redFilter.lowpass(), -1.0f, 1.0f); break; case (BLUE) : blueFilter.process(pinkFilter.pink()); stepNoises[noiseIndex] = 5.0f * clamp(0.64f * blueFilter.highpass(), -1.0f, 1.0f); break; default ://(WHITE) stepNoises[noiseIndex] = 5.0f * stepNoises[0]; break; } } return noiseIndex; } }; struct BranesWidget : ModuleWidget { struct PanelThemeItem : MenuItem { Branes *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); Branes *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; } BranesWidget(Branes *module) : ModuleWidget(module) { // Main panel from Inkscape DynamicSVGPanel *panel = new DynamicSVGPanel(); panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/BranesBG-01.svg"))); //panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/BranesBG-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 rowRulerHoldA = 119.5; static constexpr float rowRulerHoldB = 248.5f; static constexpr float radiusIn = 35.0f; static constexpr float radiusOut = 64.0f; static constexpr float offsetIn = 25.0f; static constexpr float offsetOut = 46.0f; // BraneA trig intput addInput(createDynamicPort(Vec(colRulerCenter, rowRulerHoldA), Port::INPUT, module, Branes::TRIG_INPUTS + 0, &module->panelTheme)); // BraneA inputs addInput(createDynamicPort(Vec(colRulerCenter - offsetIn, rowRulerHoldA + offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 0, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - radiusIn, rowRulerHoldA), Port::INPUT, module, Branes::IN_INPUTS + 1, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - offsetIn, rowRulerHoldA - offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 2, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter, rowRulerHoldA - radiusIn), Port::INPUT, module, Branes::IN_INPUTS + 3, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetIn, rowRulerHoldA - offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 4, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + radiusIn, rowRulerHoldA), Port::INPUT, module, Branes::IN_INPUTS + 5, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetIn, rowRulerHoldA + offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 6, &module->panelTheme)); // BraneA outputs addOutput(createDynamicPort(Vec(colRulerCenter - offsetOut, rowRulerHoldA + offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 0, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - radiusOut, rowRulerHoldA), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 1, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetOut, rowRulerHoldA - offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 2, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter, rowRulerHoldA - radiusOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 3, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + offsetOut, rowRulerHoldA - offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 4, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + radiusOut, rowRulerHoldA), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 5, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + offsetOut, rowRulerHoldA + offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 6, &module->panelTheme)); // BraneB trig intput addInput(createDynamicPort(Vec(colRulerCenter, rowRulerHoldB), Port::INPUT, module, Branes::TRIG_INPUTS + 1, &module->panelTheme)); // BraneB inputs addInput(createDynamicPort(Vec(colRulerCenter + offsetIn, rowRulerHoldB - offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 7, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + radiusIn, rowRulerHoldB), Port::INPUT, module, Branes::IN_INPUTS + 8, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + offsetIn, rowRulerHoldB + offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 9, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter, rowRulerHoldB + radiusIn), Port::INPUT, module, Branes::IN_INPUTS + 10, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - offsetIn, rowRulerHoldB + offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 11, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - radiusIn, rowRulerHoldB), Port::INPUT, module, Branes::IN_INPUTS + 12, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter - offsetIn, rowRulerHoldB - offsetIn), Port::INPUT, module, Branes::IN_INPUTS + 13, &module->panelTheme)); // BraneB outputs addOutput(createDynamicPort(Vec(colRulerCenter + offsetOut, rowRulerHoldB - offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 7, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + radiusOut, rowRulerHoldB), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 8, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter + offsetOut, rowRulerHoldB + offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 9, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter, rowRulerHoldB + radiusOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 10, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetOut, rowRulerHoldB + offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 11, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - radiusOut, rowRulerHoldB), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 12, &module->panelTheme)); addOutput(createDynamicPort(Vec(colRulerCenter - offsetOut, rowRulerHoldB - offsetOut), Port::OUTPUT, module, Branes::OUT_OUTPUTS + 13, &module->panelTheme)); static constexpr float rowRulerBypass = 345.5f; // Trigger bypass // buttons addParam(createDynamicParam(Vec(colRulerCenter - 32.0f, rowRulerBypass), module, Branes::TRIG_BYPASS_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerCenter + 32.0f, rowRulerBypass), module, Branes::TRIG_BYPASS_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // cv inputs addInput(createDynamicPort(Vec(colRulerCenter - 65.0f, rowRulerBypass), Port::INPUT, module, Branes::TRIG_BYPASS_INPUTS + 0, &module->panelTheme)); addInput(createDynamicPort(Vec(colRulerCenter + 65.0f, rowRulerBypass), Port::INPUT, module, Branes::TRIG_BYPASS_INPUTS + 1, &module->panelTheme)); // LEDs bottom addChild(createLightCentered>(Vec(colRulerCenter - 46.5f, rowRulerBypass), module, Branes::BYPASS_CV_LIGHTS + 0 * 2)); addChild(createLightCentered>(Vec(colRulerCenter + 46.5f, rowRulerBypass), module, Branes::BYPASS_CV_LIGHTS + 1 * 2)); // LEDs top addChild(createLightCentered>(Vec(colRulerCenter + 5.5f, rowRulerHoldA + 19.5f), module, Branes::BYPASS_TRIG_LIGHTS + 0 * 2)); addChild(createLightCentered>(Vec(colRulerCenter - 5.5f, rowRulerHoldB - 19.5f), module, Branes::BYPASS_TRIG_LIGHTS + 1 * 2)); } }; } // namespace rack_plugin_Geodesics using namespace rack_plugin_Geodesics; RACK_PLUGIN_MODEL_INIT(Geodesics, Branes) { Model *modelBranes = Model::create("Geodesics", "Branes", "Branes", SAMPLE_AND_HOLD_TAG); return modelBranes; } /*CHANGE LOG 0.6.0: created */