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.

640 lines
17KB

  1. #include "AH.hpp"
  2. #include "Core.hpp"
  3. #include "UI.hpp"
  4. #include "dsp/digital.hpp"
  5. #include <iostream>
  6. namespace rack_plugin_AmalgamatedHarmonics {
  7. struct BombeChord {
  8. int rootNote;
  9. int quality;
  10. int chord;
  11. int modeDegree;
  12. int inversion;
  13. float outVolts[6];
  14. BombeChord() : rootNote(0), quality(0), chord(0), modeDegree(0), inversion(0) {
  15. for (int j = 0; j < 6; j++) {
  16. outVolts[j] = 0.0f;
  17. }
  18. }
  19. };
  20. struct Bombe : AHModule {
  21. const static int NUM_PITCHES = 6;
  22. const static int N_NOTES = 12;
  23. const static int N_DEGREES = 7;
  24. const static int N_QUALITIES = 6;
  25. const static int QMAP_SIZE = 20;
  26. enum ParamIds {
  27. KEY_PARAM,
  28. MODE_PARAM,
  29. LENGTH_PARAM,
  30. X_PARAM,
  31. Y_PARAM,
  32. NUM_PARAMS
  33. };
  34. enum InputIds {
  35. CLOCK_INPUT,
  36. KEY_INPUT,
  37. MODE_INPUT,
  38. FREEZE_INPUT,
  39. Y_INPUT,
  40. NUM_INPUTS
  41. };
  42. enum OutputIds {
  43. ENUMS(PITCH_OUTPUT,6),
  44. NUM_OUTPUTS
  45. };
  46. enum LightIds {
  47. ENUMS(LOCK_LIGHT,2),
  48. NUM_LIGHTS
  49. };
  50. Bombe() : AHModule(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
  51. for(int i = 0; i < BUFFERSIZE; i++) {
  52. buffer[i].chord = 1;
  53. int *chordArray = CoreUtil().ChordTable[buffer[i].chord].root;
  54. for (int j = 0; j < 6; j++) {
  55. buffer[i].outVolts[j] = CoreUtil().getVoltsFromPitch(chordArray[j], buffer[i].rootNote);
  56. }
  57. }
  58. }
  59. void step() override;
  60. void modeRandom(BombeChord lastValue, float y);
  61. void modeSimple(BombeChord lastValue, float y);
  62. void modeKey(BombeChord lastValue, float y);
  63. void modeGalaxy(BombeChord lastValue, float y);
  64. void modeComplex(BombeChord lastValue, float y);
  65. json_t *toJson() override {
  66. json_t *rootJ = json_object();
  67. // offset
  68. json_t *offsetJ = json_integer((int) offset);
  69. json_object_set_new(rootJ, "offset", offsetJ);
  70. // mode
  71. json_t *modeJ = json_integer((int) mode);
  72. json_object_set_new(rootJ, "mode", modeJ);
  73. // inversions
  74. json_t *inversionsJ = json_integer((int) allowedInversions);
  75. json_object_set_new(rootJ, "inversions", inversionsJ);
  76. return rootJ;
  77. }
  78. void fromJson(json_t *rootJ) override {
  79. // offset
  80. json_t *offsetJ = json_object_get(rootJ, "offset");
  81. if (offsetJ)
  82. offset = json_integer_value(offsetJ);
  83. // mode
  84. json_t *modeJ = json_object_get(rootJ, "mode");
  85. if (modeJ)
  86. mode = json_integer_value(modeJ);
  87. // mode
  88. json_t *inversionsJ = json_object_get(rootJ, "inversions");
  89. if (inversionsJ)
  90. allowedInversions = json_integer_value(inversionsJ);
  91. }
  92. Core core;
  93. const static int N_CHORDS = 98;
  94. int ChordMap[N_CHORDS] = {1,2,26,29,71,28,72,91,31,97,25,44,54,61,78,95,10,14,15,17,48,79,81,85,11,30,89,94,24,3,90,98,96,60,55,86,5,93,7,56,92,16,32,46,62,77,18,49,65,68,70,82,20,22,23,45,83,87,6,21,27,42,80,9,52,69,76,13,37,88,53,58,8,41,57,47,64,73,19,50,59,66,74,12,35,38,63,33,34,51,4,36,40,43,84,67,39,75};
  95. int MajorScale[7] = {0,2,4,5,7,9,11};
  96. int Quality2Chord[N_QUALITIES] = { 1, 71, 91 }; // M, m, dim
  97. int QualityMap[3][QMAP_SIZE] = {
  98. {01,01,01,01,01,01,01,01,01,01,25,25,25,25,25,25,25,25,31,31},
  99. {71,71,71,71,71,71,71,71,71,71,78,78,78,78,78,78,78,78,31,31},
  100. {91,91,91,91,91,91,91,91,91,91,94,94,94,94,94,94,94,94,94,94}
  101. };
  102. int InversionMap[3][QMAP_SIZE] = {
  103. {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  104. {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1},
  105. {0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,2,2},
  106. };
  107. int poll = 50000;
  108. SchmittTrigger clockTrigger;
  109. int currRoot = 1;
  110. int currMode = 1;
  111. int currInversion = 0;
  112. int length = 16;
  113. int offset = 12; // 0 = random, 12 = lower octave, 24 = repeat, 36 = upper octave
  114. int mode = 1; // 0 = random chord, 1 = chord in key, 2 = chord in mode
  115. int allowedInversions = 0; // 0 = root only, 1 = root + first, 2 = root, first, second
  116. std::string rootName = "";
  117. std::string modeName = "";
  118. const static int BUFFERSIZE = 16;
  119. BombeChord buffer[BUFFERSIZE];
  120. BombeChord displayBuffer[BUFFERSIZE];
  121. };
  122. void Bombe::modeSimple(BombeChord lastValue, float y) {
  123. // Recalculate new value of buffer[0].outVolts from lastValue
  124. int shift = (rand() % (N_DEGREES - 1)) + 1; // 1 - 6 - always new chord
  125. buffer[0].modeDegree = (lastValue.modeDegree + shift) % N_DEGREES; // FIXME, come from mode2 modeDeg == -1!
  126. int q;
  127. int n;
  128. CoreUtil().getRootFromMode(currMode,currRoot,buffer[0].modeDegree,&n,&q);
  129. buffer[0].rootNote = n;
  130. buffer[0].quality = q; // 0 = Maj, 1 = Min, 2 = Dim
  131. if (randomUniform() < y) {
  132. buffer[0].chord = QualityMap[q][rand() % QMAP_SIZE]; // Get the index into the main chord table
  133. } else {
  134. buffer[0].chord = Quality2Chord[q]; // Get the index into the main chord table
  135. }
  136. buffer[0].inversion = InversionMap[allowedInversions][rand() % QMAP_SIZE];
  137. }
  138. void Bombe::modeRandom(BombeChord lastValue, float y) {
  139. // Recalculate new value of buffer[0].outVolts from lastValue
  140. float p = randomUniform();
  141. if (p < y) {
  142. buffer[0].rootNote = rand() % 12;
  143. } else {
  144. buffer[0].rootNote = MajorScale[rand() % 7];
  145. }
  146. buffer[0].modeDegree = -1; // FIXME
  147. buffer[0].quality = -1; // FIXME
  148. int maxChord = (int)((float)N_CHORDS * y) + 1;
  149. buffer[0].chord = ChordMap[rand() % maxChord];
  150. buffer[0].inversion = InversionMap[allowedInversions][rand() % QMAP_SIZE];
  151. }
  152. void Bombe::modeKey(BombeChord lastValue, float y) {
  153. int shift = (rand() % (N_DEGREES - 1)) + 1; // 1 - 6 - always new chord
  154. buffer[0].modeDegree = (lastValue.modeDegree + shift) % N_DEGREES; // FIXME, come from mode2 modeDeg == -1!
  155. int q;
  156. int n;
  157. CoreUtil().getRootFromMode(currMode,currRoot,buffer[0].modeDegree,&n,&q);
  158. buffer[0].rootNote = n;
  159. buffer[0].quality = -1; // FIXME
  160. buffer[0].chord = (rand() % (CoreUtil().NUM_CHORDS - 1)) + 1; // Get the index into the main chord table
  161. buffer[0].inversion = InversionMap[allowedInversions][rand() % QMAP_SIZE];
  162. }
  163. void Bombe::modeGalaxy(BombeChord lastValue, float y) {
  164. float excess = y - randomUniform();
  165. if (excess < 0.0) {
  166. modeSimple(lastValue, y);
  167. } else {
  168. if (excess < 0.2) {
  169. modeKey(lastValue, y);
  170. } else {
  171. modeRandom(lastValue, y);
  172. }
  173. }
  174. }
  175. void Bombe::modeComplex(BombeChord lastValue, float y) {
  176. modeGalaxy(lastValue, y); // Default to Galaxy
  177. }
  178. void Bombe::step() {
  179. AHModule::step();
  180. // Get inputs from Rack
  181. bool clocked = clockTrigger.process(inputs[CLOCK_INPUT].value);
  182. bool locked = false;
  183. bool updated = false;
  184. if (inputs[MODE_INPUT].active) {
  185. float fMode = inputs[MODE_INPUT].value;
  186. currMode = CoreUtil().getModeFromVolts(fMode);
  187. } else {
  188. currMode = params[MODE_PARAM].value;
  189. }
  190. if (inputs[KEY_INPUT].active) {
  191. float fRoot = inputs[KEY_INPUT].value;
  192. currRoot = CoreUtil().getKeyFromVolts(fRoot);
  193. } else {
  194. currRoot = params[KEY_PARAM].value;
  195. }
  196. float x = params[X_PARAM].value;
  197. float y = clamp(params[Y_PARAM].value + inputs[Y_PARAM].value * 0.1f, 0.0, 1.0);
  198. length = params[LENGTH_PARAM].value;
  199. if ((x >= 1.0f) || (inputs[FREEZE_INPUT].value > 0.000001f)) {
  200. locked = true;
  201. }
  202. switch(mode) {
  203. case 0: // Random
  204. rootName = "";
  205. modeName = "";
  206. break;
  207. case 1: // Simple
  208. rootName = CoreUtil().noteNames[currRoot];
  209. modeName = CoreUtil().modeNames[currMode];
  210. break;
  211. case 2: // Galaxy
  212. rootName = CoreUtil().noteNames[currRoot];
  213. modeName = CoreUtil().modeNames[currMode];
  214. break;
  215. case 3: // Complex
  216. rootName = CoreUtil().noteNames[currRoot];
  217. modeName = CoreUtil().modeNames[currMode];
  218. break;
  219. default:
  220. rootName = "";
  221. modeName = "";
  222. }
  223. if (clocked) {
  224. // Grab value from last element of sub-array, which will be the new head value
  225. BombeChord lastValue = buffer[length - 1];
  226. // Shift buffer
  227. for(int i = length - 1; i > 0; i--) {
  228. buffer[i] = buffer[i-1];
  229. }
  230. // Set first element
  231. if (locked) {
  232. // Buffer is locked
  233. buffer[0] = lastValue;
  234. } else {
  235. if (randomUniform() < x) {
  236. // Buffer update skipped
  237. buffer[0] = lastValue;
  238. } else {
  239. // We are going to update this entry
  240. updated = true;
  241. switch(mode) {
  242. case 0: modeRandom(lastValue, y); break;
  243. case 1: modeSimple(lastValue, y); break;
  244. case 2: modeGalaxy(lastValue, y); break;
  245. case 3: modeComplex(lastValue, y); break;
  246. default: modeSimple(lastValue, y);
  247. }
  248. // Determine which chord corresponds to the grid position
  249. int *chordArray;
  250. switch(buffer[0].inversion) {
  251. case 0: chordArray = CoreUtil().ChordTable[buffer[0].chord].root; break;
  252. case 1: chordArray = CoreUtil().ChordTable[buffer[0].chord].first; break;
  253. case 2: chordArray = CoreUtil().ChordTable[buffer[0].chord].second; break;
  254. default: chordArray = CoreUtil().ChordTable[buffer[0].chord].root;
  255. }
  256. // Determine which notes corresponds to the chord
  257. for (int j = 0; j < NUM_PITCHES; j++) {
  258. if (chordArray[j] < 0) {
  259. int off = offset;
  260. if (offset == 0) { // if offset = 0, randomise offset per note
  261. off = (rand() % 3 + 1) * 12;
  262. }
  263. buffer[0].outVolts[j] = CoreUtil().getVoltsFromPitch(chordArray[j] + off, buffer[0].rootNote);
  264. } else {
  265. buffer[0].outVolts[j] = CoreUtil().getVoltsFromPitch(chordArray[j], buffer[0].rootNote);
  266. }
  267. }
  268. }
  269. }
  270. for(int i = BUFFERSIZE - 1; i > 0; i--) {
  271. displayBuffer[i] = displayBuffer[i-1];
  272. }
  273. displayBuffer[0] = buffer[0];
  274. // for(int i = 0; i < BUFFERSIZE; i++) {
  275. // std::cout << buffer[i].rootNote;
  276. // }
  277. // std::cout << std::endl;
  278. // for(int i = 0; i < BUFFERSIZE; i++) {
  279. // std::cout << displayBuffer[i].rootNote;
  280. // }
  281. // std::cout << std::endl << std::endl;
  282. }
  283. if (updated) { // Green Update
  284. lights[LOCK_LIGHT].setBrightnessSmooth(1.0f);
  285. lights[LOCK_LIGHT + 1].setBrightnessSmooth(0.0f);
  286. } else if (locked) { // Yellow locked
  287. lights[LOCK_LIGHT].setBrightnessSmooth(0.0f);
  288. lights[LOCK_LIGHT + 1].setBrightnessSmooth(1.0f);
  289. } else { // No change
  290. lights[LOCK_LIGHT].setBrightnessSmooth(0.0f);
  291. lights[LOCK_LIGHT + 1].setBrightnessSmooth(0.0f);
  292. }
  293. // Set the output pitches and lights
  294. for (int i = 0; i < NUM_PITCHES; i++) {
  295. outputs[PITCH_OUTPUT + i].value = buffer[0].outVolts[i];
  296. }
  297. }
  298. struct BombeDisplay : TransparentWidget {
  299. Bombe *module;
  300. int frame = 0;
  301. std::shared_ptr<Font> font;
  302. BombeDisplay() {
  303. font = Font::load(assetPlugin(plugin, "res/EurostileBold.ttf"));
  304. }
  305. void draw(NVGcontext *vg) override {
  306. nvgFontSize(vg, 14);
  307. nvgFontFaceId(vg, font->handle);
  308. nvgFillColor(vg, nvgRGBA(255, 0, 0, 0xff));
  309. nvgTextLetterSpacing(vg, -1);
  310. char text[128];
  311. for (int i = 0; i < 7; i++) {
  312. std::string chordName = "";
  313. std::string chordExtName = "";
  314. if (module->displayBuffer[i].chord != 0) {
  315. chordName =
  316. CoreUtil().noteNames[module->displayBuffer[i].rootNote] + " " +
  317. CoreUtil().ChordTable[module->displayBuffer[i].chord].quality + " " +
  318. CoreUtil().inversionNames[module->displayBuffer[i].inversion];
  319. switch(module->mode) {
  320. case 0:
  321. chordExtName = "";
  322. break;
  323. case 1:
  324. case 2:
  325. case 3:
  326. if (module->displayBuffer[i].modeDegree != -1 && module->displayBuffer[i].quality != -1) { // FIXME
  327. int index = module->displayBuffer[i].modeDegree * 3 + module->displayBuffer[i].quality;
  328. chordExtName = CoreUtil().degreeNames[index];
  329. }
  330. break;
  331. default:
  332. chordExtName = "";
  333. }
  334. }
  335. snprintf(text, sizeof(text), "%s %s", chordName.c_str(), chordExtName.c_str());
  336. nvgText(vg, box.pos.x + 5, box.pos.y + i * 14, text, NULL);
  337. nvgFillColor(vg, nvgRGBA(255 - i * 32, 0, 0, 0xff));
  338. }
  339. nvgFillColor(vg, nvgRGBA(255, 0, 0, 0xff));
  340. nvgTextAlign(vg, NVG_ALIGN_RIGHT);
  341. snprintf(text, sizeof(text), "%s", module->rootName.c_str());
  342. nvgText(vg, box.size.x - 5, box.pos.y, text, NULL);
  343. snprintf(text, sizeof(text), "%s", module->modeName.c_str());
  344. nvgText(vg, box.size.x - 5, box.pos.y + 11, text, NULL);
  345. }
  346. };
  347. struct BombeWidget : ModuleWidget {
  348. Menu *createContextMenu() override;
  349. BombeWidget(Bombe *module) : ModuleWidget(module) {
  350. UI ui;
  351. box.size = Vec(240, 380);
  352. {
  353. SVGPanel *panel = new SVGPanel();
  354. panel->box.size = box.size;
  355. panel->setBackground(SVG::load(assetPlugin(plugin, "res/Bombe.svg")));
  356. addChild(panel);
  357. }
  358. {
  359. BombeDisplay *display = new BombeDisplay();
  360. display->module = module;
  361. display->box.pos = Vec(0, 20);
  362. display->box.size = Vec(240, 230);
  363. addChild(display);
  364. }
  365. for (int i = 0; i < 6; i++) {
  366. addOutput(Port::create<PJ301MPort>(ui.getPosition(UI::PORT, i, 5, true, false), Port::OUTPUT, module, Bombe::PITCH_OUTPUT + i));
  367. }
  368. addParam(ParamWidget::create<AHKnobSnap>(ui.getPosition(UI::KNOB, 0, 4, true, false), module, Bombe::KEY_PARAM, 0.0, 11.0, 0.0));
  369. addInput(Port::create<PJ301MPort>(ui.getPosition(UI::PORT, 1, 4, true, false), Port::INPUT, module, Bombe::KEY_INPUT));
  370. addParam(ParamWidget::create<AHKnobSnap>(ui.getPosition(UI::KNOB, 2, 4, true, false), module, Bombe::MODE_PARAM, 0.0, 6.0, 0.0));
  371. addInput(Port::create<PJ301MPort>(ui.getPosition(UI::PORT, 3, 4, true, false), Port::INPUT, module, Bombe::MODE_INPUT));
  372. addInput(Port::create<PJ301MPort>(ui.getPosition(UI::PORT, 4, 4, true, false), Port::INPUT, module, Bombe::CLOCK_INPUT));
  373. addParam(ParamWidget::create<AHKnobSnap>(ui.getPosition(UI::KNOB, 5, 4, true, false), module, Bombe::LENGTH_PARAM, 2.0, 16.0, 0.0));
  374. Vec XParamPos;
  375. XParamPos.x = 33;
  376. XParamPos.y = 160;
  377. addParam(ParamWidget::create<AHBigKnobNoSnap>(XParamPos, module, Bombe::X_PARAM, 0.0, 1.0001, 0.5));
  378. Vec XFreezePos;
  379. XFreezePos.x = XParamPos.x - 12;
  380. XFreezePos.y = XParamPos.y + 60;
  381. addInput(Port::create<PJ301MPort>(XFreezePos, Port::INPUT, module, Bombe::FREEZE_INPUT));
  382. Vec XLightPos;
  383. XLightPos.x = XParamPos.x + 63;
  384. XLightPos.y = XParamPos.y + 68;
  385. addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(XLightPos, module, Bombe::LOCK_LIGHT));
  386. Vec YParamPos;
  387. YParamPos.x = 137;
  388. YParamPos.y = 160;
  389. addParam(ParamWidget::create<AHBigKnobNoSnap>(YParamPos, module, Bombe::Y_PARAM, 0.0, 1.0001, 0.5));
  390. Vec YInputPos;
  391. YInputPos.x = YParamPos.x - 12;
  392. YInputPos.y = YParamPos.y + 60;
  393. addInput(Port::create<PJ301MPort>(YInputPos, Port::INPUT, module, Bombe::Y_INPUT));
  394. }
  395. };
  396. struct BombeOffsetItem : MenuItem {
  397. Bombe *bombe;
  398. int offset;
  399. void onAction(EventAction &e) override {
  400. bombe->offset = offset;
  401. }
  402. void step() override {
  403. rightText = (bombe->offset == offset) ? "✔" : "";
  404. }
  405. };
  406. struct BombeModeItem : MenuItem {
  407. Bombe *bombe;
  408. int mode;
  409. void onAction(EventAction &e) override {
  410. bombe->mode = mode;
  411. }
  412. void step() override {
  413. rightText = (bombe->mode == mode) ? "✔" : "";
  414. }
  415. };
  416. struct BombeInversionsItem : MenuItem {
  417. Bombe *bombe;
  418. int allowedInversions;
  419. void onAction(EventAction &e) override {
  420. bombe->allowedInversions = allowedInversions;
  421. }
  422. void step() override {
  423. rightText = (bombe->allowedInversions == allowedInversions) ? "✔" : "";
  424. }
  425. };
  426. Menu *BombeWidget::createContextMenu() {
  427. Menu *menu = ModuleWidget::createContextMenu();
  428. MenuLabel *spacerLabel = new MenuLabel();
  429. menu->addChild(spacerLabel);
  430. Bombe *bombe = dynamic_cast<Bombe*>(module);
  431. assert(bombe);
  432. MenuLabel *offsetLabel = new MenuLabel();
  433. offsetLabel->text = "Repeat Notes";
  434. menu->addChild(offsetLabel);
  435. BombeOffsetItem *offsetLowerItem = new BombeOffsetItem();
  436. offsetLowerItem->text = "Lower";
  437. offsetLowerItem->bombe = bombe;
  438. offsetLowerItem->offset = 12;
  439. menu->addChild(offsetLowerItem);
  440. BombeOffsetItem *offsetRepeatItem = new BombeOffsetItem();
  441. offsetRepeatItem->text = "Repeat";
  442. offsetRepeatItem->bombe = bombe;
  443. offsetRepeatItem->offset = 24;
  444. menu->addChild(offsetRepeatItem);
  445. BombeOffsetItem *offsetUpperItem = new BombeOffsetItem();
  446. offsetUpperItem->text = "Upper";
  447. offsetUpperItem->bombe = bombe;
  448. offsetUpperItem->offset = 36;
  449. menu->addChild(offsetUpperItem);
  450. BombeOffsetItem *offsetRandomItem = new BombeOffsetItem();
  451. offsetRandomItem->text = "Random";
  452. offsetRandomItem->bombe = bombe;
  453. offsetRandomItem->offset = 0;
  454. menu->addChild(offsetRandomItem);
  455. MenuLabel *modeLabel = new MenuLabel();
  456. modeLabel->text = "Mode";
  457. menu->addChild(modeLabel);
  458. BombeModeItem *modeRandomItem = new BombeModeItem();
  459. modeRandomItem->text = "Random";
  460. modeRandomItem->bombe = bombe;
  461. modeRandomItem->mode = 0;
  462. menu->addChild(modeRandomItem);
  463. BombeModeItem *modeSimpleItem = new BombeModeItem();
  464. modeSimpleItem->text = "Simple";
  465. modeSimpleItem->bombe = bombe;
  466. modeSimpleItem->mode = 1;
  467. menu->addChild(modeSimpleItem);
  468. BombeModeItem *modeGalaxyItem = new BombeModeItem();
  469. modeGalaxyItem->text = "Galaxy";
  470. modeGalaxyItem->bombe = bombe;
  471. modeGalaxyItem->mode = 2;
  472. menu->addChild(modeGalaxyItem);
  473. BombeModeItem *modeComplexItem = new BombeModeItem();
  474. modeComplexItem->text = "Complex";
  475. modeComplexItem->bombe = bombe;
  476. modeComplexItem->mode = 3;
  477. // menu->addChild(modeComplexItem);
  478. MenuLabel *invLabel = new MenuLabel();
  479. invLabel->text = "Allowed Chord Inversions";
  480. menu->addChild(invLabel);
  481. BombeInversionsItem *invRootItem = new BombeInversionsItem();
  482. invRootItem->text = "Root only";
  483. invRootItem->bombe = bombe;
  484. invRootItem->allowedInversions = 0;
  485. menu->addChild(invRootItem);
  486. BombeInversionsItem *invFirstItem = new BombeInversionsItem();
  487. invFirstItem->text = "Root and First";
  488. invFirstItem->bombe = bombe;
  489. invFirstItem->allowedInversions = 1;
  490. menu->addChild(invFirstItem);
  491. BombeInversionsItem *invSecondItem = new BombeInversionsItem();
  492. invSecondItem->text = "Root, First and Second";
  493. invSecondItem->bombe = bombe;
  494. invSecondItem->allowedInversions = 2;
  495. menu->addChild(invSecondItem);
  496. return menu;
  497. }
  498. } // namespace rack_plugin_AmalgamatedHarmonics
  499. using namespace rack_plugin_AmalgamatedHarmonics;
  500. RACK_PLUGIN_MODEL_INIT(AmalgamatedHarmonics, Bombe) {
  501. Model *modelBombe = Model::create<Bombe, BombeWidget>( "Amalgamated Harmonics", "Bombe", "Bombe", SEQUENCER_TAG);
  502. return modelBombe;
  503. }
  504. // ♯♭