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.

508 lines
14KB

  1. #include "Squinky.hpp"
  2. #ifdef _EV3
  3. #include "ctrl/WaveformSelector.h"
  4. #include "ctrl/SqWidgets.h"
  5. #include "ctrl/SqMenuItem.h"
  6. #include "WidgetComposite.h"
  7. #include "EV3.h"
  8. #include <sstream>
  9. struct EV3Module : Module
  10. {
  11. EV3Module();
  12. void step() override;
  13. EV3<WidgetComposite> ev3;
  14. };
  15. EV3Module::EV3Module()
  16. : Module(ev3.NUM_PARAMS,
  17. ev3.NUM_INPUTS,
  18. ev3.NUM_OUTPUTS,
  19. ev3.NUM_LIGHTS),
  20. ev3(this)
  21. {
  22. }
  23. void EV3Module::step()
  24. {
  25. ev3.step();
  26. }
  27. /************************************************************/
  28. class EV3PitchDisplay
  29. {
  30. public:
  31. EV3PitchDisplay(EV3Module * mod) : module(mod)
  32. {
  33. }
  34. void step();
  35. /**
  36. * Labels must be added in order
  37. */
  38. void addOctLabel(Label*);
  39. void addSemiLabel(Label*);
  40. private:
  41. EV3Module * const module;
  42. std::vector<Label*> octLabels;
  43. std::vector<Label*> semiLabels;
  44. std::vector<float> semiX;
  45. int lastOctave[3] = {100, 100, 100};
  46. int lastSemi[3] = {100, 100, 100};
  47. bool lastPatched[3] = {false, false, false};
  48. void updateAbsolute(int);
  49. void updateInterval(int);
  50. bool shouldUseInterval(int osc);
  51. };
  52. void EV3PitchDisplay::step()
  53. {
  54. bool atLeastOneChanged = false;
  55. const int delta = EV3<WidgetComposite>::OCTAVE2_PARAM - EV3<WidgetComposite>::OCTAVE1_PARAM;
  56. for (int i = 0; i < 3; ++i) {
  57. const int octaveParam = EV3<WidgetComposite>::OCTAVE1_PARAM + delta * i;
  58. const int semiParam = EV3<WidgetComposite>::SEMI1_PARAM + delta * i;
  59. const int inputId = EV3<WidgetComposite>::CV1_INPUT + i;
  60. const int oct = module->params[octaveParam].value;
  61. const int semi = module->params[semiParam].value;
  62. const bool patched = module->inputs[inputId].active;
  63. if (semi != lastSemi[i] ||
  64. oct != lastOctave[i] ||
  65. patched != lastPatched[i]) {
  66. atLeastOneChanged = true;
  67. lastSemi[i] = semi;
  68. lastOctave[i] = oct;
  69. lastPatched[i] = patched;
  70. }
  71. }
  72. if (atLeastOneChanged) {
  73. for (int i = 0; i < 3; ++i) {
  74. if (shouldUseInterval(i)) {
  75. updateInterval(i);
  76. } else {
  77. updateAbsolute(i);
  78. }
  79. }
  80. }
  81. }
  82. static const char* intervalNames[] = {
  83. "0",
  84. "m2nd",
  85. "2nd",
  86. "m3rd",
  87. "M3rd",
  88. "4th",
  89. "Dim5th",
  90. "5th",
  91. "m6th",
  92. "M6th",
  93. "m7th",
  94. "M7th",
  95. "oct"
  96. };
  97. const static int offsetNatural = 10;
  98. const static int offsetAccidental = 7;
  99. static const int pitchOffsets[] = {
  100. offsetNatural,
  101. offsetAccidental,
  102. offsetNatural, // D
  103. offsetAccidental,
  104. offsetNatural, // E
  105. offsetNatural, // F
  106. offsetAccidental,
  107. offsetNatural, // g
  108. offsetAccidental,
  109. offsetNatural,
  110. offsetAccidental,
  111. offsetNatural, //b
  112. };
  113. static const char* pitchNames[] = {
  114. "C",
  115. "C#",
  116. "D",
  117. "D#",
  118. "E",
  119. "F",
  120. "F#",
  121. "G",
  122. "G#",
  123. "A",
  124. "A#",
  125. "B"
  126. };
  127. static const int intervalOffsets[] = {
  128. 11,
  129. -3,
  130. 3, // 2nd
  131. 0,
  132. 0,
  133. 4, // 4th
  134. -5,
  135. 4, // 5th
  136. 0,
  137. 0,
  138. 0,
  139. 2,
  140. 2 // M7
  141. };
  142. void EV3PitchDisplay::addOctLabel(Label* l)
  143. {
  144. octLabels.push_back(l);
  145. }
  146. void EV3PitchDisplay::addSemiLabel(Label* l)
  147. {
  148. semiLabels.push_back(l);
  149. semiX.push_back(l->box.pos.x);
  150. }
  151. bool EV3PitchDisplay::shouldUseInterval(int osc)
  152. {
  153. bool ret = false; // always safe to use abolute
  154. if (lastPatched[osc]) {
  155. ret = false; // if current one is patched, use absolute
  156. } else if ((osc > 0) && lastPatched[osc - 1]) {
  157. ret = true; // if prev patched and we are not, go for it
  158. } else if ((osc > 1) && lastPatched[osc - 2]) {
  159. ret = true; // if prev-prev patched and we are not, go for it
  160. }
  161. //printf("should use interval (%d) ret %d", osc, ret);
  162. return ret;
  163. }
  164. void EV3PitchDisplay::updateInterval(int osc)
  165. {
  166. int refSemi = 0;
  167. int refOctave = 0;
  168. int oct = 5 + lastOctave[osc];
  169. int semi = lastSemi[osc];
  170. assert(osc > 0);
  171. const bool prevPatched = lastPatched[osc - 1];
  172. if (prevPatched) {
  173. refOctave = 5 + lastOctave[osc - 1];
  174. refSemi = lastSemi[osc - 1];
  175. // printf("got from prev %d (%d, %d)\n", osc-1, refOctave, refSemi);
  176. } else {
  177. assert(osc > 1);
  178. refOctave = 5 + lastOctave[osc - 2];
  179. refSemi = lastSemi[osc - 2];
  180. // printf("got from prev %d (%d, %d)\n", osc-2, refOctave, refSemi);
  181. }
  182. const int currentPitch = oct * 12 + semi;
  183. const int refPitch = refOctave * 12 + refSemi;
  184. const int relativePitch = currentPitch - refPitch;
  185. int adjustedOctave = 0;
  186. int adjustedSemi = 0;
  187. adjustedOctave = relativePitch / 12;
  188. adjustedSemi = relativePitch - (adjustedOctave * 12);
  189. if (adjustedSemi < 0) {
  190. adjustedOctave--;
  191. adjustedSemi += 12;
  192. }
  193. assert(adjustedSemi >= 0);
  194. assert(adjustedSemi < 12);
  195. std::stringstream so;
  196. so << adjustedOctave;
  197. octLabels[osc]->text = so.str();
  198. semiLabels[osc]->text = intervalNames[adjustedSemi];
  199. semiLabels[osc]->box.pos.x = semiX[osc] + intervalOffsets[adjustedSemi];
  200. }
  201. void EV3PitchDisplay::updateAbsolute(int osc)
  202. {
  203. std::stringstream so;
  204. int oct = 5 + lastOctave[osc];
  205. int semi = lastSemi[osc];
  206. if (semi < 0) {
  207. --oct;
  208. semi += 12;
  209. }
  210. so << oct;
  211. octLabels[osc]->text = so.str();
  212. semiLabels[osc]->text = pitchNames[semi];
  213. semiLabels[osc]->box.pos.x = semiX[osc] + pitchOffsets[semi];
  214. }
  215. struct EV3Widget : ModuleWidget
  216. {
  217. EV3Widget(EV3Module *);
  218. void makeSections(EV3Module *);
  219. void makeSection(EV3Module *, int index);
  220. void makeInputs(EV3Module *);
  221. void makeInput(EV3Module* module, int row, int col, int input,
  222. const char* name, float labelDeltaX);
  223. void makeOutputs(EV3Module *);
  224. Label* addLabel(const Vec& v, const char* str, const NVGcolor& color = COLOR_BLACK)
  225. {
  226. Label* label = new Label();
  227. label->box.pos = v;
  228. label->text = str;
  229. label->color = color;
  230. addChild(label);
  231. return label;
  232. }
  233. void step() override;
  234. Menu* createContextMenu() override;
  235. EV3PitchDisplay pitchDisplay;
  236. EV3Module* const module;
  237. Label* plusOne = nullptr;
  238. Label* plusTwo = nullptr;
  239. bool wasNormalizing = false;
  240. };
  241. inline Menu* EV3Widget::createContextMenu()
  242. {
  243. Menu* theMenu = ModuleWidget::createContextMenu();
  244. ManualMenuItem* manual = new ManualMenuItem(
  245. "https://github.com/squinkylabs/SquinkyVCV/blob/master/docs/ev3.md");
  246. theMenu->addChild(manual);
  247. return theMenu;
  248. }
  249. static const NVGcolor COLOR_GREEN2 = nvgRGB(0x90, 0xff, 0x3e);
  250. void EV3Widget::step()
  251. {
  252. ModuleWidget::step();
  253. pitchDisplay.step();
  254. bool norm = module->ev3.isLoweringVolume();
  255. if (norm != wasNormalizing) {
  256. wasNormalizing = norm;
  257. auto color = norm ? COLOR_GREEN2 : COLOR_WHITE;
  258. plusOne->color = color;
  259. plusTwo->color = color;
  260. }
  261. }
  262. const int dy = -6; // apply to everything
  263. void EV3Widget::makeSection(EV3Module *module, int index)
  264. {
  265. const float x = (30 - 4) + index * (86 + 4);
  266. const float x2 = x + (36 + 2);
  267. const float y = 80 + dy;
  268. const float y2 = y + 56 + dy;
  269. const float y3 = y2 + 40 + dy;
  270. const int delta = EV3<WidgetComposite>::OCTAVE2_PARAM - EV3<WidgetComposite>::OCTAVE1_PARAM;
  271. pitchDisplay.addOctLabel(
  272. addLabel(Vec(x - 10, y - 32), "Oct"));
  273. pitchDisplay.addSemiLabel(
  274. addLabel(Vec(x2 - 22, y - 32), "Semi"));
  275. addParam(createParamCentered<Blue30SnapKnob>(
  276. Vec(x, y), module,
  277. EV3<WidgetComposite>::OCTAVE1_PARAM + delta * index,
  278. -5.0f, 4.0f, 0.f));
  279. addParam(createParamCentered<Blue30SnapKnob>(
  280. Vec(x2, y), module,
  281. EV3<WidgetComposite>::SEMI1_PARAM + delta * index,
  282. -11.f, 11.0f, 0.f));
  283. addParam(createParamCentered<Blue30Knob>(
  284. Vec(x, y2), module,
  285. EV3<WidgetComposite>::FINE1_PARAM + delta * index,
  286. -1.0f, 1.0f, 0));
  287. addLabel(Vec(x - 20, y2 - 34), "Fine");
  288. addParam(createParamCentered<Blue30Knob>(
  289. Vec(x2, y2), module,
  290. EV3<WidgetComposite>::FM1_PARAM + delta * index,
  291. 0.f, 1.f, 0));
  292. addLabel(Vec(x2 - 19, y2 - 34), "Mod");
  293. const float dy = 27;
  294. const float x0 = x;
  295. addParam(createParamCentered<Trimpot>(
  296. Vec(x0, y3), module, EV3<WidgetComposite>::PW1_PARAM + delta * index,
  297. -1.f, 1.f, 0));
  298. if (index == 0)
  299. addLabel(Vec(x0 + 10, y3 - 8), "pw");
  300. addParam(createParamCentered<Trimpot>(
  301. Vec(x0, y3 + dy), module,
  302. EV3<WidgetComposite>::PWM1_PARAM + delta * index,
  303. -1.0f, 1.0f, 0));
  304. if (index == 0)
  305. addLabel(Vec(x0 + 10, y3 + dy - 8), "pwm");
  306. // sync switches
  307. const float swx = x + 29;
  308. const float lbx = x + 19;
  309. if (index != 0) {
  310. addParam(ParamWidget::create<CKSS>(
  311. Vec(swx, y3), module, EV3<WidgetComposite>::SYNC1_PARAM + delta * index,
  312. 0.0f, 1.0f, 0.0f));
  313. addLabel(Vec(lbx - 3, y3 - 20), "sync");
  314. addLabel(Vec(lbx + 2, y3 + 20), "off");
  315. }
  316. const float y4 = y3 + 43;
  317. const float xx = x - 12;
  318. // include one extra wf - none
  319. const float numWaves = (float) EV3<WidgetComposite>::Waves::END;
  320. const float defWave = (float) EV3<WidgetComposite>::Waves::SIN;
  321. addParam(ParamWidget::create<WaveformSelector>(
  322. Vec(xx, y4),
  323. module,
  324. EV3<WidgetComposite>::WAVE1_PARAM + delta * index,
  325. 0.0f, numWaves, defWave));
  326. }
  327. void EV3Widget::makeSections(EV3Module* module)
  328. {
  329. makeSection(module, 0);
  330. makeSection(module, 1);
  331. makeSection(module, 2);
  332. }
  333. const float row1Y = 280 + dy - 4; // -4 = move the last section up
  334. const float rowDY = 30;
  335. const float colDX = 45;
  336. void EV3Widget::makeInput(EV3Module* module, int row, int col,
  337. int inputNum, const char* name, float labelXDelta)
  338. {
  339. EV3<WidgetComposite>::InputIds input = EV3<WidgetComposite>::InputIds(inputNum);
  340. const float y = row1Y + row * rowDY;
  341. const float x = 14 + col * colDX;
  342. const float labelX = labelXDelta + x - 6;
  343. addInput(Port::create<PJ301MPort>(
  344. Vec(x, y), Port::INPUT, module, input));
  345. if (row == 0)
  346. addLabel(Vec(labelX, y - 20), name);
  347. }
  348. void EV3Widget::makeInputs(EV3Module* module)
  349. {
  350. // Row 0 = top row, 2 = bottom row
  351. auto row2Input = [](int row, EV3<WidgetComposite>::InputIds baseInput) {
  352. // map inputs directly to rows
  353. return baseInput + row;
  354. };
  355. for (int row = 0; row < 3; ++row) {
  356. makeInput(module, row, 0,
  357. row2Input(row, EV3<WidgetComposite>::CV1_INPUT),
  358. "V/oct", -3);
  359. makeInput(module, row, 1,
  360. row2Input(row, EV3<WidgetComposite>::FM1_INPUT),
  361. "Fm", 3);
  362. makeInput(module, row, 2,
  363. row2Input(row, EV3<WidgetComposite>::PWM1_INPUT),
  364. "Pwm", -2);
  365. }
  366. }
  367. void EV3Widget::makeOutputs(EV3Module *)
  368. {
  369. const float x = 160;
  370. const float trimY = row1Y + 11;
  371. const float outX = x + 30;
  372. addParam(createParamCentered<Trimpot>(
  373. Vec(x, trimY), module, EV3<WidgetComposite>::MIX1_PARAM,
  374. 0.0f, 1.0f, 0));
  375. addParam(createParamCentered<Trimpot>(
  376. Vec(x, trimY + rowDY), module, EV3<WidgetComposite>::MIX2_PARAM,
  377. 0.0f, 1.0f, 0));
  378. addParam(createParamCentered<Trimpot>(
  379. Vec(x, trimY + 2 * rowDY), module, EV3<WidgetComposite>::MIX3_PARAM,
  380. 0.0f, 1.0f, 0));
  381. addOutput(Port::create<PJ301MPort>(
  382. Vec(outX, row1Y),
  383. Port::OUTPUT, module, EV3<WidgetComposite>::VCO1_OUTPUT));
  384. addLabel(Vec(outX + 20, row1Y + 0 * rowDY + 2), "1", COLOR_WHITE);
  385. addOutput(Port::create<PJ301MPort>(
  386. Vec(outX, row1Y + rowDY),
  387. Port::OUTPUT, module, EV3<WidgetComposite>::VCO2_OUTPUT));
  388. addLabel(Vec(outX + 20, row1Y + 1 * rowDY + 2), "2", COLOR_WHITE);
  389. addOutput(Port::create<PJ301MPort>(
  390. Vec(outX, row1Y + 2 * rowDY),
  391. Port::OUTPUT, module, EV3<WidgetComposite>::VCO3_OUTPUT));
  392. addLabel(Vec(outX + 20, row1Y + 2 * rowDY + 2), "3", COLOR_WHITE);
  393. addOutput(Port::create<PJ301MPort>(
  394. Vec(outX + 41, row1Y + rowDY),
  395. Port::OUTPUT, module, EV3<WidgetComposite>::MIX_OUTPUT));
  396. plusOne = addLabel(Vec(outX + 41, row1Y + rowDY - 17), "+", COLOR_WHITE);
  397. plusTwo = addLabel(Vec(outX + 41, row1Y + rowDY + 20), "+", COLOR_WHITE);
  398. }
  399. /**
  400. * Widget constructor will describe my implementation structure and
  401. * provide meta-data.
  402. * This is not shared by all modules in the DLL, just one
  403. */
  404. EV3Widget::EV3Widget(EV3Module *module) :
  405. ModuleWidget(module),
  406. pitchDisplay(module),
  407. module(module)
  408. {
  409. box.size = Vec(18 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT);
  410. {
  411. SVGPanel *panel = new SVGPanel();
  412. panel->box.size = box.size;
  413. panel->setBackground(SVG::load(assetPlugin(plugin, "res/ev3_panel.svg")));
  414. addChild(panel);
  415. }
  416. makeSections(module);
  417. makeInputs(module);
  418. makeOutputs(module);
  419. // screws
  420. addChild(Widget::create<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
  421. addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
  422. addChild(Widget::create<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  423. addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  424. }
  425. RACK_PLUGIN_MODEL_INIT(squinkylabs_plug1, EV3) {
  426. Model *modelEV3Module = Model::create<EV3Module,
  427. EV3Widget>("Squinky Labs",
  428. "squinkylabs-ev3",
  429. "EV3: Triple VCO with even waveform", OSCILLATOR_TAG);
  430. return modelEV3Module;
  431. }
  432. #endif