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.

393 lines
17KB

  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. WORMHOLE_LIGHT,
  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
  40. int panelTheme = 0;
  41. bool isExponential[2];
  42. bool wormhole;
  43. int cvMode;// 0 is -5v to 5v, 1 is -10v to 10v; bit 0 is upper BH, bit 1 is lower BH
  44. // No need to save
  45. Trigger expTriggers[2];
  46. Trigger cvLevelTriggers[2];
  47. Trigger wormholeTrigger;
  48. unsigned int lightRefreshCounter = 0;
  49. BlackHoles() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
  50. onReset();
  51. }
  52. void onReset() override {
  53. isExponential[0] = false;
  54. isExponential[1] = false;
  55. wormhole = true;
  56. cvMode = 0x3;
  57. }
  58. void onRandomize() override {
  59. for (int i = 0; i < 2; i++) {
  60. isExponential[i] = (randomu32() % 2) > 0;
  61. }
  62. wormhole = (randomu32() % 2) > 0;
  63. }
  64. json_t *toJson() override {
  65. json_t *rootJ = json_object();
  66. // isExponential
  67. json_object_set_new(rootJ, "isExponential0", json_real(isExponential[0]));
  68. json_object_set_new(rootJ, "isExponential1", json_real(isExponential[1]));
  69. // wormhole
  70. json_object_set_new(rootJ, "wormhole", json_boolean(wormhole));
  71. // panelTheme
  72. json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme));
  73. // cvMode
  74. json_object_set_new(rootJ, "cvMode", json_integer(cvMode));
  75. return rootJ;
  76. }
  77. void fromJson(json_t *rootJ) override {
  78. // isExponential
  79. json_t *isExponential0J = json_object_get(rootJ, "isExponential0");
  80. if (isExponential0J)
  81. isExponential[0] = json_number_value(isExponential0J);
  82. json_t *isExponential1J = json_object_get(rootJ, "isExponential1");
  83. if (isExponential1J)
  84. isExponential[1] = json_number_value(isExponential1J);
  85. // wormhole
  86. json_t *wormholeJ = json_object_get(rootJ, "wormhole");
  87. if (wormholeJ)
  88. wormhole = json_is_true(wormholeJ);
  89. // panelTheme
  90. json_t *panelThemeJ = json_object_get(rootJ, "panelTheme");
  91. if (panelThemeJ)
  92. panelTheme = json_integer_value(panelThemeJ);
  93. // cvMode
  94. json_t *cvModeJ = json_object_get(rootJ, "cvMode");
  95. if (cvModeJ)
  96. cvMode = json_integer_value(cvModeJ);
  97. }
  98. void step() override {
  99. if ((lightRefreshCounter & userInputsStepSkipMask) == 0) {
  100. // Exponential buttons
  101. for (int i = 0; i < 2; i++)
  102. if (expTriggers[i].process(params[EXP_PARAMS + i].value)) {
  103. isExponential[i] = !isExponential[i];
  104. }
  105. // Wormhole buttons
  106. if (wormholeTrigger.process(params[WORMHOLE_PARAM].value)) {
  107. wormhole = ! wormhole;
  108. }
  109. // CV Level buttons
  110. for (int i = 0; i < 2; i++) {
  111. if (cvLevelTriggers[i].process(params[CVLEVEL_PARAMS + i].value))
  112. cvMode ^= (0x1 << i);
  113. }
  114. }// userInputs refresh
  115. // BlackHole 0 all outputs
  116. float blackHole0 = 0.0f;
  117. float inputs0[4] = {10.0f, 10.0f, 10.0f, 10.0f};// default to generate CV when no input connected
  118. for (int i = 0; i < 4; i++)
  119. if (inputs[IN_INPUTS + i].active)
  120. inputs0[i] = inputs[IN_INPUTS + i].value;
  121. for (int i = 0; i < 4; i++) {
  122. float chanVal = calcChannel(inputs0[i], params[LEVEL_PARAMS + i], inputs[LEVELCV_INPUTS + i], isExponential[0], cvMode & 0x1);
  123. outputs[OUT_OUTPUTS + i].value = chanVal;
  124. blackHole0 += chanVal;
  125. }
  126. outputs[BLACKHOLE_OUTPUTS + 0].value = clamp(blackHole0, -10.0f, 10.0f);
  127. // BlackHole 1 all outputs
  128. float blackHole1 = 0.0f;
  129. float inputs1[4] = {10.0f, 10.0f, 10.0f, 10.0f};// default to generate CV when no input connected
  130. for (int i = 0; i < 4; i++) {
  131. if (inputs[IN_INPUTS + i + 4].active)
  132. inputs1[i] = inputs[IN_INPUTS + i + 4].value;
  133. else if (wormhole)
  134. inputs1[i] = blackHole0;
  135. }
  136. for (int i = 0; i < 4; i++) {
  137. float chanVal = calcChannel(inputs1[i], params[LEVEL_PARAMS + i + 4], inputs[LEVELCV_INPUTS + i + 4], isExponential[1], cvMode >> 1);
  138. outputs[OUT_OUTPUTS + i + 4].value = chanVal;
  139. blackHole1 += chanVal;
  140. }
  141. outputs[BLACKHOLE_OUTPUTS + 1].value = clamp(blackHole1, -10.0f, 10.0f);
  142. lightRefreshCounter++;
  143. if (lightRefreshCounter >= displayRefreshStepSkips) {
  144. lightRefreshCounter = 0;
  145. // Wormhole light
  146. lights[WORMHOLE_LIGHT].value = (wormhole ? 1.0f : 0.0f);
  147. // isExponential lights
  148. for (int i = 0; i < 2; i++)
  149. lights[EXP_LIGHTS + i].value = isExponential[i] ? 1.0f : 0.0f;
  150. // CV Level lights
  151. bool is5V = (cvMode & 0x1) == 0;
  152. lights[CVALEVEL_LIGHTS + 0].value = is5V ? 1.0f : 0.0f;
  153. lights[CVALEVEL_LIGHTS + 1].value = is5V ? 0.0f : 1.0f;
  154. is5V = (cvMode & 0x2) == 0;
  155. lights[CVBLEVEL_LIGHTS + 0].value = is5V ? 1.0f : 0.0f;
  156. lights[CVBLEVEL_LIGHTS + 1].value = is5V ? 0.0f : 1.0f;
  157. }// lightRefreshCounter
  158. }// step()
  159. inline float calcChannel(float in, Param &level, Input &levelCV, bool isExp, int cvMode) {
  160. float levCv = levelCV.active ? (levelCV.value * (cvMode != 0 ? 0.1f : 0.2f)) : 0.0f;
  161. float lev = clamp(level.value + levCv, -1.0f, 1.0f);
  162. if (isExp) {
  163. float newlev = rescale(powf(expBase, fabsf(lev)), 1.0f, expBase, 0.0f, 1.0f);
  164. if (lev < 0.0f)
  165. newlev *= -1.0f;
  166. lev = newlev;
  167. }
  168. float ret = lev * in;
  169. return ret;
  170. }
  171. };
  172. struct BlackHolesWidget : ModuleWidget {
  173. struct PanelThemeItem : MenuItem {
  174. BlackHoles *module;
  175. int theme;
  176. void onAction(EventAction &e) override {
  177. module->panelTheme = theme;
  178. }
  179. void step() override {
  180. rightText = (module->panelTheme == theme) ? "✔" : "";
  181. }
  182. };
  183. Menu *createContextMenu() override {
  184. Menu *menu = ModuleWidget::createContextMenu();
  185. MenuLabel *spacerLabel = new MenuLabel();
  186. menu->addChild(spacerLabel);
  187. BlackHoles *module = dynamic_cast<BlackHoles*>(this->module);
  188. assert(module);
  189. MenuLabel *themeLabel = new MenuLabel();
  190. themeLabel->text = "Panel Theme";
  191. menu->addChild(themeLabel);
  192. PanelThemeItem *lightItem = new PanelThemeItem();
  193. lightItem->text = lightPanelID;// Geodesics.hpp
  194. lightItem->module = module;
  195. lightItem->theme = 0;
  196. menu->addChild(lightItem);
  197. PanelThemeItem *darkItem = new PanelThemeItem();
  198. darkItem->text = darkPanelID;// Geodesics.hpp
  199. darkItem->module = module;
  200. darkItem->theme = 1;
  201. menu->addChild(darkItem);
  202. return menu;
  203. }
  204. BlackHolesWidget(BlackHoles *module) : ModuleWidget(module) {
  205. // Main panel from Inkscape
  206. DynamicSVGPanel *panel = new DynamicSVGPanel();
  207. panel->addPanel(SVG::load(assetPlugin(plugin, "res/WhiteLight/BlackHoles-WL.svg")));
  208. panel->addPanel(SVG::load(assetPlugin(plugin, "res/DarkMatter/BlackHoles-DM.svg")));
  209. box.size = panel->box.size;
  210. panel->mode = &module->panelTheme;
  211. addChild(panel);
  212. // Screws
  213. // part of svg panel, no code required
  214. float colRulerCenter = box.size.x / 2.0f;
  215. static constexpr float rowRulerBlack0 = 108.5f;
  216. static constexpr float rowRulerBlack1 = 272.5f;
  217. static constexpr float radiusIn = 30.0f;
  218. static constexpr float radiusOut = 61.0f;
  219. static constexpr float offsetL = 53.0f;
  220. static constexpr float offsetS = 30.0f;
  221. // BlackHole0 knobs
  222. addParam(createDynamicParam<GeoKnob>(Vec(colRulerCenter, rowRulerBlack0 - radiusOut), module, BlackHoles::LEVEL_PARAMS + 0, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  223. addParam(createDynamicParam<GeoKnobRight>(Vec(colRulerCenter + radiusOut, rowRulerBlack0), module, BlackHoles::LEVEL_PARAMS + 1, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  224. addParam(createDynamicParam<GeoKnobBottom>(Vec(colRulerCenter, rowRulerBlack0 + radiusOut), module, BlackHoles::LEVEL_PARAMS + 2, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  225. addParam(createDynamicParam<GeoKnobLeft>(Vec(colRulerCenter - radiusOut, rowRulerBlack0), module, BlackHoles::LEVEL_PARAMS + 3, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  226. // BlackHole0 level CV inputs
  227. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack0 - radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 0, &module->panelTheme));
  228. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + radiusIn, rowRulerBlack0), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 1, &module->panelTheme));
  229. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack0 + radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 2, &module->panelTheme));
  230. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - radiusIn, rowRulerBlack0), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 3, &module->panelTheme));
  231. // BlackHole0 inputs
  232. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack0 - offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 0, &module->panelTheme));
  233. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack0 - offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 1, &module->panelTheme));
  234. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack0 + offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 2, &module->panelTheme));
  235. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack0 + offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 3, &module->panelTheme));
  236. // BlackHole0 outputs
  237. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack0 - offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 0, &module->panelTheme));
  238. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack0 + offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 1, &module->panelTheme));
  239. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack0 + offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 2, &module->panelTheme));
  240. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack0 - offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 3, &module->panelTheme));
  241. // BlackHole0 center output
  242. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack0), Port::OUTPUT, module, BlackHoles::BLACKHOLE_OUTPUTS + 0, &module->panelTheme));
  243. // BlackHole1 knobs
  244. addParam(createDynamicParam<GeoKnob>(Vec(colRulerCenter, rowRulerBlack1 - radiusOut), module, BlackHoles::LEVEL_PARAMS + 4, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  245. addParam(createDynamicParam<GeoKnobRight>(Vec(colRulerCenter + radiusOut, rowRulerBlack1), module, BlackHoles::LEVEL_PARAMS + 5, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  246. addParam(createDynamicParam<GeoKnobBottom>(Vec(colRulerCenter, rowRulerBlack1 + radiusOut), module, BlackHoles::LEVEL_PARAMS + 6, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  247. addParam(createDynamicParam<GeoKnobLeft>(Vec(colRulerCenter - radiusOut, rowRulerBlack1), module, BlackHoles::LEVEL_PARAMS + 7, -1.0f, 1.0f, 0.0f, &module->panelTheme));
  248. // BlackHole1 level CV inputs
  249. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack1 - radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 4, &module->panelTheme));
  250. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + radiusIn, rowRulerBlack1), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 5, &module->panelTheme));
  251. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack1 + radiusIn), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 6, &module->panelTheme));
  252. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - radiusIn, rowRulerBlack1), Port::INPUT, module, BlackHoles::LEVELCV_INPUTS + 7, &module->panelTheme));
  253. // BlackHole1 inputs
  254. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack1 - offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 4, &module->panelTheme));
  255. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack1 - offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 5, &module->panelTheme));
  256. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack1 + offsetL), Port::INPUT, module, BlackHoles::IN_INPUTS + 6, &module->panelTheme));
  257. addInput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack1 + offsetS), Port::INPUT, module, BlackHoles::IN_INPUTS + 7, &module->panelTheme));
  258. // BlackHole1 outputs
  259. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetS, rowRulerBlack1 - offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 4, &module->panelTheme));
  260. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter + offsetL, rowRulerBlack1 + offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 5, &module->panelTheme));
  261. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetS, rowRulerBlack1 + offsetL), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 6, &module->panelTheme));
  262. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter - offsetL, rowRulerBlack1 - offsetS), Port::OUTPUT, module, BlackHoles::OUT_OUTPUTS + 7, &module->panelTheme));
  263. // BlackHole1 center output
  264. addOutput(createDynamicPort<GeoPort>(Vec(colRulerCenter, rowRulerBlack1), Port::OUTPUT, module, BlackHoles::BLACKHOLE_OUTPUTS + 1, &module->panelTheme));
  265. static constexpr float offsetButtonsX = 62.0f;
  266. static constexpr float offsetButtonsY = 64.0f;
  267. static constexpr float offsetLedVsBut = 9.0f;
  268. static constexpr float offsetLedVsButS = 5.0f;// small
  269. static constexpr float offsetLedVsButL = 12.0f;// large
  270. // BlackHole0 Exp button and light
  271. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack0 + offsetButtonsY), module, BlackHoles::EXP_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  272. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack0 + offsetButtonsY - offsetLedVsBut - 1.0f), module, BlackHoles::EXP_LIGHTS + 0));
  273. // BlackHole1 Exp button and light
  274. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack1 + offsetButtonsY), module, BlackHoles::EXP_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  275. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack1 + offsetButtonsY - offsetLedVsBut -1.0f), module, BlackHoles::EXP_LIGHTS + 1));
  276. // Wormhole button and light
  277. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter - offsetButtonsX, rowRulerBlack1 - offsetButtonsY), module, BlackHoles::WORMHOLE_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  278. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter - offsetButtonsX + offsetLedVsBut, rowRulerBlack1 - offsetButtonsY + offsetLedVsBut), module, BlackHoles::WORMHOLE_LIGHT));
  279. // CV Level A button and light
  280. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter + offsetButtonsX, rowRulerBlack0 + offsetButtonsY), module, BlackHoles::CVLEVEL_PARAMS + 0, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  281. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButL, rowRulerBlack0 + offsetButtonsY + offsetLedVsButS), module, BlackHoles::CVALEVEL_LIGHTS + 0));
  282. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButS, rowRulerBlack0 + offsetButtonsY + offsetLedVsButL), module, BlackHoles::CVALEVEL_LIGHTS + 1));
  283. // CV Level B button and light
  284. addParam(createDynamicParam<GeoPushButton>(Vec(colRulerCenter + offsetButtonsX, rowRulerBlack1 + offsetButtonsY), module, BlackHoles::CVLEVEL_PARAMS + 1, 0.0f, 1.0f, 0.0f, &module->panelTheme));
  285. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButL, rowRulerBlack1 + offsetButtonsY + offsetLedVsButS), module, BlackHoles::CVBLEVEL_LIGHTS + 0));
  286. addChild(createLightCentered<SmallLight<GeoWhiteLight>>(Vec(colRulerCenter + offsetButtonsX + offsetLedVsButS, rowRulerBlack1 + offsetButtonsY + offsetLedVsButL), module, BlackHoles::CVBLEVEL_LIGHTS + 1));
  287. }
  288. };
  289. } // namespace rack_plugin_Geodesics
  290. using namespace rack_plugin_Geodesics;
  291. RACK_PLUGIN_MODEL_INIT(Geodesics, BlackHoles) {
  292. Model *modelBlackHoles = Model::create<BlackHoles, BlackHolesWidget>("Geodesics", "BlackHoles", "BlackHoles", AMPLIFIER_TAG);
  293. return modelBlackHoles;
  294. }
  295. /*CHANGE LOG
  296. 0.6.5:
  297. input refresh optimization
  298. step optimization of lights refresh
  299. 0.6.3:
  300. change wormhole behvior, simplified (no all unconnected criteria)
  301. 0.6.1:
  302. add CV level modes buttons and lights
  303. change CV level behavior
  304. 0.6.0:
  305. created
  306. */