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.

1163 lines
41KB

  1. ////////////////////////////////////////////////////////////////////////
  2. //// Metriks is a 8 HP measuring/visual module: /////
  3. //// - voltmeter (0 to 3 decimals). /////
  4. //// - frequency counter / note tuner / BPM meter. /////
  5. //// - pulses (ticks) counter (with adjustable voltage threshold). /////
  6. ////////////////////////////////////////////////////////////////////////
  7. #include "Ohmer.hpp"
  8. #include <dsp/digital.hpp>
  9. #include <string>
  10. namespace rack_plugin_Ohmer {
  11. // Module structure.
  12. struct Metriks : Module {
  13. enum ParamIds {
  14. PARAM_ENCODER,
  15. BUTTON_MODE,
  16. BUTTON_PLAYPAUSE,
  17. BUTTON_RESET,
  18. NUM_PARAMS
  19. };
  20. enum InputIds {
  21. INPUT_SOURCE,
  22. INPUT_PLAYPAUSE,
  23. INPUT_RESET,
  24. NUM_INPUTS
  25. };
  26. enum OutputIds {
  27. OUTPUT_THRU,
  28. NUM_OUTPUTS
  29. };
  30. enum LightIds {
  31. LED_PLAY_GREEN,
  32. LED_PLAY_RED,
  33. NUM_LIGHTS
  34. };
  35. // Pointer to encoder (handled as knob).
  36. Knob *mtksEncoder;
  37. // Module interface definitions, such parameters (encoder, button), input ports, output ports and LEDs.
  38. Metriks() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
  39. onReset();
  40. onRandomize();
  41. }
  42. void onReset() override {
  43. if (!avoidOnResetReentry) {
  44. encoderPrevious = 0;
  45. onFirstInitCounter = (int)(engineGetSampleRate() / 4.0f);
  46. currentStep = 0;
  47. switchedModeCounter = 0;
  48. encoderDisplayCounter = 0;
  49. currentVoltage = 0.0f;
  50. currentFreq = -1.0f; // -1.0f means unknown frequency.
  51. currentPeakCount = 0;
  52. previousStep = 0;
  53. expectedStep = 0;
  54. bIsPlaying = false;
  55. bBlinkErr = false;
  56. bBlinkErrAlt = false;
  57. blinkSyncAnim = 0;
  58. bBlinkErrTimer = 0;
  59. avoidOnResetReentry = true;
  60. }
  61. }
  62. // Inhibit "Randomize" from context-menu / Ctrl+R / Command+R keyboard shortcut over module.
  63. void onRandomize() override {
  64. }
  65. //// GENERAL PURPOSE VARIABLES/FLAGS/TABLES.
  66. int onFirstInitCounter = 0;
  67. bool avoidOnResetReentry = false; // When true, this avoid OnReset() reentry.
  68. // Current selected Metriks model (GUI theme).
  69. int Theme = 0;
  70. // DMD-font color (default is black LCD), related to selected model.
  71. NVGcolor DMDtextColor = nvgRGB(0x08, 0x08, 0x08);
  72. // Measuring mode (0: voltmeter, 1: frequency counter/note tuner/BPM meter, 2: peak counter).
  73. int Mode = 0;
  74. int fmSubMode = 0; // Submode for frequency counter measuring (Mode == 1): 0 = frequency counter, 1 = note tuner, 2 = BPM meter.
  75. // Note to frequency equivalent table (octaves 0 to 9).
  76. float noteToFreq[120] = {16.351f, 17.324f, 18.354f, 19.445f, 20.601f, 21.827f, 23.124f, 24.499f, 25.956f, 27.500f, 29.135f, 30.868f,
  77. 32.703f, 34.648f, 36.708f, 38.891f, 41.203f, 43.654f, 46.249f, 48.999f, 51.913f, 55.000f, 58.270f, 61.735f,
  78. 65.406f, 69.296f, 73.416f, 77.782f, 82.407f, 87.307f, 92.499f, 97.999f, 103.826f, 110.000f, 116.541f, 123.471f,
  79. 130.813f, 138.591f, 146.832f, 155.563f, 164.814f, 174.614f, 184.997f, 195.998f, 207.652f, 220.000f, 233.082f, 246.942f,
  80. 261.626f, 277.183f, 293.665f, 311.127f, 329.628f, 349.228f, 369.994f, 391.995f, 415.305f, 440.000f, 466.164f, 493.883f,
  81. 523.251f, 554.365f, 587.330f, 622.254f, 659.255f, 698.456f, 739.989f, 783.991f, 830.609f, 880.000f, 932.328f, 987.767f,
  82. 1046.502f, 1108.731f, 1174.659f, 1244.508f, 1318.510f, 1396.913f, 1479.978f, 1567.982f, 1661.219f, 1760.000f, 1864.655f, 1975.533f,
  83. 2093.005f, 2217.461f, 2349.318f, 2489.016f, 2637.021f, 2793.826f, 2959.955f, 3135.964f, 3322.438f, 3520.000f, 3729.310f, 3951.066f,
  84. 4186.009f, 4434.922f, 4698.636f, 4978.032f, 5274.042f, 5587.652f, 5919.910f, 6271.928f, 6644.876f, 7040.000f, 7458.620f, 7902.132f,
  85. 8372.018f, 8869.844f, 9397.272f, 9956.064f, 10548.084f, 11175.304f, 11839.820f, 12543.856f, 13289.752f, 14080.000f, 14917.240f, 15804.264f};
  86. // Note mames, including octave.
  87. std::string noteName[120] = {"C 0", "C#/Db 0", "D 0", "D#/Eb 0", "E 0", "F 0", "F#/Gb 0", "G 0", "G#/Ab 0", "A 0", "A#/Bb 0", "B 0",
  88. "C 1", "C#/Db 1", "D 1", "D#/Eb 1", "E 1", "F 1", "F#/Gb 1", "G 1", "G#/Ab 1", "A 1", "A#/Bb 1", "B 1",
  89. "C 2", "C#/Db 2", "D 2", "D#/Eb 2", "E 2", "F 2", "F#/Gb 2", "G 2", "G#/Ab 2", "A 2", "A#/Bb 2", "B 2",
  90. "C 3", "C#/Db 3", "D 3", "D#/Eb 3", "E 3", "F 3", "F#/Gb 3", "G 3", "G#/Ab 3", "A 3", "A#/Bb 3", "B 3",
  91. "C 4", "C#/Db 4", "D 4", "D#/Eb 4", "E 4", "F 4", "F#/Gb 4", "G 4", "G#/Ab 4", "A 4", "A#/Bb 4", "B 4",
  92. "C 5", "C#/Db 5", "D 5", "D#/Eb 5", "E 5", "F 5", "F#/Gb 5", "G 5", "G#/Ab 5", "A 5", "A#/Bb 5", "B 5",
  93. "C 6", "C#/Db 6", "D 6", "D#/Eb 6", "E 6", "F 6", "F#/Gb 6", "G 6", "G#/Ab 6", "A 6", "A#/Bb 6", "B 6",
  94. "C 7", "C#/Db 7", "D 7", "D#/Eb 7", "E 7", "F 7", "F#/Gb 7", "G 7", "G#/Ab 7", "A 7", "A#/Bb 7", "B 7",
  95. "C 8", "C#/Db 8", "D 8", "D#/Eb 8", "E 8", "F 8", "F#/Gb 8", "G 8", "G#/Ab 8", "A 8", "A#/Bb 8", "B 8",
  96. "C 9", "C#/Db 9", "D 9", "D#/Eb 9", "E 9", "F 9", "F#/Gb 9", "G 9", "G#/Ab 9", "A 9", "A#/Bb 9", "B 9"};
  97. // Messages displayed on DMD (dot-matrix display), using two lines..
  98. char dmdMessage1[20] = "";
  99. char dmdMessage2[20] = "";
  100. int xOffsetValue = 0; // Horizontal offset on DMD to display for second line.
  101. bool bBlinkErr = false; // When set, do blink the second line (missing input/over/unknown freq/over.
  102. bool bBlinkErrAlt = false; // Alternate true/false to blink second line.
  103. int blinkSyncAnim = 0; // Used to select animated string when determining frequency.
  104. int bBlinkErrTimer = 0; // Related timer
  105. // Encoder (registered position to be used on next step for relative move).
  106. int encoderCurrent = 0;
  107. int encoderPrevious = 0; // Encoder "absolute" (saved to jSon)...
  108. int encoderDelta = 0; // 0 if not moved, -1 if counter-clockwise (decrement), 1 if clockwise (increment).
  109. // Timers, for temporary displays.
  110. int switchedModeCounter = 0; // Used when switching to next mode (by MODE button).
  111. int encoderDisplayCounter = 0; // Used when changing a value via encoder.
  112. // MODE button.
  113. SchmittTrigger modeButton;
  114. // PLAY/STOP button and related (trigger) port (PLAY/STOP is used for "Pulse Counter" mode only).
  115. SchmittTrigger playButton;
  116. SchmittTrigger playPort;
  117. bool bIsPlaying = false;
  118. // RESET (RST) button and related (trigger) port.
  119. SchmittTrigger resetButton;
  120. SchmittTrigger resetPort;
  121. // Values can be displayed.
  122. int vmDecimals = 2;
  123. bool bSourceActive = false; // Source (input) is connected (active) or not.
  124. float currentVoltage = 0.0f; // Used by voltmeter.
  125. float currentFreq = -1.0f; // -1.0f means unknown frequency.
  126. unsigned long int currentPeakCount = 0;
  127. // Schmitt trigger used to determine frequency. Also used for peak counter mode.
  128. SchmittTrigger inputPort;
  129. float triggerVoltage = 1.7f;
  130. // Step-based counters (mainly used for frequency counter submodes).
  131. long long int currentStep = 0;
  132. long long int previousStep = 0;
  133. long long int expectedStep = 0;
  134. // Functions & methods (voids).
  135. void step() override;
  136. // Custom function to round a float at given decimal(s).
  137. float roundp(float f, int prec) {
  138. return (float)(round(f * (float)pow(10, prec)) / (float)pow(10, prec));
  139. }
  140. // Method to display moving "*" while source frequency isn't defined/stable.
  141. void playFreqFindAnimation() {
  142. char text[20] = " ";
  143. if (blinkSyncAnim < 8)
  144. text[blinkSyncAnim] = '*';
  145. else text[14 - blinkSyncAnim] = '*';
  146. strcpy(dmdMessage2, text);
  147. }
  148. int getNotebyFreq(float freq) {
  149. int aPosition = -1;
  150. for (int i = 0; i < 120; i++) {
  151. if ((freq >= (noteToFreq[i] - (noteToFreq[i] * 0.08f))) && (freq <= (noteToFreq[i] + (noteToFreq[i] * 0.08f)))) {
  152. aPosition = i;
  153. i = 120;
  154. }
  155. }
  156. return aPosition;
  157. }
  158. // Persistence for extra datas, via json functions.
  159. // These extra datas are saved to .vcv file (including "autosave.vcv") also are "transfered" when you duplicate the module.
  160. json_t *toJson() override {
  161. json_t *rootJ = json_object();
  162. json_object_set_new(rootJ, "Theme", json_integer(Theme)); // Save selected module theme (GUI).
  163. json_object_set_new(rootJ, "Mode", json_integer(Mode)); // Save current mode.
  164. json_object_set_new(rootJ, "fmSubMode", json_integer(fmSubMode)); // Save current submode for frequency counter.
  165. json_object_set_new(rootJ, "vmDecimals", json_integer(vmDecimals)); // Save number of decimals for voltmeter.
  166. json_object_set_new(rootJ, "triggerVoltage", json_real(triggerVoltage)); // Save trigger voltage for input port (default is +1.7V).
  167. return rootJ;
  168. }
  169. void fromJson(json_t *rootJ) override {
  170. // Retrieving module theme/variation (when loading .vcv and cloning module).
  171. json_t *ThemeJ = json_object_get(rootJ, "Theme");
  172. if (ThemeJ)
  173. Theme = json_integer_value(ThemeJ);
  174. // Retrieving measuring mode.
  175. json_t *ModeJ = json_object_get(rootJ, "Mode");
  176. if (ModeJ)
  177. Mode = json_integer_value(ModeJ);
  178. // Retrieving frequency counter sub mode.
  179. json_t *fmSubModeJ = json_object_get(rootJ, "fmSubMode");
  180. if (fmSubModeJ)
  181. fmSubMode = json_integer_value(fmSubModeJ);
  182. // Retrieving number of decimal set for voltmeter.
  183. json_t *vmDecimalsJ = json_object_get(rootJ, "vmDecimals");
  184. if (vmDecimalsJ)
  185. vmDecimals = json_integer_value(vmDecimalsJ);
  186. // Retrieving trigger voltage for peak counter mode (real/float value).
  187. json_t *triggerVoltageJ = json_object_get(rootJ, "triggerVoltage");
  188. if (triggerVoltageJ)
  189. triggerVoltage = json_real_value(triggerVoltageJ);
  190. }
  191. };
  192. void Metriks::step() {
  193. // step() function is the right place for DSP processing!
  194. // Depending current Metriks model (theme), set the relevant DMD-text color.
  195. DMDtextColor = tblDMDtextColor[Theme];
  196. // Bypass early steps, due to encoder/knob behaviors on init (from 0.0f... to json saved value!).
  197. if (onFirstInitCounter > 0) {
  198. currentStep++;
  199. if ((currentStep % 8000) > 4000)
  200. strcpy(dmdMessage1, "Calibrating...");
  201. else strcpy(dmdMessage1, "");
  202. xOffsetValue = 2;
  203. strcpy(dmdMessage2, "");
  204. encoderCurrent = (int)roundf(10.0f * params[PARAM_ENCODER].value);
  205. if (encoderPrevious != encoderCurrent) {
  206. onFirstInitCounter = onFirstInitCounter + (int)(engineGetSampleRate() / 512.0f);
  207. encoderPrevious = encoderCurrent;
  208. }
  209. else onFirstInitCounter--;
  210. if (onFirstInitCounter == 1) {
  211. // This is the lastest step of initialization.
  212. // Reinit encoder reading.
  213. encoderCurrent = (int)roundf(10.0f * params[PARAM_ENCODER].value);
  214. encoderPrevious = encoderCurrent;
  215. encoderDelta = 0; // Default assuming encoder isn't moved.
  216. //
  217. currentStep = 0;
  218. onFirstInitCounter = 0;
  219. }
  220. return;
  221. }
  222. // Read if the encoder have moved or not.
  223. encoderCurrent = (int)roundf(10.0f * params[PARAM_ENCODER].value);
  224. if (encoderCurrent != encoderPrevious) {
  225. if (abs(encoderCurrent - encoderPrevious) <= 2) {
  226. // Depending current mode, this will change setting, like:
  227. // - Number of displayed decimals for voltmeter.
  228. // - Threshold voltage for peak counter.
  229. switch (Mode) {
  230. case 0:
  231. // Voltmeter: changing decimals.
  232. if (encoderCurrent < encoderPrevious) {
  233. // Decrementing decimals...
  234. vmDecimals--;
  235. if (vmDecimals < 0)
  236. vmDecimals = 0; // Min: 0.
  237. }
  238. else {
  239. // Incrementing decimals...
  240. vmDecimals++;
  241. if (vmDecimals > 3)
  242. vmDecimals = 3; // Min: 0.
  243. }
  244. // Prepare display and start timer (only if not switching mode).
  245. if (switchedModeCounter == 0) {
  246. strcpy(dmdMessage1, "Decimals");
  247. xOffsetValue = 42;
  248. snprintf(dmdMessage2, sizeof(dmdMessage2), "%i", vmDecimals);
  249. // Start display timer...
  250. encoderDisplayCounter = (int)(2 * engineGetSampleRate());
  251. }
  252. break;
  253. case 1:
  254. // Frequency counter: changing submode.
  255. if (encoderCurrent < encoderPrevious) {
  256. // Decrementing: select previous submode...
  257. fmSubMode--;
  258. if (fmSubMode < 0)
  259. fmSubMode = 2; // Return to BPM meter...
  260. }
  261. else {
  262. // Decrementing: select next submode...
  263. fmSubMode++;
  264. if (fmSubMode > 2)
  265. fmSubMode = 0; // Return to frequency counter...
  266. }
  267. // Prepare display and start timer (only if not switching mode).
  268. if (switchedModeCounter == 0) {
  269. switch (fmSubMode) {
  270. case 0:
  271. strcpy(dmdMessage1, "Freq. Counter");
  272. break;
  273. case 1:
  274. strcpy(dmdMessage1, "Note Tuner");
  275. break;
  276. case 2:
  277. strcpy(dmdMessage1, "BPM Meter");
  278. }
  279. }
  280. break;
  281. case 2:
  282. // Peak counter: changing threshold (trigger) voltage.
  283. if (encoderCurrent < encoderPrevious) {
  284. // Decrementing thresold voltage by 0.1... minimum is 0.2V.
  285. triggerVoltage = triggerVoltage - 0.1f;
  286. if (triggerVoltage < 0.2f)
  287. triggerVoltage = 0.2f; // Mininum voltage: 0.2V.
  288. }
  289. else {
  290. // Incrementing thresold voltage by 0.1...
  291. triggerVoltage = triggerVoltage + 0.1f;
  292. if (triggerVoltage > 12.0f)
  293. triggerVoltage = 12.0f; // Maximum voltage: 12V.
  294. }
  295. // Prepare display and start timer (only if not switching mode).
  296. if (switchedModeCounter == 0) {
  297. strcpy(dmdMessage1, "Threshold");
  298. triggerVoltage = round(triggerVoltage * 10.0f) / 10.0f;
  299. if (triggerVoltage < 10.0f)
  300. xOffsetValue = 36;
  301. else xOffsetValue = 24;
  302. snprintf(dmdMessage2, sizeof(dmdMessage2), "%2.1fV", triggerVoltage);
  303. // Start display timer...
  304. encoderDisplayCounter = (int)(2 * engineGetSampleRate());
  305. }
  306. }
  307. }
  308. // Save current encoder position to become previous (for next check).
  309. encoderPrevious = encoderCurrent;
  310. }
  311. // MODE button trigger: change mode when pressed.
  312. if (modeButton.process(params[BUTTON_MODE].value)) {
  313. modeButton.reset();
  314. switchedModeCounter = (int)(2 * engineGetSampleRate());
  315. Mode++;
  316. if (Mode > 2)
  317. Mode = 0;
  318. }
  319. bSourceActive = inputs[INPUT_SOURCE].active;
  320. currentVoltage = bSourceActive ? inputs[INPUT_SOURCE].value : 0.0f;
  321. if (bSourceActive) {
  322. // Transmit (as passthru) source voltage directly to output port. This may useful to insert the Metriks instrument module in chain.
  323. outputs[OUTPUT_THRU].value = currentVoltage;
  324. if (Mode == 0) {
  325. // Voltmeter: just unlit PLAY/PAUSE LED, that's all.
  326. lights[LED_PLAY_GREEN].value = 0.0;
  327. lights[LED_PLAY_RED].value = 0.0;
  328. }
  329. else if (Mode == 1) {
  330. // Frequency counter / note tuner / BPM meter.
  331. // Increment step number.
  332. currentStep++;
  333. // Using Schmitt trigger (SchmittTrigger is provided by dsp/digital.hpp) to detect triggers on CLK input jack.
  334. // It uses "triggerVoltage" value for trigger: default +1.7V, or redefined from Peak Counter mode (min +0.2V)!
  335. if (inputPort.process(rescale(inputs[INPUT_SOURCE].value, 0.1f, triggerVoltage, 0.0f, 1.0f))) {
  336. // It's a rising edge.
  337. if (previousStep == 0) {
  338. // Source CLK frequency is unknown.
  339. currentFreq = -1.0f; // Set to -1.0f by this way it will displayed as "Unknown" frequency.
  340. expectedStep = 0;
  341. }
  342. else {
  343. // But perhaps this rising edge comes too early (source frequency is increased).
  344. if (currentFreq != -1.0f) {
  345. if ((currentStep != expectedStep) && (currentStep != (expectedStep + 1)) && (currentStep != (expectedStep - 1))) {
  346. currentFreq = -1.0f; // Forced to -1.0f by this way it will displayed as "Unknown" frequency.
  347. expectedStep = 0;
  348. currentStep = 1;
  349. }
  350. else {
  351. // Source CLK frequency is stable.
  352. currentFreq = engineGetSampleRate() / (float)(currentStep - previousStep);
  353. expectedStep = currentStep - previousStep + currentStep;
  354. }
  355. }
  356. else {
  357. // Source CLK frequency was unknow at previous rising edge, but for now it can be established on this (next) rising edge.
  358. currentFreq = engineGetSampleRate() / (float)(currentStep - previousStep);
  359. expectedStep = currentStep - previousStep + currentStep;
  360. }
  361. }
  362. previousStep = currentStep;
  363. }
  364. }
  365. else if (Mode == 2) {
  366. // Peak counter mode.
  367. if (bIsPlaying) {
  368. if (inputPort.process(rescale(inputs[INPUT_SOURCE].value, 0.1f, triggerVoltage, 0.0f, 1.0f))) {
  369. currentPeakCount++;
  370. if (currentPeakCount > 99999999)
  371. currentPeakCount = 100000000;
  372. }
  373. }
  374. // Play/Pause via button or via trigger signal on related port.
  375. if (playButton.process(params[BUTTON_PLAYPAUSE].value) || playPort.process(rescale(inputs[INPUT_PLAYPAUSE].value, 0.2f, 1.7f, 0.0f, 1.0f))) {
  376. playButton.reset();
  377. bIsPlaying = !bIsPlaying;
  378. }
  379. lights[LED_PLAY_GREEN].value = bIsPlaying ? 1.0 : 0.0;
  380. lights[LED_PLAY_RED].value = bIsPlaying ? 0.0 : 1.0;
  381. // RST via button or via trigger signal on related port.
  382. if (resetButton.process(params[BUTTON_RESET].value) || resetPort.process(rescale(inputs[INPUT_RESET].value, 0.2f, 1.7f, 0.0f, 1.0f))) {
  383. resetButton.reset();
  384. // Reset Peak counter.
  385. currentPeakCount = 0;
  386. }
  387. }
  388. }
  389. else {
  390. // Input port isn't connected.
  391. // Sending 0V to output port.
  392. outputs[OUTPUT_THRU].value = 0;
  393. // Turn off PLAY/PAUSE LED.
  394. lights[LED_PLAY_GREEN].value = 0.0;
  395. lights[LED_PLAY_RED].value = 0.0;
  396. // Reset some useless counters/flags with input port isn't connected.
  397. currentFreq = -1.0f; // -1.0f means unknown frequency.
  398. currentPeakCount = 0;
  399. bIsPlaying = false;
  400. currentStep = 0;
  401. previousStep = 0;
  402. expectedStep = 0;
  403. }
  404. if (switchedModeCounter != 0) {
  405. // Currently switching mode: displaying new mode selected for a certain delay.
  406. // Cancelling other temporary displays (mode switching have higher priority).
  407. encoderDisplayCounter = 0;
  408. bBlinkErr = false;
  409. // Decrementing timer counter...
  410. switchedModeCounter--;
  411. // Display new mode for a given delay.
  412. strcpy(dmdMessage1, "Switch To >");
  413. switch (Mode) {
  414. case 0:
  415. // Voltmeter.
  416. xOffsetValue = 3;
  417. strcpy(dmdMessage2, "Voltmeter");
  418. break;
  419. case 1:
  420. // Frequency counter.
  421. xOffsetValue = 0;
  422. // Frequency counter / Note tuner / BPM meter.
  423. switch (fmSubMode) {
  424. case 0:
  425. strcpy(dmdMessage2, "Freq. Cnt.");
  426. break;
  427. case 1:
  428. strcpy(dmdMessage2, "Note Tun.");
  429. break;
  430. case 2:
  431. strcpy(dmdMessage2, "BPM Meter");
  432. }
  433. break;
  434. case 2:
  435. // Peak counter. //DONE
  436. xOffsetValue = 1;
  437. strcpy(dmdMessage2, "Peak Cnt.");
  438. }
  439. }
  440. else if (encoderDisplayCounter != 0) {
  441. // Parameter changed via encoder: holding display the parameter during short time.
  442. bBlinkErr = false;
  443. encoderDisplayCounter--;
  444. }
  445. else {
  446. // Normal display mode (depending current mode and its parameter).
  447. switch (Mode) {
  448. case 0:
  449. // Voltmeter.
  450. strcpy(dmdMessage1, "Voltmeter");
  451. if (bSourceActive) {
  452. // Doing a voltage range limitation, regardling actual number of decimals we're using.
  453. // Doing via "switch" will reduce CPU loads (instead of math formula).
  454. float vFloor;
  455. float vCeiling;
  456. switch (vmDecimals) {
  457. case 0:
  458. currentVoltage = clamp(currentVoltage, -9999.9f, 99999.4f);
  459. vFloor = -9999.0f;
  460. vCeiling = 99999.3f; // Positive, we can display one more digit!
  461. break;
  462. case 1:
  463. currentVoltage = clamp(currentVoltage, -999.99f, 999.94f);
  464. vFloor = -999.90f;
  465. vCeiling = 999.93f;
  466. break;
  467. case 2:
  468. currentVoltage = clamp(currentVoltage, -999.999f, 999.994f);
  469. vFloor = -999.990f;
  470. vCeiling = 999.993f;
  471. break;
  472. case 3:
  473. currentVoltage = clamp(currentVoltage, -99.9999f, 99.9994f);
  474. vFloor = -99.9990f;
  475. vCeiling = 99.9993f;
  476. break;
  477. }
  478. if ((currentVoltage < vFloor) || (currentVoltage > vCeiling)) {
  479. // Voltage is out of range (overflow).
  480. xOffsetValue = 3;
  481. if (!bBlinkErr) {
  482. bBlinkErr = true;
  483. bBlinkErrAlt = true;
  484. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  485. }
  486. else {
  487. if (bBlinkErrTimer > 0) {
  488. bBlinkErrTimer--; // Delaying...
  489. }
  490. else {
  491. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  492. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  493. }
  494. if (bBlinkErrAlt)
  495. strcpy(dmdMessage2, "!Overflow!");
  496. else strcpy(dmdMessage2, "");
  497. }
  498. }
  499. else {
  500. // Second stage of voltage processing (voltage is in range).
  501. // Doing a decimal precision rounding.
  502. currentVoltage = roundp(currentVoltage, vmDecimals);
  503. switch (vmDecimals) {
  504. case 0:
  505. // Avoiding "-0"...
  506. if (int(currentVoltage) == 0)
  507. currentVoltage = 0.0f;
  508. xOffsetValue = 14;
  509. snprintf(dmdMessage2, sizeof(dmdMessage2), "%5.0fV", currentVoltage);
  510. break;
  511. case 1:
  512. // Avoiding "-0.0"...
  513. if (int(currentVoltage * 10) == 0)
  514. currentVoltage = 0.0f;
  515. //
  516. xOffsetValue = 20;
  517. if (currentVoltage <= -100.0f)
  518. xOffsetValue = 8;
  519. snprintf(dmdMessage2, sizeof(dmdMessage2), "%5.1fV", currentVoltage);
  520. break;
  521. case 2:
  522. // Avoiding "-0.00"...
  523. if (int(currentVoltage * 100) == 0)
  524. currentVoltage = 0.0f;
  525. //
  526. xOffsetValue = 38;
  527. if (currentVoltage <= -100.0f)
  528. xOffsetValue = 2;
  529. else if (currentVoltage <= -10.0f)
  530. xOffsetValue = 14;
  531. else if (currentVoltage < 0.00f)
  532. xOffsetValue = 26;
  533. else if (currentVoltage >= 100.0f)
  534. xOffsetValue = 14;
  535. else if (currentVoltage >= 10.0f)
  536. xOffsetValue = 26;
  537. snprintf(dmdMessage2, sizeof(dmdMessage2), "%4.2fV", currentVoltage);
  538. break;
  539. case 3:
  540. // Avoiding "-0.000"...
  541. if (int(currentVoltage * 1000) == 0)
  542. currentVoltage = 0.0f;
  543. //
  544. xOffsetValue = 26;
  545. if (currentVoltage <= -10.0f)
  546. xOffsetValue = 2;
  547. else if (currentVoltage < 0.0f)
  548. xOffsetValue = 14;
  549. else if (currentVoltage >= 10.0f)
  550. xOffsetValue = 14;
  551. snprintf(dmdMessage2, sizeof(dmdMessage2), "%3.3fV", currentVoltage);
  552. }
  553. }
  554. }
  555. else {
  556. // Input port isn't connected (not active).
  557. xOffsetValue = -1;
  558. if (!bBlinkErr) {
  559. bBlinkErr = true;
  560. bBlinkErrAlt = true;
  561. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  562. }
  563. else {
  564. if (bBlinkErrTimer > 0) {
  565. bBlinkErrTimer--; // Delaying...
  566. }
  567. else {
  568. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  569. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  570. }
  571. if (bBlinkErrAlt)
  572. strcpy(dmdMessage2, "? Input ?");
  573. else strcpy(dmdMessage2, "");
  574. }
  575. }
  576. break;
  577. case 1:
  578. // Frequency counter / Note tuner / BPM meter.
  579. switch (fmSubMode) {
  580. case 0:
  581. strcpy(dmdMessage1, "Freq. Counter");
  582. break;
  583. case 1:
  584. strcpy(dmdMessage1, "Note Tuner");
  585. break;
  586. case 2:
  587. strcpy(dmdMessage1, "BPM Meter");
  588. }
  589. if (bSourceActive) {
  590. switch (fmSubMode) {
  591. case 0: // Frequency counter.
  592. if (currentFreq < 0.0f) {
  593. xOffsetValue = 1;
  594. if (!bBlinkErr) {
  595. bBlinkErr = true;
  596. blinkSyncAnim = 0;
  597. bBlinkErrTimer = round(engineGetSampleRate() / 32);
  598. }
  599. else {
  600. if (bBlinkErrTimer > 0) {
  601. bBlinkErrTimer--; // Delaying...
  602. }
  603. else {
  604. blinkSyncAnim++;
  605. if (blinkSyncAnim > 13)
  606. blinkSyncAnim = 0;
  607. bBlinkErrTimer = round(engineGetSampleRate() / 32);
  608. }
  609. playFreqFindAnimation();
  610. }
  611. }
  612. else if (currentFreq > 99999.8f) {
  613. xOffsetValue = 3;
  614. if (!bBlinkErr) {
  615. bBlinkErr = true;
  616. bBlinkErrAlt = true;
  617. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  618. }
  619. else {
  620. if (bBlinkErrTimer > 0) {
  621. bBlinkErrTimer--; // Delaying...
  622. }
  623. else {
  624. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  625. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  626. }
  627. if (bBlinkErrAlt)
  628. strcpy(dmdMessage2, "!Overflow!");
  629. else strcpy(dmdMessage2, "");
  630. }
  631. }
  632. else if (currentFreq >= 1000.0f) {
  633. xOffsetValue = 13;
  634. snprintf(dmdMessage2, sizeof(dmdMessage2), "%5.0fHz", currentFreq);
  635. }
  636. else {
  637. xOffsetValue = 19;
  638. snprintf(dmdMessage2, sizeof(dmdMessage2), "%5.1fHz", currentFreq);
  639. }
  640. break;
  641. case 1:
  642. // Note Tuner.
  643. if ((currentFreq > 15.534f) && (currentFreq < 16594.4f)) {
  644. int iNote = 0;
  645. iNote = getNotebyFreq(currentFreq);
  646. if (iNote == -1)
  647. iNote = getNotebyFreq(currentFreq + (currentFreq * 0.12f));
  648. if (iNote == -1)
  649. iNote = getNotebyFreq(currentFreq - (currentFreq * 0.12f));
  650. if (iNote > -1) {
  651. std::string str = noteName[iNote];
  652. char *cstr = new char[str.length() + 1];
  653. strcpy(cstr, str.c_str());
  654. if (str.length() == 3)
  655. xOffsetValue = 31;
  656. else xOffsetValue = 8;
  657. strcpy(dmdMessage2, cstr);
  658. delete [] cstr;
  659. }
  660. else {
  661. xOffsetValue = 32;
  662. strcpy(dmdMessage2, "???");
  663. }
  664. }
  665. else {
  666. // Unknown frequency...
  667. xOffsetValue = 32;
  668. strcpy(dmdMessage2, "");
  669. }
  670. break;
  671. case 2:
  672. // Submode BPM Meter...
  673. if (currentFreq < 0.0f) {
  674. xOffsetValue = 1;
  675. if (!bBlinkErr) {
  676. bBlinkErr = true;
  677. blinkSyncAnim = 0;
  678. bBlinkErrTimer = round(engineGetSampleRate() / 32);
  679. }
  680. else {
  681. if (bBlinkErrTimer > 0) {
  682. bBlinkErrTimer--; // Delaying...
  683. }
  684. else {
  685. blinkSyncAnim++;
  686. if (blinkSyncAnim > 13)
  687. blinkSyncAnim = 0;
  688. bBlinkErrTimer = round(engineGetSampleRate() / 32);
  689. }
  690. playFreqFindAnimation();
  691. }
  692. }
  693. else if (currentFreq > 99999.8f) {
  694. xOffsetValue = 3;
  695. if (!bBlinkErr) {
  696. bBlinkErr = true;
  697. bBlinkErrAlt = true;
  698. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  699. }
  700. else {
  701. if (bBlinkErrTimer > 0) {
  702. bBlinkErrTimer--; // Delaying...
  703. }
  704. else {
  705. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  706. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  707. }
  708. if (bBlinkErrAlt)
  709. strcpy(dmdMessage2, "!Overflow!");
  710. else strcpy(dmdMessage2, "");
  711. }
  712. }
  713. else {
  714. float fBPM = round(currentFreq * 600.0f) / 10.0f;
  715. if (fBPM < 999999.5f) {
  716. if (fBPM >= 100000.0f)
  717. xOffsetValue = 5;
  718. else if (fBPM >= 10000.0f)
  719. xOffsetValue = 17;
  720. else xOffsetValue = 29;
  721. snprintf(dmdMessage2, sizeof(dmdMessage2), "%6.1f", fBPM);
  722. }
  723. else {
  724. xOffsetValue = 3;
  725. if (!bBlinkErr) {
  726. bBlinkErr = true;
  727. bBlinkErrAlt = true;
  728. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  729. }
  730. else {
  731. if (bBlinkErrTimer > 0) {
  732. bBlinkErrTimer--; // Delaying...
  733. }
  734. else {
  735. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  736. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  737. }
  738. if (bBlinkErrAlt)
  739. strcpy(dmdMessage2, "!Overflow!");
  740. else strcpy(dmdMessage2, "");
  741. }
  742. }
  743. }
  744. }
  745. }
  746. else {
  747. xOffsetValue = -1;
  748. if (!bBlinkErr) {
  749. bBlinkErr = true;
  750. bBlinkErrAlt = true;
  751. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  752. }
  753. else {
  754. if (bBlinkErrTimer > 0) {
  755. bBlinkErrTimer--; // Delaying...
  756. }
  757. else {
  758. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  759. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  760. }
  761. if (bBlinkErrAlt)
  762. strcpy(dmdMessage2, "? Input ?");
  763. else strcpy(dmdMessage2, "");
  764. }
  765. }
  766. break;
  767. case 2:
  768. // Pulse counter.
  769. strcpy(dmdMessage1, "Peak Counter");
  770. if (bSourceActive) {
  771. // Displaying current peak counter.
  772. if (currentPeakCount > 99999999) {
  773. currentPeakCount = 100000000;
  774. xOffsetValue = 3;
  775. if (!bBlinkErr) {
  776. bBlinkErr = true;
  777. bBlinkErrAlt = true;
  778. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  779. }
  780. else {
  781. if (bBlinkErrTimer > 0) {
  782. bBlinkErrTimer--; // Delaying...
  783. }
  784. else {
  785. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  786. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  787. }
  788. if (bBlinkErrAlt)
  789. strcpy(dmdMessage2, "!Overflow!");
  790. else strcpy(dmdMessage2, "");
  791. }
  792. }
  793. else {
  794. xOffsetValue = 2;
  795. snprintf(dmdMessage2, sizeof(dmdMessage2), "%8lu", currentPeakCount);
  796. }
  797. }
  798. else {
  799. xOffsetValue = -1;
  800. if (!bBlinkErr) {
  801. bBlinkErr = true;
  802. bBlinkErrAlt = true;
  803. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  804. }
  805. else {
  806. if (bBlinkErrTimer > 0) {
  807. bBlinkErrTimer--; // Delaying...
  808. }
  809. else {
  810. bBlinkErrAlt = !bBlinkErrAlt; // Alternate display.
  811. bBlinkErrTimer = round(engineGetSampleRate() / 4);
  812. }
  813. if (bBlinkErrAlt)
  814. strcpy(dmdMessage2, "? Input ?");
  815. else strcpy(dmdMessage2, "");
  816. }
  817. }
  818. }
  819. }
  820. }
  821. // Dot-matrix display (DMD) handler. Mainly hardcoded for best performances.
  822. struct MetriksDMD : TransparentWidget {
  823. Metriks *module;
  824. std::shared_ptr<Font> font;
  825. MetriksDMD() {
  826. font = Font::load(assetPlugin(plugin, "res/fonts/LEDCounter7.ttf"));
  827. }
  828. void updateDMD1(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, char* dmdMessage1) {
  829. nvgFontSize(vg, 16);
  830. nvgFontFaceId(vg, font->handle);
  831. nvgTextLetterSpacing(vg, -2);
  832. nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff));
  833. nvgText(vg, pos.x, pos.y, dmdMessage1, NULL);
  834. }
  835. void updateDMD2(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, int xOffsetValue, char* dmdMessage2) {
  836. nvgFontSize(vg, 20);
  837. nvgFontFaceId(vg, font->handle);
  838. nvgTextLetterSpacing(vg, -1);
  839. nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff));
  840. nvgText(vg, pos.x + xOffsetValue, pos.y, dmdMessage2, NULL);
  841. }
  842. void draw(NVGcontext *vg) override {
  843. updateDMD1(vg, Vec(14, box.size.y - 174), module->DMDtextColor, module->dmdMessage1);
  844. updateDMD2(vg, Vec(12, box.size.y - 152), module->DMDtextColor, module->xOffsetValue, module->dmdMessage2);
  845. }
  846. };
  847. struct MetriksWidget : ModuleWidget {
  848. // Themed plates.
  849. SVGPanel *panelClassic;
  850. SVGPanel *panelStageRepro;
  851. SVGPanel *panelAbsoluteNight;
  852. SVGPanel *panelDarkSignature;
  853. SVGPanel *panelDeepBlueSignature;
  854. SVGPanel *panelCarbonSignature;
  855. // Silver Torx screws.
  856. SVGScrew *topLeftScrewSilver;
  857. SVGScrew *topRightScrewSilver;
  858. SVGScrew *bottomLeftScrewSilver;
  859. SVGScrew *bottomRightScrewSilver;
  860. // Gold Torx screws.
  861. SVGScrew *topLeftScrewGold;
  862. SVGScrew *topRightScrewGold;
  863. SVGScrew *bottomLeftScrewGold;
  864. SVGScrew *bottomRightScrewGold;
  865. // Silver MODE button.
  866. SVGSwitch *buttonModeSilver;
  867. // Gold MODE button.
  868. SVGSwitch *buttonModeGold;
  869. // Silver PLAY/PAUSE (toggle) button.
  870. SVGSwitch *buttonPlayPauseSilver;
  871. // Gold PLAY/PAUSE (toggle) button.
  872. SVGSwitch *buttonPlayPauseGold;
  873. // Silver RESET button.
  874. SVGSwitch *buttonResetSilver;
  875. // Gold RESET button.
  876. SVGSwitch *buttonResetGold;
  877. //
  878. MetriksWidget(Metriks *module);
  879. void step() override;
  880. // Action for "Initialize", from context-menu, is (for now) bypassed.
  881. void reset() override {
  882. return;
  883. };
  884. // Action for "Randomize", from context-menu, is (for now) bypassed.
  885. void randomize() override {
  886. return;
  887. };
  888. Menu* createContextMenu() override;
  889. };
  890. MetriksWidget::MetriksWidget(Metriks *module) : ModuleWidget(module) {
  891. box.size = Vec(8 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT);
  892. // Model: Classic (beige plate, always default GUI when added from modules menu).
  893. panelClassic = new SVGPanel();
  894. panelClassic->box.size = box.size;
  895. panelClassic->setBackground(SVG::load(assetPlugin(plugin, "res/Metriks_Classic.svg")));
  896. addChild(panelClassic);
  897. // Model: Stage Repro.
  898. panelStageRepro = new SVGPanel();
  899. panelStageRepro->box.size = box.size;
  900. panelStageRepro->setBackground(SVG::load(assetPlugin(plugin, "res/Metriks_Stage_Repro.svg")));
  901. addChild(panelStageRepro);
  902. // Model: Absolute Night.
  903. panelAbsoluteNight = new SVGPanel();
  904. panelAbsoluteNight->box.size = box.size;
  905. panelAbsoluteNight->setBackground(SVG::load(assetPlugin(plugin, "res/Metriks_Absolute_Night.svg")));
  906. addChild(panelAbsoluteNight);
  907. // Model: Dark "Signature".
  908. panelDarkSignature = new SVGPanel();
  909. panelDarkSignature->box.size = box.size;
  910. panelDarkSignature->setBackground(SVG::load(assetPlugin(plugin, "res/Metriks_Dark_Signature.svg")));
  911. addChild(panelDarkSignature);
  912. // Model: Deepblue "Signature".
  913. panelDeepBlueSignature = new SVGPanel();
  914. panelDeepBlueSignature->box.size = box.size;
  915. panelDeepBlueSignature->setBackground(SVG::load(assetPlugin(plugin, "res/Metriks_Deepblue_Signature.svg")));
  916. addChild(panelDeepBlueSignature);
  917. // Model: Carbon "Signature".
  918. panelCarbonSignature = new SVGPanel();
  919. panelCarbonSignature->box.size = box.size;
  920. panelCarbonSignature->setBackground(SVG::load(assetPlugin(plugin, "res/Metriks_Carbon_Signature.svg")));
  921. addChild(panelCarbonSignature);
  922. // Always four screws for 12 HP module, may are silver or gold, depending model.
  923. // Top-left silver Torx screw.
  924. topLeftScrewSilver = Widget::create<Torx_Silver>(Vec(RACK_GRID_WIDTH, 0));
  925. addChild(topLeftScrewSilver);
  926. // Top-right silver Torx screw.
  927. topRightScrewSilver = Widget::create<Torx_Silver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0));
  928. addChild(topRightScrewSilver);
  929. // Bottom-left silver Torx screw.
  930. bottomLeftScrewSilver = Widget::create<Torx_Silver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH));
  931. addChild(bottomLeftScrewSilver);
  932. // Bottom-right silver Torx screw.
  933. bottomRightScrewSilver = Widget::create<Torx_Silver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH));
  934. addChild(bottomRightScrewSilver);
  935. // Top-left gold Torx screw.
  936. topLeftScrewGold = Widget::create<Torx_Gold>(Vec(RACK_GRID_WIDTH, 0));
  937. addChild(topLeftScrewGold);
  938. // Top-right gold Torx screw.
  939. topRightScrewGold = Widget::create<Torx_Gold>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0));
  940. addChild(topRightScrewGold);
  941. // Bottom-left gold Torx screw.
  942. bottomLeftScrewGold = Widget::create<Torx_Gold>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH));
  943. addChild(bottomLeftScrewGold);
  944. // Bottom-right gold Torx screw.
  945. bottomRightScrewGold = Widget::create<Torx_Gold>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH));
  946. addChild(bottomRightScrewGold);
  947. // DMD Display.
  948. {
  949. MetriksDMD *display = new MetriksDMD();
  950. display->module = module;
  951. display->box.pos = Vec(0, 0);
  952. display->box.size = Vec(box.size.x, 234);
  953. addChild(display);
  954. }
  955. // Input ports (golden jacks).
  956. addInput(Port::create<PJ301M_In>(Vec(24, 304), Port::INPUT, module, Metriks::INPUT_SOURCE));
  957. addInput(Port::create<PJ301M_In>(Vec(24, 262), Port::INPUT, module, Metriks::INPUT_PLAYPAUSE));
  958. addInput(Port::create<PJ301M_In>(Vec(72, 262), Port::INPUT, module, Metriks::INPUT_RESET));
  959. // Output port (golden jacks).
  960. addOutput(Port::create<PJ301M_Out>(Vec(72, 304), Port::OUTPUT, module, Metriks::OUTPUT_THRU));
  961. // Encoder (using same encoder than KlokSpid module).
  962. //addParam(ParamWidget::create<KS_Encoder>(Vec(20, 106), module, Metriks::PARAM_ENCODER, -INFINITY, +INFINITY, 0.0));
  963. module->mtksEncoder = dynamic_cast<Knob*>(Knob::create<KS_Encoder>(Vec(20, 106), module, Metriks::PARAM_ENCODER, -INFINITY, +INFINITY, 0.0));
  964. addParam(module->mtksEncoder);
  965. // Push button (silver) used to select MODE (same used by KlokSpid modules).
  966. buttonModeSilver = ParamWidget::create<KS_ButtonSilver>(Vec(94, 178), module, Metriks::BUTTON_MODE, 0.0, 1.0, 0.0);
  967. addParam(buttonModeSilver);
  968. // Push button (gold) used to select MODE (same used by KlokSpid modules).
  969. buttonModeGold = ParamWidget::create<KS_ButtonGold>(Vec(94, 178), module, Metriks::BUTTON_MODE, 0.0, 1.0, 0.0);
  970. addParam(buttonModeGold);
  971. // Push button (silver) above toggle input port, used to toggle PLAY/PAUSE (same used by KlokSpid modules).
  972. buttonPlayPauseSilver = ParamWidget::create<KS_ButtonSilver>(Vec(27.4, 240), module, Metriks::BUTTON_PLAYPAUSE, 0.0, 1.0, 0.0);
  973. addParam(buttonPlayPauseSilver);
  974. // Push button (gold) above toggle input port, used to toggle PLAY/PAUSE (same used by KlokSpid modules).
  975. buttonPlayPauseGold = ParamWidget::create<KS_ButtonGold>(Vec(27.4, 240), module, Metriks::BUTTON_PLAYPAUSE, 0.0, 1.0, 0.0);
  976. addParam(buttonPlayPauseGold);
  977. // Push button (silver) above RST input port, used to RESET peak counter (same used by KlokSpid modules).
  978. buttonResetSilver = ParamWidget::create<KS_ButtonSilver>(Vec(75.4, 240), module, Metriks::BUTTON_RESET, 0.0, 1.0, 0.0);
  979. addParam(buttonResetSilver);
  980. // Push button (gold) above RST input port, used to RESET peak counter (same used by KlokSpid modules).
  981. buttonResetGold = ParamWidget::create<KS_ButtonGold>(Vec(75.4, 240), module, Metriks::BUTTON_RESET, 0.0, 1.0, 0.0);
  982. addParam(buttonResetGold);
  983. // PLAY/STOP (bicolored green/red) LED.
  984. addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(Vec(18, 252), module, Metriks::LED_PLAY_GREEN));
  985. }
  986. //// Make one visible theme at once!
  987. void MetriksWidget::step() {
  988. Metriks *metriks = dynamic_cast<Metriks*>(module);
  989. assert(metriks);
  990. // "Signature"-line modules are using gold parts (instead of silver).
  991. bool isSignatureLine = (metriks->Theme > 2);
  992. // Themed module plate, selected via context-menu, is visible (all others are obviously, hidden).
  993. panelClassic->visible = (metriks->Theme == 0);
  994. panelStageRepro->visible = (metriks->Theme == 1);
  995. panelAbsoluteNight->visible = (metriks->Theme == 2);
  996. panelDarkSignature->visible = (metriks->Theme == 3);
  997. panelDeepBlueSignature->visible = (metriks->Theme == 4);
  998. panelCarbonSignature->visible = (metriks->Theme == 5);
  999. // Silver Torx screws are visible only for non-"Signature" modules.
  1000. topLeftScrewSilver->visible = !isSignatureLine;
  1001. topRightScrewSilver->visible = !isSignatureLine;
  1002. bottomLeftScrewSilver->visible = !isSignatureLine;
  1003. bottomRightScrewSilver->visible = !isSignatureLine;
  1004. // Gold Torx screws are visible only for "Signature" modules.
  1005. topLeftScrewGold->visible = isSignatureLine;
  1006. topRightScrewGold->visible = isSignatureLine;
  1007. bottomLeftScrewGold->visible = isSignatureLine;
  1008. bottomRightScrewGold->visible = isSignatureLine;
  1009. // Silver or gold MODE button is visible at once (opposite is, obvisouly, hidden).
  1010. buttonModeSilver->visible = !isSignatureLine;
  1011. buttonModeGold->visible = isSignatureLine;
  1012. // Silver or gold PLAY/PAUSE button is visible at once (opposite is, obvisouly, hidden).
  1013. buttonPlayPauseSilver->visible = !isSignatureLine;
  1014. buttonPlayPauseGold->visible = isSignatureLine;
  1015. // Silver or gold RESET button is visible at once (opposite is, obvisouly, hidden).
  1016. buttonResetSilver->visible = !isSignatureLine;
  1017. buttonResetGold->visible = isSignatureLine;
  1018. // Resume original step() method.
  1019. ModuleWidget::step();
  1020. }
  1021. //// CONTEXT-MENU (RIGHT-CLICK ON MODULE).
  1022. // Classic (default beige) module.
  1023. struct metriksClassicMenu : MenuItem {
  1024. Metriks *metriks;
  1025. void onAction(EventAction &e) override {
  1026. metriks->Theme = 0;
  1027. }
  1028. void step() override {
  1029. rightText = (metriks->Theme == 0) ? "✔" : "";
  1030. MenuItem::step();
  1031. }
  1032. };
  1033. // Stage Repro module.
  1034. struct metriksStageReproMenu : MenuItem {
  1035. Metriks *metriks;
  1036. void onAction(EventAction &e) override {
  1037. metriks->Theme = 1;
  1038. }
  1039. void step() override {
  1040. rightText = (metriks->Theme == 1) ? "✔" : "";
  1041. MenuItem::step();
  1042. }
  1043. };
  1044. // Absolute Night module.
  1045. struct metriksAbsoluteNightMenu : MenuItem {
  1046. Metriks *metriks;
  1047. void onAction(EventAction &e) override {
  1048. metriks->Theme = 2;
  1049. }
  1050. void step() override {
  1051. rightText = (metriks->Theme == 2) ? "✔" : "";
  1052. MenuItem::step();
  1053. }
  1054. };
  1055. // Dark "Signature" module.
  1056. struct metriksDarkSignatureMenu : MenuItem {
  1057. Metriks *metriks;
  1058. void onAction(EventAction &e) override {
  1059. metriks->Theme = 3;
  1060. }
  1061. void step() override {
  1062. rightText = (metriks->Theme == 3) ? "✔" : "";
  1063. MenuItem::step();
  1064. }
  1065. };
  1066. // Deepblue "Signature" module.
  1067. struct metriksDeepBlueSignatureMenu : MenuItem {
  1068. Metriks *metriks;
  1069. void onAction(EventAction &e) override {
  1070. metriks->Theme = 4;
  1071. }
  1072. void step() override {
  1073. rightText = (metriks->Theme == 4) ? "✔" : "";
  1074. MenuItem::step();
  1075. }
  1076. };
  1077. // Carbon "Signature" module.
  1078. struct metriksCarbonSignatureMenu : MenuItem {
  1079. Metriks *metriks;
  1080. void onAction(EventAction &e) override {
  1081. metriks->Theme = 5;
  1082. }
  1083. void step() override {
  1084. rightText = (metriks->Theme == 5) ? "✔" : "";
  1085. MenuItem::step();
  1086. }
  1087. };
  1088. // CONTEXT-MENU CONSTRUCTION.
  1089. Menu* MetriksWidget::createContextMenu() {
  1090. Menu* menu = ModuleWidget::createContextMenu();
  1091. Metriks *metriks = dynamic_cast<Metriks*>(module);
  1092. assert(metriks);
  1093. menu->addChild(construct<MenuEntry>());
  1094. menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Model:"));
  1095. menu->addChild(construct<metriksClassicMenu>(&metriksClassicMenu::text, "Classic (default)", &metriksClassicMenu::metriks, metriks));
  1096. menu->addChild(construct<metriksStageReproMenu>(&metriksStageReproMenu::text, "Stage Repro", &metriksStageReproMenu::metriks, metriks));
  1097. menu->addChild(construct<metriksAbsoluteNightMenu>(&metriksAbsoluteNightMenu::text, "Absolute Night", &metriksAbsoluteNightMenu::metriks, metriks));
  1098. menu->addChild(construct<metriksDarkSignatureMenu>(&metriksDarkSignatureMenu::text, "Dark \"Signature\"", &metriksDarkSignatureMenu::metriks, metriks));
  1099. menu->addChild(construct<metriksDeepBlueSignatureMenu>(&metriksDeepBlueSignatureMenu::text, "Deepblue \"Signature\"", &metriksDeepBlueSignatureMenu::metriks, metriks));
  1100. menu->addChild(construct<metriksCarbonSignatureMenu>(&metriksCarbonSignatureMenu::text, "Carbon \"Signature\"", &metriksCarbonSignatureMenu::metriks, metriks));
  1101. return menu;
  1102. }
  1103. } // namespace rack_plugin_Ohmer
  1104. using namespace rack_plugin_Ohmer;
  1105. RACK_PLUGIN_MODEL_INIT(Ohmer, Metriks) {
  1106. Model *modelMetriks = Model::create<Metriks, MetriksWidget>("Ohmer Modules", "Metriks", "Metriks", VISUAL_TAG);
  1107. return modelMetriks;
  1108. }