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.

804 lines
23KB

  1. #include "plugin.hpp"
  2. /*! \brief Decode System Exclusive messages.
  3. SysEx messages are encoded to guarantee transmission of data bytes higher than
  4. 127 without breaking the MIDI protocol. Use this static method to reassemble
  5. your received message.
  6. \param inSysEx The SysEx data received from MIDI in.
  7. \param outData The output buffer where to store the decrypted message.
  8. \param inLength The length of the input buffer.
  9. \param inFlipHeaderBits True for Korg and other who store MSB in reverse order
  10. \return The length of the output buffer.
  11. @see encodeSysEx @see getSysExArrayLength
  12. Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com
  13. */
  14. unsigned decodeSysEx(const uint8_t* inSysEx,
  15. uint8_t* outData,
  16. unsigned inLength,
  17. bool inFlipHeaderBits) {
  18. unsigned count = 0;
  19. uint8_t msbStorage = 0;
  20. uint8_t byteIndex = 0;
  21. for (unsigned i = 0; i < inLength; ++i) {
  22. if ((i % 8) == 0) {
  23. msbStorage = inSysEx[i];
  24. byteIndex = 6;
  25. }
  26. else {
  27. const uint8_t body = inSysEx[i];
  28. const uint8_t shift = inFlipHeaderBits ? 6 - byteIndex : byteIndex;
  29. const uint8_t msb = uint8_t(((msbStorage >> shift) & 1) << 7);
  30. byteIndex--;
  31. outData[count++] = msb | body;
  32. }
  33. }
  34. return count;
  35. }
  36. struct RoundRobinProcessor {
  37. // if a channel (0 - 11) should be updated, return it's index, otherwise return -1
  38. int process(float sampleTime, float period, int numActiveChannels) {
  39. if (numActiveChannels == 0 || period <= 0) {
  40. return -1;
  41. }
  42. time += sampleTime;
  43. if (time > period) {
  44. time -= period;
  45. // special case: when there's only one channel, the below logic (which looks for when active channel changes)
  46. // wont fire. as we've completed a period, return an "update channel 0" value
  47. if (numActiveChannels == 1) {
  48. return 0;
  49. }
  50. }
  51. int currentActiveChannel = numActiveChannels * time / period;
  52. if (currentActiveChannel != previousActiveChannel) {
  53. previousActiveChannel = currentActiveChannel;
  54. return currentActiveChannel;
  55. }
  56. // if we've got this far, no updates needed (-1)
  57. return -1;
  58. }
  59. private:
  60. float time = 0.f;
  61. int previousActiveChannel = -1;
  62. };
  63. struct MidiThing : Module {
  64. enum ParamId {
  65. REFRESH_PARAM,
  66. PARAMS_LEN
  67. };
  68. enum InputId {
  69. A1_INPUT,
  70. B1_INPUT,
  71. C1_INPUT,
  72. A2_INPUT,
  73. B2_INPUT,
  74. C2_INPUT,
  75. A3_INPUT,
  76. B3_INPUT,
  77. C3_INPUT,
  78. A4_INPUT,
  79. B4_INPUT,
  80. C4_INPUT,
  81. INPUTS_LEN
  82. };
  83. enum OutputId {
  84. OUTPUTS_LEN
  85. };
  86. enum LightId {
  87. LIGHTS_LEN
  88. };
  89. /// Port mode
  90. enum PORTMODE_t {
  91. NOPORTMODE = 0,
  92. MODE10V,
  93. MODEPN5V,
  94. MODENEG10V,
  95. MODE8V,
  96. MODE5V,
  97. LASTPORTMODE
  98. };
  99. const char* cfgPortModeNames[7] = {
  100. "No Mode",
  101. "0/10v",
  102. "-5/5v",
  103. "-10/0v",
  104. "0/8v",
  105. "0/5v",
  106. ""
  107. };
  108. const std::vector<float> updateRates = {250., 500., 1000., 2000., 4000., 8000.};
  109. const std::vector<std::string> updateRateNames = {"250 Hz (fewest active channels, slowest, lowest-cpu)", "500 Hz", "1 kHz", "2 kHz", "4 kHz",
  110. "8 kHz (most active channels, fast, highest-cpu)"
  111. };
  112. int updateRateIdx = 2;
  113. // use Pre-def 4 for bridge mode
  114. const static int VCV_BRIDGE_PREDEF = 4;
  115. midi::Output midiOut;
  116. RoundRobinProcessor roundRobinProcessor;
  117. MidiThing() {
  118. config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
  119. configButton(REFRESH_PARAM, "");
  120. for (int i = 0; i < NUM_INPUTS; ++i) {
  121. portModes[i] = MODE10V;
  122. configInput(A1_INPUT + i, string::f("Port %d", i + 1));
  123. }
  124. }
  125. void onReset() override {
  126. midiOut.reset();
  127. }
  128. void requestAllChannelsParamsOverSysex() {
  129. for (int row = 0; row < 4; ++row) {
  130. for (int col = 0; col < 3; ++col) {
  131. const int PORT_CONFIG = 2;
  132. requestParamOverSysex(row, col, PORT_CONFIG);
  133. }
  134. }
  135. }
  136. // request that MidiThing loads a pre-defined template, 1-4
  137. void setPredef(uint8_t predef) {
  138. predef = clamp(predef, 1, 4);
  139. midi::Message msg;
  140. msg.bytes.resize(8);
  141. // Midi spec is zeroo indexed
  142. uint8_t predefToSend = predef - 1;
  143. msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x00, 0x02, 0x00, predefToSend, 0xF7};
  144. midiOut.setChannel(0);
  145. midiOut.sendMessage(msg);
  146. // DEBUG("Predef %d msg request sent: %s", predef, msg.toString().c_str());
  147. }
  148. void setMidiMergeViaSysEx(bool mergeOn) {
  149. midi::Message msg;
  150. msg.bytes.resize(8);
  151. msg.bytes = {0xF0, 0x7D, 0x19, 0x00, 0x05, 0x02, 0x00, (uint8_t) mergeOn, 0xF7};
  152. midiOut.setChannel(0);
  153. midiOut.sendMessage(msg);
  154. // DEBUG("Predef %d msg request sent: %s", mergeOn, msg.toString().c_str());
  155. }
  156. void setVoltageModeOnHardware(uint8_t row, uint8_t col, PORTMODE_t outputMode_) {
  157. uint8_t port = 3 * row + col;
  158. portModes[port] = outputMode_;
  159. midi::Message msg;
  160. msg.bytes.resize(8);
  161. // F0 7D 17 2n 02 02 00 0m F7
  162. // Where n = 0 based port number
  163. // and m is the volt output mode to select from:
  164. msg.bytes = {0xF0, 0x7D, 0x17, static_cast<unsigned char>(32 + port), 0x02, 0x02, 0x00, (uint8_t) portModes[port], 0xF7};
  165. midiOut.sendMessage(msg);
  166. // DEBUG("Voltage mode msg sent: port %d (%d), mode %d", port, static_cast<unsigned char>(32 + port), portModes[port]);
  167. }
  168. void setVoltageModeOnHardware(uint8_t row, uint8_t col) {
  169. setVoltageModeOnHardware(row, col, portModes[3 * row + col]);
  170. }
  171. void syncVcvStateToHardware() {
  172. for (int row = 0; row < 4; ++row) {
  173. for (int col = 0; col < 3; ++col) {
  174. setVoltageModeOnHardware(row, col);
  175. }
  176. }
  177. }
  178. midi::InputQueue inputQueue;
  179. void requestParamOverSysex(uint8_t row, uint8_t col, uint8_t mode) {
  180. midi::Message msg;
  181. msg.bytes.resize(8);
  182. // F0 7D 17 00 01 03 00 nm pp F7
  183. uint8_t port = 3 * row + col;
  184. //Where n is:
  185. // 0 = Full configuration request. The module will send only pre def, port functions and modified parameters
  186. // 2 = Send Port configuration
  187. // 4 = Send MIDI Channel configuration
  188. // 6 = Send Voice Configuration
  189. uint8_t n = mode * 16;
  190. uint8_t m = port; // element number: 0-11 port number, 1-16 channel or voice number
  191. uint8_t pp = 2;
  192. msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x01, 0x03, 0x00, static_cast<uint8_t>(n + m), pp, 0xF7};
  193. midiOut.sendMessage(msg);
  194. // DEBUG("API request mode msg sent: port %d, pp %s", port, msg.toString().c_str());
  195. }
  196. int getVoltageMode(uint8_t row, uint8_t col) {
  197. // -1 because menu is zero indexed but enum is not
  198. int channel = clamp(3 * row + col, 0, NUM_INPUTS - 1);
  199. return portModes[channel] - 1;
  200. }
  201. const static int NUM_INPUTS = 12;
  202. bool isClipping[NUM_INPUTS] = {};
  203. bool checkIsVoltageWithinRange(uint8_t channel, float voltage) {
  204. const float tol = 0.001;
  205. switch (portModes[channel]) {
  206. case MODE10V: return 0 - tol < voltage && voltage < 10 + tol;
  207. case MODEPN5V: return -5 - tol < voltage && voltage < 5 + tol;
  208. case MODENEG10V: return -10 - tol < voltage && voltage < 0 + tol;
  209. case MODE8V: return 0 - tol < voltage && voltage < 8 + tol;
  210. case MODE5V: return 0 - tol < voltage && voltage < 5 + tol;
  211. default: return false;
  212. }
  213. }
  214. uint16_t rescaleVoltageForChannel(uint8_t channel, float voltage) {
  215. switch (portModes[channel]) {
  216. case MODE10V: return rescale(clamp(voltage, 0.f, 10.f), 0.f, +10.f, 0, 16383);
  217. case MODEPN5V: return rescale(clamp(voltage, -5.f, 5.f), -5.f, +5.f, 0, 16383);
  218. case MODENEG10V: return rescale(clamp(voltage, -10.f, 0.f), -10.f, +0.f, 0, 16383);
  219. case MODE8V: return rescale(clamp(voltage, 0.f, 8.f), 0.f, +8.f, 0, 16383);
  220. case MODE5V: return rescale(clamp(voltage, 0.f, 5.f), 0.f, +5.f, 0, 16383);
  221. default: return 0;
  222. }
  223. }
  224. // one way sync (VCV -> hardware) for now
  225. void doSync() {
  226. // switch to VCV template (predef 4)
  227. setPredef(4);
  228. // disable MIDI merge (otherwise large sample rates will not work)
  229. setMidiMergeViaSysEx(false);
  230. // send full VCV config
  231. syncVcvStateToHardware();
  232. // disabled for now, but this would request what state the hardware is in
  233. if (parseSysExMessagesFromHardware) {
  234. requestAllChannelsParamsOverSysex();
  235. }
  236. }
  237. // debug only
  238. bool parseSysExMessagesFromHardware = false;
  239. int numActiveChannels = 0;
  240. dsp::BooleanTrigger buttonTrigger;
  241. dsp::Timer rateLimiterTimer;
  242. PORTMODE_t portModes[NUM_INPUTS] = {};
  243. void process(const ProcessArgs& args) override {
  244. if (buttonTrigger.process(params[REFRESH_PARAM].getValue())) {
  245. doSync();
  246. }
  247. // disabled for now, but this is how VCV would read SysEx coming from the hardware (if requested above)
  248. if (parseSysExMessagesFromHardware) {
  249. midi::Message msg;
  250. uint8_t outData[32] = {};
  251. while (inputQueue.tryPop(&msg, args.frame)) {
  252. uint8_t outLen = decodeSysEx(&msg.bytes[0], outData, msg.bytes.size(), false);
  253. if (outLen > 3) {
  254. int channel = (outData[2] & 0x0f) >> 0;
  255. if (channel >= 0 && channel < NUM_INPUTS) {
  256. if (outData[outLen - 1] < LASTPORTMODE) {
  257. portModes[channel] = (PORTMODE_t) outData[outLen - 1];
  258. }
  259. }
  260. }
  261. }
  262. }
  263. std::vector<int> activeChannels;
  264. for (int c = 0; c < NUM_INPUTS; ++c) {
  265. if (inputs[A1_INPUT + c].isConnected()) {
  266. activeChannels.push_back(c);
  267. }
  268. }
  269. numActiveChannels = activeChannels.size();
  270. // we're done if no channels are active
  271. if (numActiveChannels == 0) {
  272. return;
  273. }
  274. //DEBUG("updateRateIdx: %d", updateRateIdx);
  275. const float updateRateHz = updateRates[updateRateIdx];
  276. //DEBUG("updateRateHz: %f", updateRateHz);
  277. const int maxCCMessagesPerSecondPerChannel = updateRateHz / numActiveChannels;
  278. // MIDI baud rate is 31250 b/s, or 3125 B/s.
  279. // CC messages are 3 bytes, so we can send a maximum of 1041 CC messages per second.
  280. // The refresh rate period (i.e. how often we can send X channels of data is:
  281. const float rateLimiterPeriod = 1.f / maxCCMessagesPerSecondPerChannel;
  282. // this returns -1 if no channel should be updated, or the index of the channel that should be updated
  283. // it distributes update times in a round robin fashion
  284. int channelIdxToUpdate = roundRobinProcessor.process(args.sampleTime, rateLimiterPeriod, numActiveChannels);
  285. if (channelIdxToUpdate >= 0 && channelIdxToUpdate < numActiveChannels) {
  286. int c = activeChannels[channelIdxToUpdate];
  287. const float channelVoltage = inputs[A1_INPUT + c].getVoltage();
  288. uint16_t pw = rescaleVoltageForChannel(c, channelVoltage);
  289. isClipping[c] = !checkIsVoltageWithinRange(c, channelVoltage);
  290. midi::Message m;
  291. m.setStatus(0xe);
  292. m.setNote(pw & 0x7f);
  293. m.setValue((pw >> 7) & 0x7f);
  294. m.setFrame(args.frame);
  295. midiOut.setChannel(c);
  296. midiOut.sendMessage(m);
  297. }
  298. }
  299. json_t* dataToJson() override {
  300. json_t* rootJ = json_object();
  301. json_object_set_new(rootJ, "midiOutput", midiOut.toJson());
  302. json_object_set_new(rootJ, "inputQueue", inputQueue.toJson());
  303. json_object_set_new(rootJ, "updateRateIdx", json_integer(updateRateIdx));
  304. for (int c = 0; c < NUM_INPUTS; ++c) {
  305. json_object_set_new(rootJ, string::f("portMode%d", c).c_str(), json_integer(portModes[c]));
  306. }
  307. return rootJ;
  308. }
  309. void dataFromJson(json_t* rootJ) override {
  310. json_t* midiOutputJ = json_object_get(rootJ, "midiOutput");
  311. if (midiOutputJ) {
  312. midiOut.fromJson(midiOutputJ);
  313. }
  314. json_t* midiInputQueueJ = json_object_get(rootJ, "inputQueue");
  315. if (midiInputQueueJ) {
  316. inputQueue.fromJson(midiInputQueueJ);
  317. }
  318. json_t* updateRateIdxJ = json_object_get(rootJ, "updateRateIdx");
  319. if (updateRateIdxJ) {
  320. updateRateIdx = json_integer_value(updateRateIdxJ);
  321. }
  322. for (int c = 0; c < NUM_INPUTS; ++c) {
  323. json_t* portModeJ = json_object_get(rootJ, string::f("portMode%d", c).c_str());
  324. if (portModeJ) {
  325. portModes[c] = (PORTMODE_t)json_integer_value(portModeJ);
  326. }
  327. }
  328. // requestAllChannelsParamsOverSysex();
  329. syncVcvStateToHardware();
  330. }
  331. };
  332. struct MidiThingPort : BefacoInputPort {
  333. int row = 0, col = 0;
  334. MidiThing* module;
  335. void appendContextMenu(Menu* menu) override {
  336. menu->addChild(new MenuSeparator());
  337. std::string label = string::f("Voltage Mode Port %d", 3 * row + col + 1);
  338. menu->addChild(createIndexSubmenuItem(label,
  339. {"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"},
  340. [ = ]() {
  341. return module->getVoltageMode(row, col);
  342. },
  343. [ = ](int modeIdx) {
  344. MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(modeIdx + 1);
  345. module->setVoltageModeOnHardware(row, col, mode);
  346. }
  347. ));
  348. /*
  349. menu->addChild(createIndexSubmenuItem("Get Port Info",
  350. {"Full", "Port", "MIDI", "Voice"},
  351. [ = ]() {
  352. return -1;
  353. },
  354. [ = ](int mode) {
  355. module->requestParamOverSysex(row, col, 2 * mode);
  356. }
  357. ));
  358. */
  359. }
  360. };
  361. // dervied from https://github.com/countmodula/VCVRackPlugins/blob/v2.0.0/src/components/CountModulaLEDDisplay.hpp
  362. struct LEDDisplay : LightWidget {
  363. float fontSize = 9;
  364. Vec textPos = Vec(1, 13);
  365. int numChars = 7;
  366. int row = 0, col = 0;
  367. MidiThing* module;
  368. LEDDisplay() {
  369. box.size = mm2px(Vec(9.298, 5.116));
  370. }
  371. void setCentredPos(Vec pos) {
  372. box.pos.x = pos.x - box.size.x / 2;
  373. box.pos.y = pos.y - box.size.y / 2;
  374. }
  375. void drawBackground(const DrawArgs& args) override {
  376. // Background
  377. NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20);
  378. NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
  379. nvgBeginPath(args.vg);
  380. nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
  381. nvgFillColor(args.vg, backgroundColor);
  382. nvgFill(args.vg);
  383. nvgStrokeWidth(args.vg, 1.0);
  384. nvgStrokeColor(args.vg, borderColor);
  385. nvgStroke(args.vg);
  386. }
  387. void drawLight(const DrawArgs& args) override {
  388. // Background
  389. NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20);
  390. NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
  391. NVGcolor textColor = nvgRGB(0xff, 0x10, 0x10);
  392. nvgBeginPath(args.vg);
  393. nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
  394. nvgFillColor(args.vg, backgroundColor);
  395. nvgFill(args.vg);
  396. nvgStrokeWidth(args.vg, 1.0);
  397. if (module) {
  398. const bool isClipping = module->isClipping[col + row * 3];
  399. if (isClipping) {
  400. borderColor = nvgRGB(0xff, 0x20, 0x20);
  401. }
  402. }
  403. nvgStrokeColor(args.vg, borderColor);
  404. nvgStroke(args.vg);
  405. std::shared_ptr<Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));
  406. if (font && font->handle >= 0) {
  407. std::string text = "?-?v"; // fallback if module not yet defined
  408. if (module) {
  409. text = module->cfgPortModeNames[module->getVoltageMode(row, col) + 1];
  410. }
  411. char buffer[numChars + 1];
  412. int l = text.size();
  413. if (l > numChars)
  414. l = numChars;
  415. nvgGlobalTint(args.vg, color::WHITE);
  416. text.copy(buffer, l);
  417. buffer[l] = '\0';
  418. nvgFontSize(args.vg, fontSize);
  419. nvgFontFaceId(args.vg, font->handle);
  420. nvgFillColor(args.vg, textColor);
  421. nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
  422. NVGtextRow textRow;
  423. nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
  424. nvgTextBox(args.vg, textPos.x, textPos.y, box.size.x, textRow.start, textRow.end);
  425. }
  426. }
  427. void onButton(const ButtonEvent& e) override {
  428. if (e.button == GLFW_MOUSE_BUTTON_RIGHT && e.action == GLFW_PRESS) {
  429. ui::Menu* menu = createMenu();
  430. menu->addChild(createMenuLabel(string::f("Voltage mode port %d:", col + 3 * row + 1)));
  431. const std::string labels[5] = {"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"};
  432. for (int i = 0; i < 5; ++i) {
  433. menu->addChild(createCheckMenuItem(labels[i], "",
  434. [ = ]() {
  435. return module->getVoltageMode(row, col) == i;
  436. },
  437. [ = ]() {
  438. MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(i + 1);
  439. module->setVoltageModeOnHardware(row, col, mode);
  440. }
  441. ));
  442. }
  443. e.consume(this);
  444. return;
  445. }
  446. LightWidget::onButton(e);
  447. }
  448. };
  449. struct MidiThingWidget : ModuleWidget {
  450. struct LedDisplayCenterChoiceEx : LedDisplayChoice {
  451. LedDisplayCenterChoiceEx() {
  452. box.size = mm2px(math::Vec(0, 8.0));
  453. color = nvgRGB(0xf0, 0xf0, 0xf0);
  454. bgColor = nvgRGBAf(0, 0, 0, 0);
  455. textOffset = math::Vec(0, 16);
  456. }
  457. void drawLayer(const DrawArgs& args, int layer) override {
  458. nvgScissor(args.vg, RECT_ARGS(args.clipBox));
  459. if (layer == 1) {
  460. if (bgColor.a > 0.0) {
  461. nvgBeginPath(args.vg);
  462. nvgRect(args.vg, 0, 0, box.size.x, box.size.y);
  463. nvgFillColor(args.vg, bgColor);
  464. nvgFill(args.vg);
  465. }
  466. std::shared_ptr<window::Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));
  467. if (font && font->handle >= 0 && !text.empty()) {
  468. nvgFillColor(args.vg, color);
  469. nvgFontFaceId(args.vg, font->handle);
  470. nvgTextLetterSpacing(args.vg, -0.6f);
  471. nvgFontSize(args.vg, 10);
  472. nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
  473. NVGtextRow textRow;
  474. nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
  475. nvgTextBox(args.vg, textOffset.x, textOffset.y, box.size.x, textRow.start, textRow.end);
  476. }
  477. }
  478. nvgResetScissor(args.vg);
  479. }
  480. };
  481. struct MidiDriverItem : ui::MenuItem {
  482. midi::Port* port;
  483. int driverId;
  484. void onAction(const event::Action& e) override {
  485. port->setDriverId(driverId);
  486. }
  487. };
  488. struct MidiDriverChoice : LedDisplayCenterChoiceEx {
  489. midi::Port* port;
  490. void onAction(const event::Action& e) override {
  491. if (!port)
  492. return;
  493. createContextMenu();
  494. }
  495. virtual ui::Menu* createContextMenu() {
  496. ui::Menu* menu = createMenu();
  497. menu->addChild(createMenuLabel("MIDI driver"));
  498. for (int driverId : midi::getDriverIds()) {
  499. MidiDriverItem* item = new MidiDriverItem;
  500. item->port = port;
  501. item->driverId = driverId;
  502. item->text = midi::getDriver(driverId)->getName();
  503. item->rightText = CHECKMARK(item->driverId == port->driverId);
  504. menu->addChild(item);
  505. }
  506. return menu;
  507. }
  508. void step() override {
  509. text = port ? port->getDriver()->getName() : "";
  510. if (text.empty()) {
  511. text = "(No driver)";
  512. color.a = 0.5f;
  513. }
  514. else {
  515. color.a = 1.f;
  516. }
  517. }
  518. };
  519. struct MidiDeviceItem : ui::MenuItem {
  520. midi::Port* outPort, *inPort;
  521. int deviceId;
  522. void onAction(const event::Action& e) override {
  523. outPort->setDeviceId(deviceId);
  524. inPort->setDeviceId(deviceId);
  525. }
  526. };
  527. struct MidiDeviceChoice : LedDisplayCenterChoiceEx {
  528. midi::Port* outPort, *inPort;
  529. void onAction(const event::Action& e) override {
  530. if (!outPort || !inPort)
  531. return;
  532. createContextMenu();
  533. }
  534. virtual ui::Menu* createContextMenu() {
  535. ui::Menu* menu = createMenu();
  536. menu->addChild(createMenuLabel("MIDI device"));
  537. {
  538. MidiDeviceItem* item = new MidiDeviceItem;
  539. item->outPort = outPort;
  540. item->inPort = inPort;
  541. item->deviceId = -1;
  542. item->text = "(No device)";
  543. item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
  544. menu->addChild(item);
  545. }
  546. for (int deviceId : outPort->getDeviceIds()) {
  547. MidiDeviceItem* item = new MidiDeviceItem;
  548. item->outPort = outPort;
  549. item->inPort = inPort;
  550. item->deviceId = deviceId;
  551. item->text = outPort->getDeviceName(deviceId);
  552. item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
  553. menu->addChild(item);
  554. }
  555. return menu;
  556. }
  557. void step() override {
  558. text = outPort ? outPort->getDeviceName(outPort->deviceId) : "";
  559. if (text.empty()) {
  560. text = "(No device)";
  561. color.a = 0.5f;
  562. }
  563. else {
  564. color.a = 1.f;
  565. }
  566. }
  567. };
  568. struct MidiWidget : LedDisplay {
  569. MidiDriverChoice* driverChoice;
  570. LedDisplaySeparator* driverSeparator;
  571. MidiDeviceChoice* deviceChoice;
  572. LedDisplaySeparator* deviceSeparator;
  573. void setMidiPorts(midi::Port* outPort, midi::Port* inPort) {
  574. clearChildren();
  575. math::Vec pos;
  576. MidiDriverChoice* driverChoice = createWidget<MidiDriverChoice>(pos);
  577. driverChoice->box.size = Vec(box.size.x, 20.f);
  578. //driverChoice->textOffset = Vec(6.f, 14.7f);
  579. driverChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
  580. driverChoice->port = outPort;
  581. addChild(driverChoice);
  582. pos = driverChoice->box.getBottomLeft();
  583. this->driverChoice = driverChoice;
  584. this->driverSeparator = createWidget<LedDisplaySeparator>(pos);
  585. this->driverSeparator->box.size.x = box.size.x;
  586. addChild(this->driverSeparator);
  587. MidiDeviceChoice* deviceChoice = createWidget<MidiDeviceChoice>(pos);
  588. deviceChoice->box.size = Vec(box.size.x, 21.f);
  589. //deviceChoice->textOffset = Vec(6.f, 14.7f);
  590. deviceChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
  591. deviceChoice->outPort = outPort;
  592. deviceChoice->inPort = inPort;
  593. addChild(deviceChoice);
  594. pos = deviceChoice->box.getBottomLeft();
  595. this->deviceChoice = deviceChoice;
  596. }
  597. };
  598. MidiThingWidget(MidiThing* module) {
  599. setModule(module);
  600. setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/MidiThing.svg")));
  601. addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
  602. addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  603. MidiWidget* midiInputWidget = createWidget<MidiWidget>(Vec(1.5f, 36.4f)); //mm2px(Vec(0.5f, 10.f)));
  604. midiInputWidget->box.size = mm2px(Vec(5.08 * 6 - 1, 13.5f));
  605. if (module) {
  606. midiInputWidget->setMidiPorts(&module->midiOut, &module->inputQueue);
  607. }
  608. else {
  609. midiInputWidget->setMidiPorts(nullptr, nullptr);
  610. }
  611. addChild(midiInputWidget);
  612. addParam(createParamCentered<BefacoButton>(mm2px(Vec(21.12, 57.32)), module, MidiThing::REFRESH_PARAM));
  613. const float xStartLed = 0.2 + 0.628;
  614. const float yStartLed = 28.019;
  615. for (int row = 0; row < 4; row++) {
  616. for (int col = 0; col < 3; col++) {
  617. LEDDisplay* display = createWidget<LEDDisplay>(mm2px(Vec(xStartLed + 9.751 * col, yStartLed + 5.796 * row)));
  618. display->module = module;
  619. display->row = row;
  620. display->col = col;
  621. addChild(display);
  622. auto input = createInputCentered<MidiThingPort>(mm2px(Vec(5.08 + 10 * col, 69.77 + 14.225 * row)), module, MidiThing::A1_INPUT + 3 * row + col);
  623. input->row = row;
  624. input->col = col;
  625. input->module = module;
  626. addInput(input);
  627. }
  628. }
  629. }
  630. void appendContextMenu(Menu* menu) override {
  631. MidiThing* module = dynamic_cast<MidiThing*>(this->module);
  632. assert(module);
  633. menu->addChild(new MenuSeparator());
  634. menu->addChild(createSubmenuItem("Select MIDI Device", "",
  635. [ = ](Menu * menu) {
  636. for (auto driverId : rack::midi::getDriverIds()) {
  637. midi::Driver* driver = midi::getDriver(driverId);
  638. const bool activeDriver = module->midiOut.getDriverId() == driverId;
  639. menu->addChild(createSubmenuItem(driver->getName(), CHECKMARK(activeDriver),
  640. [ = ](Menu * menu) {
  641. for (auto deviceId : driver->getOutputDeviceIds()) {
  642. const bool activeDevice = activeDriver && module->midiOut.getDeviceId() == deviceId;
  643. menu->addChild(createMenuItem(driver->getOutputDeviceName(deviceId),
  644. CHECKMARK(activeDevice),
  645. [ = ]() {
  646. module->midiOut.setDriverId(driverId);
  647. module->midiOut.setDeviceId(deviceId);
  648. module->inputQueue.setDriverId(driverId);
  649. module->inputQueue.setDeviceId(deviceId);
  650. module->inputQueue.setChannel(0); // TODO update
  651. module->doSync();
  652. // DEBUG("Updating Output MIDI settings - driver: %s, device: %s",
  653. // driver->getName().c_str(), driver->getOutputDeviceName(deviceId).c_str());
  654. }));
  655. }
  656. }));
  657. }
  658. }));
  659. menu->addChild(createIndexPtrSubmenuItem("All channels MIDI update rate",
  660. module->updateRateNames,
  661. &module->updateRateIdx));
  662. float updateRate = module->updateRates[module->updateRateIdx] / module->numActiveChannels;
  663. menu->addChild(createMenuLabel(string::f("Per-channel MIDI update rate: %.3g Hz", updateRate)));
  664. }
  665. };
  666. Model* modelMidiThing = createModel<MidiThing, MidiThingWidget>("MidiThingV2");