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.

656 lines
17KB

  1. #include <algorithm>
  2. #include "plugin.hpp"
  3. namespace rack {
  4. namespace core {
  5. struct MIDI_CV : Module {
  6. enum ParamIds {
  7. NUM_PARAMS
  8. };
  9. enum InputIds {
  10. NUM_INPUTS
  11. };
  12. enum OutputIds {
  13. CV_OUTPUT,
  14. GATE_OUTPUT,
  15. VELOCITY_OUTPUT,
  16. AFTERTOUCH_OUTPUT,
  17. PITCH_OUTPUT,
  18. MOD_OUTPUT,
  19. RETRIGGER_OUTPUT,
  20. CLOCK_OUTPUT,
  21. CLOCK_DIV_OUTPUT,
  22. START_OUTPUT,
  23. STOP_OUTPUT,
  24. CONTINUE_OUTPUT,
  25. NUM_OUTPUTS
  26. };
  27. enum LightIds {
  28. NUM_LIGHTS
  29. };
  30. midi::InputQueue midiInput;
  31. bool smooth;
  32. int channels;
  33. enum PolyMode {
  34. ROTATE_MODE,
  35. REUSE_MODE,
  36. RESET_MODE,
  37. MPE_MODE,
  38. NUM_POLY_MODES
  39. };
  40. PolyMode polyMode;
  41. uint32_t clock = 0;
  42. int clockDivision;
  43. bool pedal;
  44. // Indexed by channel
  45. uint8_t notes[16];
  46. bool gates[16];
  47. uint8_t velocities[16];
  48. uint8_t aftertouches[16];
  49. std::vector<uint8_t> heldNotes;
  50. int rotateIndex;
  51. // 16 channels for MPE. When MPE is disabled, only the first channel is used.
  52. uint16_t pitches[16];
  53. uint8_t mods[16];
  54. dsp::ExponentialFilter pitchFilters[16];
  55. dsp::ExponentialFilter modFilters[16];
  56. dsp::PulseGenerator clockPulse;
  57. dsp::PulseGenerator clockDividerPulse;
  58. dsp::PulseGenerator retriggerPulses[16];
  59. dsp::PulseGenerator startPulse;
  60. dsp::PulseGenerator stopPulse;
  61. dsp::PulseGenerator continuePulse;
  62. MIDI_CV() {
  63. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  64. configOutput(CV_OUTPUT, "V/oct");
  65. configOutput(GATE_OUTPUT, "Gate");
  66. configOutput(VELOCITY_OUTPUT, "Velocity");
  67. configOutput(AFTERTOUCH_OUTPUT, "Aftertouch");
  68. configOutput(PITCH_OUTPUT, "Pitch wheel");
  69. configOutput(MOD_OUTPUT, "Mod wheel");
  70. configOutput(RETRIGGER_OUTPUT, "Retrigger");
  71. configOutput(CLOCK_OUTPUT, "Clock");
  72. configOutput(CLOCK_DIV_OUTPUT, "Clock divider");
  73. configOutput(START_OUTPUT, "Start");
  74. configOutput(STOP_OUTPUT, "Stop");
  75. configOutput(CONTINUE_OUTPUT, "Continue");
  76. heldNotes.reserve(128);
  77. for (int c = 0; c < 16; c++) {
  78. pitchFilters[c].setTau(1 / 30.f);
  79. modFilters[c].setTau(1 / 30.f);
  80. }
  81. onReset();
  82. }
  83. void onReset() override {
  84. smooth = true;
  85. channels = 1;
  86. polyMode = ROTATE_MODE;
  87. clockDivision = 24;
  88. panic();
  89. midiInput.reset();
  90. }
  91. /** Resets performance state */
  92. void panic() {
  93. for (int c = 0; c < 16; c++) {
  94. notes[c] = 60;
  95. gates[c] = false;
  96. velocities[c] = 0;
  97. aftertouches[c] = 0;
  98. pitches[c] = 8192;
  99. mods[c] = 0;
  100. pitchFilters[c].reset();
  101. modFilters[c].reset();
  102. }
  103. pedal = false;
  104. rotateIndex = -1;
  105. heldNotes.clear();
  106. }
  107. void process(const ProcessArgs& args) override {
  108. while (!midiInput.queue.empty()) {
  109. const midi::Message& msg = midiInput.queue.front();
  110. // Don't process MIDI message until we've reached its frame.
  111. if (msg.frame > args.frame)
  112. break;
  113. processMessage(msg);
  114. midiInput.queue.pop();
  115. }
  116. outputs[CV_OUTPUT].setChannels(channels);
  117. outputs[GATE_OUTPUT].setChannels(channels);
  118. outputs[VELOCITY_OUTPUT].setChannels(channels);
  119. outputs[AFTERTOUCH_OUTPUT].setChannels(channels);
  120. outputs[RETRIGGER_OUTPUT].setChannels(channels);
  121. for (int c = 0; c < channels; c++) {
  122. outputs[CV_OUTPUT].setVoltage((notes[c] - 60.f) / 12.f, c);
  123. outputs[GATE_OUTPUT].setVoltage(gates[c] ? 10.f : 0.f, c);
  124. outputs[VELOCITY_OUTPUT].setVoltage(rescale(velocities[c], 0, 127, 0.f, 10.f), c);
  125. outputs[AFTERTOUCH_OUTPUT].setVoltage(rescale(aftertouches[c], 0, 127, 0.f, 10.f), c);
  126. outputs[RETRIGGER_OUTPUT].setVoltage(retriggerPulses[c].process(args.sampleTime) ? 10.f : 0.f, c);
  127. }
  128. // Set pitch and mod wheel
  129. auto updatePitch = [&](int c) {
  130. float pitch = ((int) pitches[c] - 8192) / 8191.f;
  131. pitch = clamp(pitch, -1.f, 1.f);
  132. if (smooth)
  133. pitch = pitchFilters[c].process(args.sampleTime, pitch);
  134. else
  135. pitchFilters[c].out = pitch;
  136. outputs[PITCH_OUTPUT].setVoltage(pitchFilters[c].out * 5.f);
  137. };
  138. auto updateMod = [&](int c) {
  139. float mod = mods[c] / 127.f;
  140. mod = clamp(mod, 0.f, 1.f);
  141. if (smooth)
  142. modFilters[c].process(args.sampleTime, mod);
  143. else
  144. modFilters[c].out = mod;
  145. outputs[MOD_OUTPUT].setVoltage(modFilters[c].out * 10.f);
  146. };
  147. if (polyMode == MPE_MODE) {
  148. for (int c = 0; c < channels; c++) {
  149. updatePitch(c);
  150. outputs[PITCH_OUTPUT].setChannels(1);
  151. updateMod(c);
  152. outputs[MOD_OUTPUT].setChannels(1);
  153. }
  154. }
  155. else {
  156. updatePitch(0);
  157. outputs[PITCH_OUTPUT].setChannels(1);
  158. updateMod(0);
  159. outputs[MOD_OUTPUT].setChannels(1);
  160. }
  161. outputs[CLOCK_OUTPUT].setVoltage(clockPulse.process(args.sampleTime) ? 10.f : 0.f);
  162. outputs[CLOCK_DIV_OUTPUT].setVoltage(clockDividerPulse.process(args.sampleTime) ? 10.f : 0.f);
  163. outputs[START_OUTPUT].setVoltage(startPulse.process(args.sampleTime) ? 10.f : 0.f);
  164. outputs[STOP_OUTPUT].setVoltage(stopPulse.process(args.sampleTime) ? 10.f : 0.f);
  165. outputs[CONTINUE_OUTPUT].setVoltage(continuePulse.process(args.sampleTime) ? 10.f : 0.f);
  166. }
  167. void processMessage(const midi::Message& msg) {
  168. // DEBUG("MIDI: %ld %s", msg.frame, msg.toString().c_str());
  169. switch (msg.getStatus()) {
  170. // note off
  171. case 0x8: {
  172. releaseNote(msg.getNote());
  173. } break;
  174. // note on
  175. case 0x9: {
  176. if (msg.getValue() > 0) {
  177. int c = msg.getChannel();
  178. pressNote(msg.getNote(), &c);
  179. velocities[c] = msg.getValue();
  180. }
  181. else {
  182. // For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released.
  183. releaseNote(msg.getNote());
  184. }
  185. } break;
  186. // key pressure
  187. case 0xa: {
  188. // Set the aftertouches with the same note
  189. // TODO Should we handle the MPE case differently?
  190. for (int c = 0; c < 16; c++) {
  191. if (notes[c] == msg.getNote())
  192. aftertouches[c] = msg.getValue();
  193. }
  194. } break;
  195. // cc
  196. case 0xb: {
  197. processCC(msg);
  198. } break;
  199. // channel pressure
  200. case 0xd: {
  201. if (polyMode == MPE_MODE) {
  202. // Set the channel aftertouch
  203. aftertouches[msg.getChannel()] = msg.getNote();
  204. }
  205. else {
  206. // Set all aftertouches
  207. for (int c = 0; c < 16; c++) {
  208. aftertouches[c] = msg.getNote();
  209. }
  210. }
  211. } break;
  212. // pitch wheel
  213. case 0xe: {
  214. int c = (polyMode == MPE_MODE) ? msg.getChannel() : 0;
  215. pitches[c] = ((uint16_t) msg.getValue() << 7) | msg.getNote();
  216. } break;
  217. case 0xf: {
  218. processSystem(msg);
  219. } break;
  220. default: break;
  221. }
  222. }
  223. void processCC(const midi::Message &msg) {
  224. switch (msg.getNote()) {
  225. // mod
  226. case 0x01: {
  227. int c = (polyMode == MPE_MODE) ? msg.getChannel() : 0;
  228. mods[c] = msg.getValue();
  229. } break;
  230. // sustain
  231. case 0x40: {
  232. if (msg.getValue() >= 64)
  233. pressPedal();
  234. else
  235. releasePedal();
  236. } break;
  237. // all notes off (panic)
  238. case 0x7b: {
  239. if (msg.getValue() == 0) {
  240. panic();
  241. }
  242. } break;
  243. default: break;
  244. }
  245. }
  246. void processSystem(const midi::Message &msg) {
  247. switch (msg.getChannel()) {
  248. // Timing
  249. case 0x8: {
  250. clockPulse.trigger(1e-3);
  251. if (clock % clockDivision == 0) {
  252. clockDividerPulse.trigger(1e-3);
  253. }
  254. clock++;
  255. } break;
  256. // Start
  257. case 0xa: {
  258. startPulse.trigger(1e-3);
  259. clock = 0;
  260. } break;
  261. // Continue
  262. case 0xb: {
  263. continuePulse.trigger(1e-3);
  264. } break;
  265. // Stop
  266. case 0xc: {
  267. stopPulse.trigger(1e-3);
  268. clock = 0;
  269. } break;
  270. default: break;
  271. }
  272. }
  273. int assignChannel(uint8_t note) {
  274. if (channels == 1)
  275. return 0;
  276. switch (polyMode) {
  277. case REUSE_MODE: {
  278. // Find channel with the same note
  279. for (int c = 0; c < channels; c++) {
  280. if (notes[c] == note)
  281. return c;
  282. }
  283. } // fallthrough
  284. case ROTATE_MODE: {
  285. // Find next available channel
  286. for (int i = 0; i < channels; i++) {
  287. rotateIndex++;
  288. if (rotateIndex >= channels)
  289. rotateIndex = 0;
  290. if (!gates[rotateIndex])
  291. return rotateIndex;
  292. }
  293. // No notes are available. Advance rotateIndex once more.
  294. rotateIndex++;
  295. if (rotateIndex >= channels)
  296. rotateIndex = 0;
  297. return rotateIndex;
  298. } break;
  299. case RESET_MODE: {
  300. for (int c = 0; c < channels; c++) {
  301. if (!gates[c])
  302. return c;
  303. }
  304. return channels - 1;
  305. } break;
  306. case MPE_MODE: {
  307. // This case is handled by querying the MIDI message channel.
  308. return 0;
  309. } break;
  310. default: return 0;
  311. }
  312. }
  313. void pressNote(uint8_t note, int* channel) {
  314. // Remove existing similar note
  315. auto it = std::find(heldNotes.begin(), heldNotes.end(), note);
  316. if (it != heldNotes.end())
  317. heldNotes.erase(it);
  318. // Push note
  319. heldNotes.push_back(note);
  320. // Determine actual channel
  321. if (polyMode == MPE_MODE) {
  322. // Channel is already decided for us
  323. }
  324. else {
  325. *channel = assignChannel(note);
  326. }
  327. // Set note
  328. notes[*channel] = note;
  329. gates[*channel] = true;
  330. retriggerPulses[*channel].trigger(1e-3);
  331. }
  332. void releaseNote(uint8_t note) {
  333. // Remove the note
  334. auto it = std::find(heldNotes.begin(), heldNotes.end(), note);
  335. if (it != heldNotes.end())
  336. heldNotes.erase(it);
  337. // Hold note if pedal is pressed
  338. if (pedal)
  339. return;
  340. // Turn off gate of all channels with note
  341. for (int c = 0; c < channels; c++) {
  342. if (notes[c] == note) {
  343. gates[c] = false;
  344. }
  345. }
  346. // Set last note if monophonic
  347. if (channels == 1) {
  348. if (note == notes[0] && !heldNotes.empty()) {
  349. uint8_t lastNote = heldNotes.back();
  350. notes[0] = lastNote;
  351. gates[0] = true;
  352. return;
  353. }
  354. }
  355. }
  356. void pressPedal() {
  357. if (pedal)
  358. return;
  359. pedal = true;
  360. }
  361. void releasePedal() {
  362. if (!pedal)
  363. return;
  364. pedal = false;
  365. // Set last note if monophonic
  366. if (channels == 1) {
  367. if (!heldNotes.empty()) {
  368. uint8_t lastNote = heldNotes.back();
  369. notes[0] = lastNote;
  370. }
  371. }
  372. // Clear notes that are not held if polyphonic
  373. else {
  374. for (int c = 0; c < channels; c++) {
  375. if (!gates[c])
  376. continue;
  377. gates[c] = false;
  378. for (uint8_t note : heldNotes) {
  379. if (notes[c] == note) {
  380. gates[c] = true;
  381. break;
  382. }
  383. }
  384. }
  385. }
  386. }
  387. void setChannels(int channels) {
  388. if (channels == this->channels)
  389. return;
  390. this->channels = channels;
  391. panic();
  392. }
  393. void setPolyMode(PolyMode polyMode) {
  394. if (polyMode == this->polyMode)
  395. return;
  396. this->polyMode = polyMode;
  397. panic();
  398. }
  399. json_t* dataToJson() override {
  400. json_t* rootJ = json_object();
  401. json_object_set_new(rootJ, "smooth", json_boolean(smooth));
  402. json_object_set_new(rootJ, "channels", json_integer(channels));
  403. json_object_set_new(rootJ, "polyMode", json_integer(polyMode));
  404. json_object_set_new(rootJ, "clockDivision", json_integer(clockDivision));
  405. // Saving/restoring pitch and mod doesn't make much sense for MPE.
  406. if (polyMode != MPE_MODE) {
  407. json_object_set_new(rootJ, "lastPitch", json_integer(pitches[0]));
  408. json_object_set_new(rootJ, "lastMod", json_integer(mods[0]));
  409. }
  410. json_object_set_new(rootJ, "midi", midiInput.toJson());
  411. return rootJ;
  412. }
  413. void dataFromJson(json_t* rootJ) override {
  414. json_t* smoothJ = json_object_get(rootJ, "smooth");
  415. if (smoothJ)
  416. smooth = json_boolean_value(smoothJ);
  417. json_t* channelsJ = json_object_get(rootJ, "channels");
  418. if (channelsJ)
  419. setChannels(json_integer_value(channelsJ));
  420. json_t* polyModeJ = json_object_get(rootJ, "polyMode");
  421. if (polyModeJ)
  422. polyMode = (PolyMode) json_integer_value(polyModeJ);
  423. json_t* clockDivisionJ = json_object_get(rootJ, "clockDivision");
  424. if (clockDivisionJ)
  425. clockDivision = json_integer_value(clockDivisionJ);
  426. json_t* lastPitchJ = json_object_get(rootJ, "lastPitch");
  427. if (lastPitchJ)
  428. pitches[0] = json_integer_value(lastPitchJ);
  429. json_t* lastModJ = json_object_get(rootJ, "lastMod");
  430. if (lastModJ)
  431. mods[0] = json_integer_value(lastModJ);
  432. json_t* midiJ = json_object_get(rootJ, "midi");
  433. if (midiJ)
  434. midiInput.fromJson(midiJ);
  435. }
  436. };
  437. struct MIDI_CVWidget : ModuleWidget {
  438. MIDI_CVWidget(MIDI_CV* module) {
  439. setModule(module);
  440. setPanel(APP->window->loadSvg(asset::system("res/Core/MIDI-CV.svg")));
  441. addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
  442. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
  443. addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  444. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  445. addOutput(createOutput<PJ301MPort>(mm2px(Vec(4.61505, 60.1445)), module, MIDI_CV::CV_OUTPUT));
  446. addOutput(createOutput<PJ301MPort>(mm2px(Vec(16.214, 60.1445)), module, MIDI_CV::GATE_OUTPUT));
  447. addOutput(createOutput<PJ301MPort>(mm2px(Vec(27.8143, 60.1445)), module, MIDI_CV::VELOCITY_OUTPUT));
  448. addOutput(createOutput<PJ301MPort>(mm2px(Vec(4.61505, 76.1449)), module, MIDI_CV::AFTERTOUCH_OUTPUT));
  449. addOutput(createOutput<PJ301MPort>(mm2px(Vec(16.214, 76.1449)), module, MIDI_CV::PITCH_OUTPUT));
  450. addOutput(createOutput<PJ301MPort>(mm2px(Vec(27.8143, 76.1449)), module, MIDI_CV::MOD_OUTPUT));
  451. addOutput(createOutput<PJ301MPort>(mm2px(Vec(4.61505, 92.1439)), module, MIDI_CV::CLOCK_OUTPUT));
  452. addOutput(createOutput<PJ301MPort>(mm2px(Vec(16.214, 92.1439)), module, MIDI_CV::CLOCK_DIV_OUTPUT));
  453. addOutput(createOutput<PJ301MPort>(mm2px(Vec(27.8143, 92.1439)), module, MIDI_CV::RETRIGGER_OUTPUT));
  454. addOutput(createOutput<PJ301MPort>(mm2px(Vec(4.61505, 108.144)), module, MIDI_CV::START_OUTPUT));
  455. addOutput(createOutput<PJ301MPort>(mm2px(Vec(16.214, 108.144)), module, MIDI_CV::STOP_OUTPUT));
  456. addOutput(createOutput<PJ301MPort>(mm2px(Vec(27.8143, 108.144)), module, MIDI_CV::CONTINUE_OUTPUT));
  457. MidiWidget* midiWidget = createWidget<MidiWidget>(mm2px(Vec(3.41891, 14.8373)));
  458. midiWidget->box.size = mm2px(Vec(33.840, 28));
  459. midiWidget->setMidiPort(module ? &module->midiInput : NULL);
  460. addChild(midiWidget);
  461. }
  462. void appendContextMenu(Menu* menu) override {
  463. MIDI_CV* module = dynamic_cast<MIDI_CV*>(this->module);
  464. menu->addChild(new MenuSeparator);
  465. struct SmoothItem : MenuItem {
  466. MIDI_CV* module;
  467. void onAction(const ActionEvent& e) override {
  468. module->smooth ^= true;
  469. }
  470. };
  471. SmoothItem* smoothItem = new SmoothItem;
  472. smoothItem->text = "Smooth pitch/mod wheel";
  473. smoothItem->rightText = CHECKMARK(module->smooth);
  474. smoothItem->module = module;
  475. menu->addChild(smoothItem);
  476. struct ClockDivisionValueItem : MenuItem {
  477. MIDI_CV* module;
  478. int clockDivision;
  479. void onAction(const ActionEvent& e) override {
  480. module->clockDivision = clockDivision;
  481. }
  482. };
  483. struct ClockDivisionItem : MenuItem {
  484. MIDI_CV* module;
  485. Menu* createChildMenu() override {
  486. Menu* menu = new Menu;
  487. std::vector<int> divisions = {24 * 4, 24 * 2, 24, 24 / 2, 24 / 4, 24 / 8, 2, 1};
  488. std::vector<std::string> divisionNames = {"Whole", "Half", "Quarter", "8th", "16th", "32nd", "12 PPQN", "24 PPQN"};
  489. for (size_t i = 0; i < divisions.size(); i++) {
  490. ClockDivisionValueItem* item = new ClockDivisionValueItem;
  491. item->text = divisionNames[i];
  492. item->rightText = CHECKMARK(module->clockDivision == divisions[i]);
  493. item->module = module;
  494. item->clockDivision = divisions[i];
  495. menu->addChild(item);
  496. }
  497. return menu;
  498. }
  499. };
  500. ClockDivisionItem* clockDivisionItem = new ClockDivisionItem;
  501. clockDivisionItem->text = "CLK/N divider";
  502. clockDivisionItem->rightText = RIGHT_ARROW;
  503. clockDivisionItem->module = module;
  504. menu->addChild(clockDivisionItem);
  505. struct ChannelValueItem : MenuItem {
  506. MIDI_CV* module;
  507. int channels;
  508. void onAction(const ActionEvent& e) override {
  509. module->setChannels(channels);
  510. }
  511. };
  512. struct ChannelItem : MenuItem {
  513. MIDI_CV* module;
  514. Menu* createChildMenu() override {
  515. Menu* menu = new Menu;
  516. for (int channels = 1; channels <= 16; channels++) {
  517. ChannelValueItem* item = new ChannelValueItem;
  518. if (channels == 1)
  519. item->text = "Monophonic";
  520. else
  521. item->text = string::f("%d", channels);
  522. item->rightText = CHECKMARK(module->channels == channels);
  523. item->module = module;
  524. item->channels = channels;
  525. menu->addChild(item);
  526. }
  527. return menu;
  528. }
  529. };
  530. ChannelItem* channelItem = new ChannelItem;
  531. channelItem->text = "Polyphony channels";
  532. channelItem->rightText = string::f("%d", module->channels) + " " + RIGHT_ARROW;
  533. channelItem->module = module;
  534. menu->addChild(channelItem);
  535. struct PolyModeValueItem : MenuItem {
  536. MIDI_CV* module;
  537. MIDI_CV::PolyMode polyMode;
  538. void onAction(const ActionEvent& e) override {
  539. module->setPolyMode(polyMode);
  540. }
  541. };
  542. struct PolyModeItem : MenuItem {
  543. MIDI_CV* module;
  544. Menu* createChildMenu() override {
  545. Menu* menu = new Menu;
  546. std::vector<std::string> polyModeNames = {
  547. "Rotate",
  548. "Reuse",
  549. "Reset",
  550. "MPE",
  551. };
  552. for (int i = 0; i < MIDI_CV::NUM_POLY_MODES; i++) {
  553. MIDI_CV::PolyMode polyMode = (MIDI_CV::PolyMode) i;
  554. PolyModeValueItem* item = new PolyModeValueItem;
  555. item->text = polyModeNames[i];
  556. item->rightText = CHECKMARK(module->polyMode == polyMode);
  557. item->module = module;
  558. item->polyMode = polyMode;
  559. menu->addChild(item);
  560. }
  561. return menu;
  562. }
  563. };
  564. PolyModeItem* polyModeItem = new PolyModeItem;
  565. polyModeItem->text = "Polyphony mode";
  566. polyModeItem->rightText = RIGHT_ARROW;
  567. polyModeItem->module = module;
  568. menu->addChild(polyModeItem);
  569. struct PanicItem : MenuItem {
  570. MIDI_CV* module;
  571. void onAction(const ActionEvent& e) override {
  572. module->panic();
  573. }
  574. };
  575. PanicItem* panicItem = new PanicItem;
  576. panicItem->text = "Panic";
  577. panicItem->module = module;
  578. menu->addChild(panicItem);
  579. // Example of using appendMidiMenu()
  580. // menu->addChild(new MenuSeparator);
  581. // appendMidiMenu(menu, &module->midiInput);
  582. }
  583. };
  584. // Use legacy slug for compatibility
  585. Model* modelMIDI_CV = createModel<MIDI_CV, MIDI_CVWidget>("MIDIToCVInterface");
  586. } // namespace core
  587. } // namespace rack