#include "plugin.hpp" struct MotionMTR : Module { enum ParamId { MODE1_PARAM, CTRL_1_PARAM, MODE2_PARAM, CTRL_2_PARAM, MODE3_PARAM, CTRL_3_PARAM, PARAMS_LEN }; enum InputId { IN1_INPUT, IN2_INPUT, IN3_INPUT, INPUTS_LEN }; enum OutputId { OUT1_OUTPUT, OUT2_OUTPUT, OUT3_OUTPUT, OUTPUTS_LEN }; enum LightDisplayType { CV_INV, CV_ATT, AUDIO }; const std::vector modeLabels = {"CV Inversion", "CV Attentuation", "Audio"}; static const int NUM_LIGHTS_PER_DIAL = 20; enum LightId { ENUMS(LIGHT_1, NUM_LIGHTS_PER_DIAL * 3), ENUMS(LIGHT_2, NUM_LIGHTS_PER_DIAL * 3), ENUMS(LIGHT_3, NUM_LIGHTS_PER_DIAL * 3), LIGHTS_LEN }; struct Map { float dbValue; long ledNumber; }; const Map lut[20] = { { -30.5, 1}, { -30, 2}, { -30, 3}, { -26, 4}, { -25, 5}, { -24, 6}, { -23, 7 }, { -22, 8 }, { -21, 9}, { -20, 10}, { -19, 11}, { -18, 12}, { -16, 13}, { -14, 14}, { -12, 15}, { -10, 16}, { -8, 17}, { -6, 18}, { -4, 19}, { -2, 20} }; dsp::VuMeter2 vuBar[3]; const int updateLEDRate = 16; dsp::ClockDivider sliderUpdate; bool startingUp = true; dsp::Timer startupTimer; MotionMTR() { config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); configSwitch(MODE1_PARAM, 0.f, 2.f, 1.f, "Channel 1 mode", modeLabels); configParam(CTRL_1_PARAM, 0.f, 1.f, 0.f, "Channel 1 gain"); configSwitch(MODE2_PARAM, 0.f, 2.f, 1.f, "Channel 2 mode", modeLabels); configParam(CTRL_2_PARAM, 0.f, 1.f, 0.f, "Channel 2 gain"); configSwitch(MODE3_PARAM, 0.f, 2.f, 1.f, "Channel 3 mode", modeLabels); configParam(CTRL_3_PARAM, 0.f, 1.f, 0.f, "Channel 3 gain"); auto in1 = configInput(IN1_INPUT, "Channel 1"); in1->description = "Normalled to 10V (except in audio mode)"; auto in2 = configInput(IN2_INPUT, "Channel 2"); in2->description = "Normalled to 10V (except in audio mode)"; auto in3 = configInput(IN3_INPUT, "Channel 3"); in3->description = "Normalled to 10V (except in audio mode)"; configOutput(OUT1_OUTPUT, "Channel 1"); configOutput(OUT2_OUTPUT, "Channel 2"); configOutput(OUT3_OUTPUT, "Channel 3 (Mix)"); for (int i = 1; i < NUM_LIGHTS_PER_DIAL; ++i) { configLight(LIGHT_1 + i * 3, string::f("%g to %g dB", lut[i - 1].dbValue, lut[i].dbValue)); configLight(LIGHT_2 + i * 3, string::f("%g to %g dB", lut[i - 1].dbValue, lut[i].dbValue)); configLight(LIGHT_3 + i * 3, string::f("%g to %g dB", lut[i - 1].dbValue, lut[i].dbValue)); } for (int i = 0; i < 3; ++i) { vuBar[i].mode = dsp::VuMeter2::PEAK; vuBar[i].lambda = 10.f * updateLEDRate; } // only poll EQ sliders every 16 samples sliderUpdate.setDivision(updateLEDRate); // timer for startup animation startupTimer.reset(); } void onReset(const ResetEvent& e) override { startingUp = true; startupTimer.reset(); Module::onReset(e); } void process(const ProcessArgs& args) override { const LightDisplayType mode1 = (LightDisplayType) params[MODE1_PARAM].getValue(); const LightDisplayType mode2 = (LightDisplayType) params[MODE2_PARAM].getValue(); const LightDisplayType mode3 = (LightDisplayType) params[MODE3_PARAM].getValue(); const float in1 = inputs[IN1_INPUT].getNormalVoltage(10.f * (mode1 != AUDIO)); const float in2 = inputs[IN2_INPUT].getNormalVoltage(10.f * (mode2 != AUDIO)); const float in3 = inputs[IN3_INPUT].getNormalVoltage(10.f * (mode3 != AUDIO)); const float out1 = in1 * params[CTRL_1_PARAM].getValue() * (mode1 == CV_INV ? -1 : +1); const float out2 = in2 * params[CTRL_2_PARAM].getValue() * (mode2 == CV_INV ? -1 : +1); float out3 = in3 * params[CTRL_3_PARAM].getValue() * (mode3 == CV_INV ? -1 : +1); if (!outputs[OUT1_OUTPUT].isConnected()) { out3 += out1; } if (!outputs[OUT2_OUTPUT].isConnected()) { out3 += out2; } // special light pattern when starting up :) if (startingUp) { processStartup(args); } // otherwise (periodically) update LEDS according to value else if (sliderUpdate.process()) { lightsForSignal(mode1, LIGHT_1, out1, args, 0); lightsForSignal(mode2, LIGHT_2, out2, args, 1); lightsForSignal(mode3, LIGHT_3, out3, args, 2); } outputs[OUT1_OUTPUT].setVoltage(out1); outputs[OUT2_OUTPUT].setVoltage(out2); outputs[OUT3_OUTPUT].setVoltage(out3); } void processStartup(const ProcessArgs& args) { float time = startupTimer.process(args.sampleTime); const float ringTime = 0.4; if (time < ringTime) { int light = floor(NUM_LIGHTS_PER_DIAL * time / ringTime); setLightHSBSmooth(LIGHT_1 + 3 * light, args, 360 * time / ringTime, 1., 1.); } else if (time < 2 * ringTime) { time -= ringTime; int light = floor(NUM_LIGHTS_PER_DIAL * time / ringTime); setLightHSBSmooth(LIGHT_2 + 3 * light, args, 360 * time / ringTime, 1., 1.); } else if (time < 3 * ringTime) { time -= 2 * ringTime; int light = floor(NUM_LIGHTS_PER_DIAL * time / ringTime); setLightHSBSmooth(LIGHT_3 + 3 * light, args, 360 * time / ringTime, 1., 1.); } else { startingUp = false; } } void setLightRGB(int lightId, float R, float G, float B) { lights[lightId + 0].setBrightness(R); lights[lightId + 1].setBrightness(G); lights[lightId + 2].setBrightness(B); } void setLightRGBSmooth(int lightId, const ProcessArgs& args, float R, float G, float B) { // inverse time constant for LED smoothing const float lambda = 10.f * updateLEDRate; lights[lightId + 0].setBrightnessSmooth(R, args.sampleTime, lambda); lights[lightId + 1].setBrightnessSmooth(G, args.sampleTime, lambda); lights[lightId + 2].setBrightnessSmooth(B, args.sampleTime, lambda); } // hue: 0 - 360 void setLightHSBSmooth(int lightId, const ProcessArgs& args, float H, float S, float V) { float C = S * V; float X = C * (1 - std::abs(std::fmod(H / 60.0, 2) - 1)); float m = V - C; float r, g, b; if (H >= 0 && H < 60) { r = C, g = X, b = 0; } else if (H >= 60 && H < 120) { r = X, g = C, b = 0; } else if (H >= 120 && H < 180) { r = 0, g = C, b = X; } else if (H >= 180 && H < 240) { r = 0, g = X, b = C; } else if (H >= 240 && H < 300) { r = X, g = 0, b = C; } else { r = C, g = 0, b = X; } float R = (r + m); float G = (g + m); float B = (b + m); setLightRGBSmooth(lightId, args, R, G, B); } void lightsForSignal(LightDisplayType type, const LightId lightId, float signal, const ProcessArgs& args, const int channel) { if (type == AUDIO) { setLightRGB(lightId, 0.f, 1.0f, 0.f); vuBar[channel].process(args.sampleTime * updateLEDRate, signal / 10.f); for (int i = 1; i < NUM_LIGHTS_PER_DIAL; i++) { const float value = vuBar[channel].getBrightness(lut[i - 1].dbValue, lut[i].dbValue); if (i < 15) { // green setLightRGB(lightId + 3 * i, 0.f, value, 0.f); } else if (i < NUM_LIGHTS_PER_DIAL - 1) { // yellow setLightRGB(lightId + 3 * i, value, 0.65 * value, 0.f); } else { // red setLightRGB(lightId + 3 * i, value, 0.f, 0.f); } } } else { setLightRGBSmooth(lightId, args, 0.82f, 0.0f, 0.82f); if (signal >= 0) { for (int i = 1; i < NUM_LIGHTS_PER_DIAL; ++i) { float value = 0.5f * (signal > (10 * (i + 1.) / (NUM_LIGHTS_PER_DIAL + 1))); // purple setLightRGBSmooth(lightId + 3 * i, args, 0.82f * value, 0.0f, 0.82f * value); } } else { for (int i = 1; i < NUM_LIGHTS_PER_DIAL; ++i) { float value = (signal < (-10 * (NUM_LIGHTS_PER_DIAL - i + 1.) / (NUM_LIGHTS_PER_DIAL + 1.))); setLightRGBSmooth(lightId + 3 * i, args, value, 0.65f * value, 0.f); } } } } }; struct MotionMTRWidget : ModuleWidget { MotionMTRWidget(MotionMTR* module) { setModule(module); setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/MotionMTR.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParam(mm2px(Vec(1.298, 17.851)), module, MotionMTR::MODE1_PARAM)); addParam(createParamCentered(mm2px(Vec(18.217, 22.18)), module, MotionMTR::CTRL_1_PARAM)); addParam(createParam(mm2px(Vec(23.762, 46.679)), module, MotionMTR::MODE2_PARAM)); addParam(createParamCentered(mm2px(Vec(11.777, 50.761)), module, MotionMTR::CTRL_2_PARAM)); addParam(createParam(mm2px(Vec(1.34, 74.461)), module, MotionMTR::MODE3_PARAM)); addParam(createParamCentered(mm2px(Vec(18.31, 78.89)), module, MotionMTR::CTRL_3_PARAM)); addInput(createInputCentered(mm2px(Vec(5.008, 100.315)), module, MotionMTR::IN1_INPUT)); addInput(createInputCentered(mm2px(Vec(14.993, 100.315)), module, MotionMTR::IN2_INPUT)); addInput(createInputCentered(mm2px(Vec(24.978, 100.315)), module, MotionMTR::IN3_INPUT)); addOutput(createOutputCentered(mm2px(Vec(5.0, 113.207)), module, MotionMTR::OUT1_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(14.978, 113.185)), module, MotionMTR::OUT2_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(25.014, 113.207)), module, MotionMTR::OUT3_OUTPUT)); struct LightRingDetails { MotionMTR::LightId startingId; float x, y; // centre } ; std::vector ringDetails = { {MotionMTR::LIGHT_1, 18.217, 22.18}, {MotionMTR::LIGHT_2, 11.777, 50.761}, {MotionMTR::LIGHT_3, 18.217, 78.85} }; const float R = 9.65; // mm for (auto detailForRing : ringDetails) { for (int i = 0; i < MotionMTR::NUM_LIGHTS_PER_DIAL; ++i) { float theta = 2 * M_PI * i / MotionMTR::NUM_LIGHTS_PER_DIAL; float x = detailForRing.x + sin(theta) * R; float y = detailForRing.y - cos(theta) * R; addChild(createLightCentered>(mm2px(Vec(x, y)), module, detailForRing.startingId + 3 * i)); } } } }; Model* modelMotionMTR = createModel("MotionMTR");