You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

413 lines
18KB

  1. //***********************************************************************************************
  2. //Gravitational Voltage Controled Amplifiers module for VCV Rack by Pierre Collard and Marc Boulé
  3. //
  4. //Based on code from the Fundamental plugins by Andrew Belt and graphics
  5. // from the Component Library by Wes Milholen.
  6. //See ./LICENSE.txt for all licenses
  7. //See ./res/fonts/ for font licenses
  8. //
  9. //***********************************************************************************************
  10. #include "Geodesics.hpp"
  11. namespace rack_plugin_Geodesics {
  12. struct BlackHoles : Module {
  13. enum ParamIds {
  14. ENUMS(LEVEL_PARAMS, 8),// -1.0f to 1.0f knob, set to default (0.0f) when using CV input
  15. ENUMS(EXP_PARAMS, 2),// push-button
  16. WORMHOLE_PARAM,
  17. ENUMS(CVLEVEL_PARAMS, 2),// push-button
  18. NUM_PARAMS
  19. };
  20. enum InputIds {
  21. ENUMS(IN_INPUTS, 8),// -10 to 10 V
  22. ENUMS(LEVELCV_INPUTS, 8),// 0 to 10V CV or -5 to 5V depeding on cvMode
  23. NUM_INPUTS
  24. };
  25. enum OutputIds {
  26. ENUMS(OUT_OUTPUTS, 8),// input * [-1;1] when input connected, else [-10;10] CV when input unconnected
  27. ENUMS(BLACKHOLE_OUTPUTS, 2),
  28. NUM_OUTPUTS
  29. };
  30. enum LightIds {
  31. ENUMS(EXP_LIGHTS, 2),
  32. ENUMS(WORMHOLE_LIGHT, 2),// room for WhiteRed
  33. ENUMS(CVALEVEL_LIGHTS, 2),// White, but two lights (light 0 is cvMode bit = 0, light 1 is cvMode bit = 1)
  34. ENUMS(CVBLEVEL_LIGHTS, 2),// White, but two lights
  35. NUM_LIGHTS
  36. };
  37. // Constants
  38. static constexpr float expBase = 50.0f;
  39. // Need to save, with reset
  40. bool isExponential[2];
  41. bool wormhole;
  42. int cvMode;// 0 is -5v to 5v, 1 is -10v to 10v; bit 0 is upper BH, bit 1 is lower BH
  43. // Need to save, no reset
  44. int panelTheme;
  45. // No need to save, with reset
  46. // none
  47. // No need to save, no reset
  48. SchmittTrigger expTriggers[2];
  49. SchmittTrigger cvLevelTriggers[2];
  50. SchmittTrigger wormholeTrigger;
  51. BlackHoles() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
  52. // Need to save, no reset
  53. panelTheme = 0;
  54. // No need to save, no reset
  55. expTriggers[0].reset();
  56. expTriggers[1].reset();
  57. onReset();
  58. }
  59. // widgets are not yet created when module is created
  60. // even if widgets not created yet, can use params[] and should handle 0.0f value since step may call
  61. // this before widget creation anyways
  62. // called from the main thread if by constructor, called by engine thread if right-click initialization
  63. // when called by constructor, module is created before the first step() is called
  64. void onReset() override {
  65. // Need to save, with reset
  66. cvMode = 0x3;
  67. isExponential[0] = false;
  68. isExponential[1] = false;
  69. wormhole = true;
  70. // No need to save, with reset
  71. // none
  72. }
  73. // widgets randomized before onRandomize() is called
  74. // called by engine thread if right-click randomize
  75. void onRandomize() override {
  76. // Need to save, with reset
  77. for (int i = 0; i < 2; i++) {
  78. isExponential[i] = (randomu32() % 2) > 0;
  79. }
  80. wormhole = (randomu32() % 2) > 0;
  81. // No need to save, with reset
  82. // none
  83. }
  84. // called by main thread
  85. json_t *toJson() override {
  86. json_t *rootJ = json_object();
  87. // Need to save (reset or not)
  88. // isExponential
  89. json_object_set_new(rootJ, "isExponential0", json_real(isExponential[0]));
  90. json_object_set_new(rootJ, "isExponential1", json_real(isExponential[1]));
  91. // wormhole
  92. json_object_set_new(rootJ, "wormhole", json_boolean(wormhole));
  93. // panelTheme
  94. json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme));
  95. // cvMode
  96. json_object_set_new(rootJ, "cvMode", json_integer(cvMode));
  97. return rootJ;
  98. }
  99. // widgets have their fromJson() called before this fromJson() is called
  100. // called by main thread
  101. void fromJson(json_t *rootJ) override {
  102. // Need to save (reset or not)
  103. // isExponential
  104. json_t *isExponential0J = json_object_get(rootJ, "isExponential0");
  105. if (isExponential0J)
  106. isExponential[0] = json_real_value(isExponential0J);
  107. json_t *isExponential1J = json_object_get(rootJ, "isExponential1");
  108. if (isExponential1J)
  109. isExponential[1] = json_real_value(isExponential1J);
  110. // wormhole
  111. json_t *wormholeJ = json_object_get(rootJ, "wormhole");
  112. if (wormholeJ)
  113. wormhole = json_is_true(wormholeJ);
  114. // panelTheme
  115. json_t *panelThemeJ = json_object_get(rootJ, "panelTheme");
  116. if (panelThemeJ)
  117. panelTheme = json_integer_value(panelThemeJ);
  118. // cvMode
  119. json_t *cvModeJ = json_object_get(rootJ, "cvMode");
  120. if (cvModeJ)
  121. cvMode = json_integer_value(cvModeJ);
  122. // No need to save, with reset
  123. // none
  124. }
  125. // Advances the module by 1 audio frame with duration 1.0 / engineGetSampleRate()
  126. void step() override {
  127. // Exponential buttons
  128. for (int i = 0; i < 2; i++)
  129. if (expTriggers[i].process(params[EXP_PARAMS + i].value)) {
  130. isExponential[i] = !isExponential[i];
  131. }
  132. // Wormhole buttons
  133. if (wormholeTrigger.process(params[WORMHOLE_PARAM].value)) {
  134. wormhole = ! wormhole;
  135. }
  136. // CV Level buttons
  137. for (int i = 0; i < 2; i++) {
  138. if (cvLevelTriggers[i].process(params[CVLEVEL_PARAMS + i].value))
  139. cvMode ^= (0x1 << i);
  140. }
  141. // BlackHole 0 all outputs
  142. float blackHole0 = 0.0f;
  143. float inputs0[4] = {10.0f, 10.0f, 10.0f, 10.0f};// default to generate CV when no input connected
  144. for (int i = 0; i < 4; i++)
  145. if (inputs[IN_INPUTS + i].active)
  146. inputs0[i] = inputs[IN_INPUTS + i].value;
  147. for (int i = 0; i < 4; i++) {
  148. float chanVal = calcChannel(inputs0[i], params[LEVEL_PARAMS + i], inputs[LEVELCV_INPUTS + i], isExponential[0], cvMode & 0x1);
  149. outputs[OUT_OUTPUTS + i].value = chanVal;
  150. blackHole0 += chanVal;
  151. }
  152. outputs[BLACKHOLE_OUTPUTS + 0].value = clamp(blackHole0, -10.0f, 10.0f);
  153. // BlackHole 1 all outputs
  154. float blackHole1 = 0.0f;
  155. float inputs1[4] = {10.0f, 10.0f, 10.0f, 10.0f};// default to generate CV when no input connected
  156. bool allUnconnected = true;
  157. for (int i = 0; i < 4; i++)
  158. if (inputs[IN_INPUTS + i + 4].active) {
  159. inputs1[i] = inputs[IN_INPUTS + i + 4].value;
  160. allUnconnected = false;
  161. }
  162. if (allUnconnected && wormhole)
  163. for (int i = 0; i < 4; i++)
  164. inputs1[i] = blackHole0;
  165. for (int i = 0; i < 4; i++) {
  166. float chanVal = calcChannel(inputs1[i], params[LEVEL_PARAMS + i + 4], inputs[LEVELCV_INPUTS + i + 4], isExponential[1], cvMode >> 1);
  167. outputs[OUT_OUTPUTS + i + 4].value = chanVal;
  168. blackHole1 += chanVal;
  169. }
  170. outputs[BLACKHOLE_OUTPUTS + 1].value = clamp(blackHole1, -10.0f, 10.0f);
  171. // Wormhole light
  172. lights[WORMHOLE_LIGHT + 0].value = ((wormhole && allUnconnected) ? 1.0f : 0.0f);
  173. lights[WORMHOLE_LIGHT + 1].value = ((wormhole && !allUnconnected) ? 1.0f : 0.0f);
  174. // isExponential lights
  175. for (int i = 0; i < 2; i++)
  176. lights[EXP_LIGHTS + i].value = isExponential[i] ? 1.0f : 0.0f;
  177. // CV Level lights
  178. lights[CVALEVEL_LIGHTS + 0].value = (cvMode & 0x1) == 0 ? 1.0f : 0.0f;
  179. lights[CVALEVEL_LIGHTS + 1].value = 1.0f - lights[CVALEVEL_LIGHTS + 0].value;
  180. lights[CVBLEVEL_LIGHTS + 0].value = (cvMode & 0x2) == 0 ? 1.0f : 0.0f;
  181. lights[CVBLEVEL_LIGHTS + 1].value = 1.0f - lights[CVBLEVEL_LIGHTS + 0].value;
  182. }// step()
  183. float calcChannel(float in, Param &level, Input &levelCV, bool isExp, int cvMode) {
  184. float levCv = levelCV.active ? (levelCV.value / (cvMode != 0 ? 10.0f : 5.0f)) : 0.0f;
  185. float lev = clamp(level.value + levCv, -1.0f, 1.0f);
  186. if (isExp) {
  187. float newlev = rescale(powf(expBase, fabs(lev)), 1.0f, expBase, 0.0f, 1.0f);
  188. if (lev < 0.0f)
  189. newlev *= -1.0f;
  190. lev = newlev;
  191. }
  192. float ret = lev * in;
  193. return ret;
  194. }
  195. };
  196. struct BlackHolesWidget : ModuleWidget {
  197. struct PanelThemeItem : MenuItem {
  198. BlackHoles *module;
  199. int theme;
  200. void onAction(EventAction &e) override {
  201. module->panelTheme = theme;
  202. }
  203. void step() override {
  204. rightText = (module->panelTheme == theme) ? "✔" : "";
  205. }
  206. };
  207. Menu *createContextMenu() override {
  208. Menu *menu = ModuleWidget::createContextMenu();
  209. MenuLabel *spacerLabel = new MenuLabel();
  210. menu->addChild(spacerLabel);
  211. BlackHoles *module = dynamic_cast<BlackHoles*>(this->module);
  212. assert(module);
  213. MenuLabel *themeLabel = new MenuLabel();
  214. themeLabel->text = "Panel Theme";
  215. menu->addChild(themeLabel);
  216. PanelThemeItem *lightItem = new PanelThemeItem();
  217. lightItem->text = lightPanelID;// Geodesics.hpp
  218. lightItem->module = module;
  219. lightItem->theme = 0;
  220. menu->addChild(lightItem);
  221. PanelThemeItem *darkItem = new PanelThemeItem();
  222. darkItem->text = darkPanelID;// Geodesics.hpp
  223. darkItem->module = module;
  224. darkItem->theme = 1;
  225. //menu->addChild(darkItem);
  226. return menu;
  227. }
  228. BlackHolesWidget(BlackHoles *module) : ModuleWidget(module) {
  229. // Main panel from Inkscape
  230. DynamicSVGPanel *panel = new DynamicSVGPanel();
  231. panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/BlackHolesBG-01.svg")));
  232. //panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/BlackHolesBG-02.svg")));// no dark pannel for now
  233. box.size = panel->box.size;
  234. panel->mode = &module->panelTheme;
  235. addChild(panel);
  236. // Screws
  237. // part of svg panel, no code required
  238. float colRulerCenter = box.size.x / 2.0f;
  239. static constexpr float rowRulerBlack0 = 108.5f;
  240. static constexpr float rowRulerBlack1 = 272.5f;
  241. static constexpr float radiusIn = 30.0f;
  242. static constexpr float radiusOut = 61.0f;
  243. static constexpr float offsetL = 53.0f;
  244. static constexpr float offsetS = 30.0f;
  245. // BlackHole0 knobs
  246. addParam(createDynamicParam<GeoKnob>(Vec(colRulerCenter, rowRulerBlack0 - radiusOut), module, BlackHoles::LEVEL_PARAMS + 0, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  247. addParam(createDynamicParam<GeoKnobRight>(Vec(colRulerCenter + radiusOut, rowRulerBlack0), module, BlackHoles::LEVEL_PARAMS + 1, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  248. addParam(createDynamicParam<GeoKnobBottom>(Vec(colRulerCenter, rowRulerBlack0 + radiusOut), module, BlackHoles::LEVEL_PARAMS + 2, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  249. addParam(createDynamicParam<GeoKnobLeft>(Vec(colRulerCenter - radiusOut, rowRulerBlack0), module, BlackHoles::LEVEL_PARAMS + 3, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  250. // BlackHole0 level CV inputs
  251. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack0 - radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 0, &module->panelTheme));
  252. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + radiusIn, rowRulerBlack0), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 1, &module->panelTheme));
  253. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack0 + radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 2, &module->panelTheme));
  254. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - radiusIn, rowRulerBlack0), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 3, &module->panelTheme));
  255. // BlackHole0 inputs
  256. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack0 - offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 0, &module->panelTheme));
  257. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack0 - offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 1, &module->panelTheme));
  258. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack0 + offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 2, &module->panelTheme));
  259. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack0 + offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 3, &module->panelTheme));
  260. // BlackHole0 outputs
  261. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack0 - offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 0, &module->panelTheme));
  262. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack0 + offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 1, &module->panelTheme));
  263. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack0 + offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 2, &module->panelTheme));
  264. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack0 - offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 3, &module->panelTheme));
  265. // BlackHole0 center output
  266. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack0), Port::OUTPUT, module, BlackHoles::BLACKHOLE_OUTPUTS + 0, &module->panelTheme));
  267. // BlackHole1 knobs
  268. addParam(createDynamicParam<GeoKnob>(Vec(colRulerCenter, rowRulerBlack1 - radiusOut), module, BlackHoles::LEVEL_PARAMS + 4, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  269. addParam(createDynamicParam<GeoKnobRight>(Vec(colRulerCenter + radiusOut, rowRulerBlack1), module, BlackHoles::LEVEL_PARAMS + 5, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  270. addParam(createDynamicParam<GeoKnobBottom>(Vec(colRulerCenter, rowRulerBlack1 + radiusOut), module, BlackHoles::LEVEL_PARAMS + 6, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  271. addParam(createDynamicParam<GeoKnobLeft>(Vec(colRulerCenter - radiusOut, rowRulerBlack1), module, BlackHoles::LEVEL_PARAMS + 7, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  272. // BlackHole1 level CV inputs
  273. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack1 - radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 4, &module->panelTheme));
  274. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + radiusIn, rowRulerBlack1), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 5, &module->panelTheme));
  275. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack1 + radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 6, &module->panelTheme));
  276. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - radiusIn, rowRulerBlack1), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 7, &module->panelTheme));
  277. // BlackHole1 inputs
  278. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack1 - offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 4, &module->panelTheme));
  279. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack1 - offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 5, &module->panelTheme));
  280. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack1 + offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 6, &module->panelTheme));
  281. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack1 + offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 7, &module->panelTheme));
  282. // BlackHole1 outputs
  283. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack1 - offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 4, &module->panelTheme));
  284. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack1 + offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 5, &module->panelTheme));
  285. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack1 + offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 6, &module->panelTheme));
  286. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack1 - offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 7, &module->panelTheme));
  287. // BlackHole1 center output
  288. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack1), Port::OUTPUT, module, BlackHoles::BLACKHOLE_OUTPUTS + 1, &module->panelTheme));
  289. static constexpr float offsetButtonsX = 62.0f;
  290. static constexpr float offsetButtonsY = 64.0f;
  291. static constexpr float offsetLedVsBut = 9.0f;
  292. static constexpr float offsetLedVsButS = 5.0f;// small
  293. static constexpr float offsetLedVsButL = 12.0f;// large
  294. // BlackHole0 Exp button and light
  295. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack0 + offsetButtonsY), module, BlackHoles::EXP_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  296. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack0 + offsetButtonsY - offsetLedVsBut - 1.0f), module, BlackHoles::EXP_LIGHTS + 0));
  297. // BlackHole1 Exp button and light
  298. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack1 + offsetButtonsY), module, BlackHoles::EXP_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  299. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack1 + offsetButtonsY - offsetLedVsBut -1.0f), module, BlackHoles::EXP_LIGHTS + 1));
  300. // Wormhole button and light
  301. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack1 - offsetButtonsY), module, BlackHoles::WORMHOLE_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  302. addChild(createLightCentered<SmallLight<GeoWhiteRedLight>>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack1 - offsetButtonsY + offsetLedVsBut), module, BlackHoles::WORMHOLE_LIGHT));
  303. // CV Level A button and light
  304. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter + offsetButtonsX, rowRulerBlack0 + offsetButtonsY), module, BlackHoles::CVLEVEL_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  305. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButL, rowRulerBlack0 + offsetButtonsY + offsetLedVsButS), module, BlackHoles::CVALEVEL_LIGHTS + 0));
  306. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButS, rowRulerBlack0 + offsetButtonsY + offsetLedVsButL), module, BlackHoles::CVALEVEL_LIGHTS + 1));
  307. // CV Level B button and light
  308. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter + offsetButtonsX, rowRulerBlack1 + offsetButtonsY), module, BlackHoles::CVLEVEL_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  309. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButL, rowRulerBlack1 + offsetButtonsY + offsetLedVsButS), module, BlackHoles::CVBLEVEL_LIGHTS + 0));
  310. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButS, rowRulerBlack1 + offsetButtonsY + offsetLedVsButL), module, BlackHoles::CVBLEVEL_LIGHTS + 1));
  311. }
  312. };
  313. } // namespace rack_plugin_Geodesics
  314. using namespace rack_plugin_Geodesics;
  315. RACK_PLUGIN_MODEL_INIT(Geodesics, BlackHoles) {
  316. Model *modelBlackHoles = Model::create<BlackHoles, BlackHolesWidget>("Geodesics", "BlackHoles", "BlackHoles", AMPLIFIER_TAG);
  317. return modelBlackHoles;
  318. }
  319. /*CHANGE LOG
  320. 0.6.1:
  321. add CV level modes buttons and lights
  322. change CV level behavior
  323. 0.6.0:
  324. created
  325. */