The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
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.

1387 lines
45KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 7 technical preview.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For the technical preview this file cannot be licensed commercially.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. #include <JuceHeader.h>
  14. #include "GraphEditorPanel.h"
  15. #include "../Plugins/InternalPlugins.h"
  16. #include "MainHostWindow.h"
  17. //==============================================================================
  18. #if JUCE_IOS
  19. class AUScanner
  20. {
  21. public:
  22. AUScanner (KnownPluginList& list)
  23. : knownPluginList (list), pool (5)
  24. {
  25. knownPluginList.clearBlacklistedFiles();
  26. paths = formatToScan.getDefaultLocationsToSearch();
  27. startScan();
  28. }
  29. private:
  30. KnownPluginList& knownPluginList;
  31. AudioUnitPluginFormat formatToScan;
  32. std::unique_ptr<PluginDirectoryScanner> scanner;
  33. FileSearchPath paths;
  34. ThreadPool pool;
  35. void startScan()
  36. {
  37. auto deadMansPedalFile = getAppProperties().getUserSettings()
  38. ->getFile().getSiblingFile ("RecentlyCrashedPluginsList");
  39. scanner.reset (new PluginDirectoryScanner (knownPluginList, formatToScan, paths,
  40. true, deadMansPedalFile, true));
  41. for (int i = 5; --i >= 0;)
  42. pool.addJob (new ScanJob (*this), true);
  43. }
  44. bool doNextScan()
  45. {
  46. String pluginBeingScanned;
  47. if (scanner->scanNextFile (true, pluginBeingScanned))
  48. return true;
  49. return false;
  50. }
  51. struct ScanJob : public ThreadPoolJob
  52. {
  53. ScanJob (AUScanner& s) : ThreadPoolJob ("pluginscan"), scanner (s) {}
  54. JobStatus runJob()
  55. {
  56. while (scanner.doNextScan() && ! shouldExit())
  57. {}
  58. return jobHasFinished;
  59. }
  60. AUScanner& scanner;
  61. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob)
  62. };
  63. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUScanner)
  64. };
  65. #endif
  66. //==============================================================================
  67. struct GraphEditorPanel::PinComponent : public Component,
  68. public SettableTooltipClient
  69. {
  70. PinComponent (GraphEditorPanel& p, AudioProcessorGraph::NodeAndChannel pinToUse, bool isIn)
  71. : panel (p), graph (p.graph), pin (pinToUse), isInput (isIn)
  72. {
  73. if (auto node = graph.graph.getNodeForId (pin.nodeID))
  74. {
  75. String tip;
  76. if (pin.isMIDI())
  77. {
  78. tip = isInput ? "MIDI Input"
  79. : "MIDI Output";
  80. }
  81. else
  82. {
  83. auto& processor = *node->getProcessor();
  84. auto channel = processor.getOffsetInBusBufferForAbsoluteChannelIndex (isInput, pin.channelIndex, busIdx);
  85. if (auto* bus = processor.getBus (isInput, busIdx))
  86. tip = bus->getName() + ": " + AudioChannelSet::getAbbreviatedChannelTypeName (bus->getCurrentLayout().getTypeOfChannel (channel));
  87. else
  88. tip = (isInput ? "Main Input: "
  89. : "Main Output: ") + String (pin.channelIndex + 1);
  90. }
  91. setTooltip (tip);
  92. }
  93. setSize (16, 16);
  94. }
  95. void paint (Graphics& g) override
  96. {
  97. auto w = (float) getWidth();
  98. auto h = (float) getHeight();
  99. Path p;
  100. p.addEllipse (w * 0.25f, h * 0.25f, w * 0.5f, h * 0.5f);
  101. p.addRectangle (w * 0.4f, isInput ? (0.5f * h) : 0.0f, w * 0.2f, h * 0.5f);
  102. auto colour = (pin.isMIDI() ? Colours::red : Colours::green);
  103. g.setColour (colour.withRotatedHue ((float) busIdx / 5.0f));
  104. g.fillPath (p);
  105. }
  106. void mouseDown (const MouseEvent& e) override
  107. {
  108. AudioProcessorGraph::NodeAndChannel dummy { {}, 0 };
  109. panel.beginConnectorDrag (isInput ? dummy : pin,
  110. isInput ? pin : dummy,
  111. e);
  112. }
  113. void mouseDrag (const MouseEvent& e) override
  114. {
  115. panel.dragConnector (e);
  116. }
  117. void mouseUp (const MouseEvent& e) override
  118. {
  119. panel.endDraggingConnector (e);
  120. }
  121. GraphEditorPanel& panel;
  122. PluginGraph& graph;
  123. AudioProcessorGraph::NodeAndChannel pin;
  124. const bool isInput;
  125. int busIdx = 0;
  126. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PinComponent)
  127. };
  128. //==============================================================================
  129. struct GraphEditorPanel::PluginComponent : public Component,
  130. public Timer,
  131. private AudioProcessorParameter::Listener,
  132. private AsyncUpdater
  133. {
  134. PluginComponent (GraphEditorPanel& p, AudioProcessorGraph::NodeID id) : panel (p), graph (p.graph), pluginID (id)
  135. {
  136. shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, { 0, 1 }));
  137. setComponentEffect (&shadow);
  138. if (auto f = graph.graph.getNodeForId (pluginID))
  139. {
  140. if (auto* processor = f->getProcessor())
  141. {
  142. if (auto* bypassParam = processor->getBypassParameter())
  143. bypassParam->addListener (this);
  144. }
  145. }
  146. setSize (150, 60);
  147. }
  148. PluginComponent (const PluginComponent&) = delete;
  149. PluginComponent& operator= (const PluginComponent&) = delete;
  150. ~PluginComponent() override
  151. {
  152. if (auto f = graph.graph.getNodeForId (pluginID))
  153. {
  154. if (auto* processor = f->getProcessor())
  155. {
  156. if (auto* bypassParam = processor->getBypassParameter())
  157. bypassParam->removeListener (this);
  158. }
  159. }
  160. }
  161. void mouseDown (const MouseEvent& e) override
  162. {
  163. originalPos = localPointToGlobal (Point<int>());
  164. toFront (true);
  165. if (isOnTouchDevice())
  166. {
  167. startTimer (750);
  168. }
  169. else
  170. {
  171. if (e.mods.isPopupMenu())
  172. showPopupMenu();
  173. }
  174. }
  175. void mouseDrag (const MouseEvent& e) override
  176. {
  177. if (isOnTouchDevice() && e.getDistanceFromDragStart() > 5)
  178. stopTimer();
  179. if (! e.mods.isPopupMenu())
  180. {
  181. auto pos = originalPos + e.getOffsetFromDragStart();
  182. if (getParentComponent() != nullptr)
  183. pos = getParentComponent()->getLocalPoint (nullptr, pos);
  184. pos += getLocalBounds().getCentre();
  185. graph.setNodePosition (pluginID,
  186. { pos.x / (double) getParentWidth(),
  187. pos.y / (double) getParentHeight() });
  188. panel.updateComponents();
  189. }
  190. }
  191. void mouseUp (const MouseEvent& e) override
  192. {
  193. if (isOnTouchDevice())
  194. {
  195. stopTimer();
  196. callAfterDelay (250, []() { PopupMenu::dismissAllActiveMenus(); });
  197. }
  198. if (e.mouseWasDraggedSinceMouseDown())
  199. {
  200. graph.setChangedFlag (true);
  201. }
  202. else if (e.getNumberOfClicks() == 2)
  203. {
  204. if (auto f = graph.graph.getNodeForId (pluginID))
  205. if (auto* w = graph.getOrCreateWindowFor (f, PluginWindow::Type::normal))
  206. w->toFront (true);
  207. }
  208. }
  209. bool hitTest (int x, int y) override
  210. {
  211. for (auto* child : getChildren())
  212. if (child->getBounds().contains (x, y))
  213. return true;
  214. return x >= 3 && x < getWidth() - 6 && y >= pinSize && y < getHeight() - pinSize;
  215. }
  216. void paint (Graphics& g) override
  217. {
  218. auto boxArea = getLocalBounds().reduced (4, pinSize);
  219. bool isBypassed = false;
  220. if (auto* f = graph.graph.getNodeForId (pluginID))
  221. isBypassed = f->isBypassed();
  222. auto boxColour = findColour (TextEditor::backgroundColourId);
  223. if (isBypassed)
  224. boxColour = boxColour.brighter();
  225. g.setColour (boxColour);
  226. g.fillRect (boxArea.toFloat());
  227. g.setColour (findColour (TextEditor::textColourId));
  228. g.setFont (font);
  229. g.drawFittedText (getName(), boxArea, Justification::centred, 2);
  230. }
  231. void resized() override
  232. {
  233. if (auto f = graph.graph.getNodeForId (pluginID))
  234. {
  235. if (auto* processor = f->getProcessor())
  236. {
  237. for (auto* pin : pins)
  238. {
  239. const bool isInput = pin->isInput;
  240. auto channelIndex = pin->pin.channelIndex;
  241. int busIdx = 0;
  242. processor->getOffsetInBusBufferForAbsoluteChannelIndex (isInput, channelIndex, busIdx);
  243. const int total = isInput ? numIns : numOuts;
  244. const int index = pin->pin.isMIDI() ? (total - 1) : channelIndex;
  245. auto totalSpaces = static_cast<float> (total) + (static_cast<float> (jmax (0, processor->getBusCount (isInput) - 1)) * 0.5f);
  246. auto indexPos = static_cast<float> (index) + (static_cast<float> (busIdx) * 0.5f);
  247. pin->setBounds (proportionOfWidth ((1.0f + indexPos) / (totalSpaces + 1.0f)) - pinSize / 2,
  248. pin->isInput ? 0 : (getHeight() - pinSize),
  249. pinSize, pinSize);
  250. }
  251. }
  252. }
  253. }
  254. Point<float> getPinPos (int index, bool isInput) const
  255. {
  256. for (auto* pin : pins)
  257. if (pin->pin.channelIndex == index && isInput == pin->isInput)
  258. return getPosition().toFloat() + pin->getBounds().getCentre().toFloat();
  259. return {};
  260. }
  261. void update()
  262. {
  263. const AudioProcessorGraph::Node::Ptr f (graph.graph.getNodeForId (pluginID));
  264. jassert (f != nullptr);
  265. auto& processor = *f->getProcessor();
  266. numIns = processor.getTotalNumInputChannels();
  267. if (processor.acceptsMidi())
  268. ++numIns;
  269. numOuts = processor.getTotalNumOutputChannels();
  270. if (processor.producesMidi())
  271. ++numOuts;
  272. int w = 100;
  273. int h = 60;
  274. w = jmax (w, (jmax (numIns, numOuts) + 1) * 20);
  275. const int textWidth = font.getStringWidth (processor.getName());
  276. w = jmax (w, 16 + jmin (textWidth, 300));
  277. if (textWidth > 300)
  278. h = 100;
  279. setSize (w, h);
  280. setName (processor.getName() + formatSuffix);
  281. {
  282. auto p = graph.getNodePosition (pluginID);
  283. setCentreRelative ((float) p.x, (float) p.y);
  284. }
  285. if (numIns != numInputs || numOuts != numOutputs)
  286. {
  287. numInputs = numIns;
  288. numOutputs = numOuts;
  289. pins.clear();
  290. for (int i = 0; i < processor.getTotalNumInputChannels(); ++i)
  291. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, i }, true)));
  292. if (processor.acceptsMidi())
  293. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, AudioProcessorGraph::midiChannelIndex }, true)));
  294. for (int i = 0; i < processor.getTotalNumOutputChannels(); ++i)
  295. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, i }, false)));
  296. if (processor.producesMidi())
  297. addAndMakeVisible (pins.add (new PinComponent (panel, { pluginID, AudioProcessorGraph::midiChannelIndex }, false)));
  298. resized();
  299. }
  300. }
  301. AudioProcessor* getProcessor() const
  302. {
  303. if (auto node = graph.graph.getNodeForId (pluginID))
  304. return node->getProcessor();
  305. return {};
  306. }
  307. void showPopupMenu()
  308. {
  309. menu.reset (new PopupMenu);
  310. menu->addItem ("Delete this filter", [this] { graph.graph.removeNode (pluginID); });
  311. menu->addItem ("Disconnect all pins", [this] { graph.graph.disconnectNode (pluginID); });
  312. menu->addItem ("Toggle Bypass", [this]
  313. {
  314. if (auto* node = graph.graph.getNodeForId (pluginID))
  315. node->setBypassed (! node->isBypassed());
  316. repaint();
  317. });
  318. menu->addSeparator();
  319. if (getProcessor()->hasEditor())
  320. menu->addItem ("Show plugin GUI", [this] { showWindow (PluginWindow::Type::normal); });
  321. menu->addItem ("Show all programs", [this] { showWindow (PluginWindow::Type::programs); });
  322. menu->addItem ("Show all parameters", [this] { showWindow (PluginWindow::Type::generic); });
  323. menu->addItem ("Show debug log", [this] { showWindow (PluginWindow::Type::debug); });
  324. if (autoScaleOptionAvailable)
  325. addPluginAutoScaleOptionsSubMenu (dynamic_cast<AudioPluginInstance*> (getProcessor()), *menu);
  326. menu->addSeparator();
  327. menu->addItem ("Configure Audio I/O", [this] { showWindow (PluginWindow::Type::audioIO); });
  328. menu->addItem ("Test state save/load", [this] { testStateSaveLoad(); });
  329. #if ! JUCE_IOS && ! JUCE_ANDROID
  330. menu->addSeparator();
  331. menu->addItem ("Save plugin state", [this] { savePluginState(); });
  332. menu->addItem ("Load plugin state", [this] { loadPluginState(); });
  333. #endif
  334. menu->showMenuAsync ({});
  335. }
  336. void testStateSaveLoad()
  337. {
  338. if (auto* processor = getProcessor())
  339. {
  340. MemoryBlock state;
  341. processor->getStateInformation (state);
  342. processor->setStateInformation (state.getData(), (int) state.getSize());
  343. }
  344. }
  345. void showWindow (PluginWindow::Type type)
  346. {
  347. if (auto node = graph.graph.getNodeForId (pluginID))
  348. if (auto* w = graph.getOrCreateWindowFor (node, type))
  349. w->toFront (true);
  350. }
  351. void timerCallback() override
  352. {
  353. // this should only be called on touch devices
  354. jassert (isOnTouchDevice());
  355. stopTimer();
  356. showPopupMenu();
  357. }
  358. void parameterValueChanged (int, float) override
  359. {
  360. // Parameter changes might come from the audio thread or elsewhere, but
  361. // we can only call repaint from the message thread.
  362. triggerAsyncUpdate();
  363. }
  364. void parameterGestureChanged (int, bool) override {}
  365. void handleAsyncUpdate() override { repaint(); }
  366. void savePluginState()
  367. {
  368. fileChooser = std::make_unique<FileChooser> ("Save plugin state");
  369. const auto onChosen = [ref = SafePointer<PluginComponent> (this)] (const FileChooser& chooser)
  370. {
  371. if (ref == nullptr)
  372. return;
  373. const auto result = chooser.getResult();
  374. if (result == File())
  375. return;
  376. if (auto* node = ref->graph.graph.getNodeForId (ref->pluginID))
  377. {
  378. MemoryBlock block;
  379. node->getProcessor()->getStateInformation (block);
  380. result.replaceWithData (block.getData(), block.getSize());
  381. }
  382. };
  383. fileChooser->launchAsync (FileBrowserComponent::saveMode | FileBrowserComponent::warnAboutOverwriting, onChosen);
  384. }
  385. void loadPluginState()
  386. {
  387. fileChooser = std::make_unique<FileChooser> ("Load plugin state");
  388. const auto onChosen = [ref = SafePointer<PluginComponent> (this)] (const FileChooser& chooser)
  389. {
  390. if (ref == nullptr)
  391. return;
  392. const auto result = chooser.getResult();
  393. if (result == File())
  394. return;
  395. if (auto* node = ref->graph.graph.getNodeForId (ref->pluginID))
  396. {
  397. if (auto stream = result.createInputStream())
  398. {
  399. MemoryBlock block;
  400. stream->readIntoMemoryBlock (block);
  401. node->getProcessor()->setStateInformation (block.getData(), (int) block.getSize());
  402. }
  403. }
  404. };
  405. fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, onChosen);
  406. }
  407. GraphEditorPanel& panel;
  408. PluginGraph& graph;
  409. const AudioProcessorGraph::NodeID pluginID;
  410. OwnedArray<PinComponent> pins;
  411. int numInputs = 0, numOutputs = 0;
  412. int pinSize = 16;
  413. Point<int> originalPos;
  414. Font font { 13.0f, Font::bold };
  415. int numIns = 0, numOuts = 0;
  416. DropShadowEffect shadow;
  417. std::unique_ptr<PopupMenu> menu;
  418. std::unique_ptr<FileChooser> fileChooser;
  419. const String formatSuffix = getFormatSuffix (getProcessor());
  420. };
  421. //==============================================================================
  422. struct GraphEditorPanel::ConnectorComponent : public Component,
  423. public SettableTooltipClient
  424. {
  425. explicit ConnectorComponent (GraphEditorPanel& p)
  426. : panel (p), graph (p.graph)
  427. {
  428. setAlwaysOnTop (true);
  429. }
  430. void setInput (AudioProcessorGraph::NodeAndChannel newSource)
  431. {
  432. if (connection.source != newSource)
  433. {
  434. connection.source = newSource;
  435. update();
  436. }
  437. }
  438. void setOutput (AudioProcessorGraph::NodeAndChannel newDest)
  439. {
  440. if (connection.destination != newDest)
  441. {
  442. connection.destination = newDest;
  443. update();
  444. }
  445. }
  446. void dragStart (Point<float> pos)
  447. {
  448. lastInputPos = pos;
  449. resizeToFit();
  450. }
  451. void dragEnd (Point<float> pos)
  452. {
  453. lastOutputPos = pos;
  454. resizeToFit();
  455. }
  456. void update()
  457. {
  458. Point<float> p1, p2;
  459. getPoints (p1, p2);
  460. if (lastInputPos != p1 || lastOutputPos != p2)
  461. resizeToFit();
  462. }
  463. void resizeToFit()
  464. {
  465. Point<float> p1, p2;
  466. getPoints (p1, p2);
  467. auto newBounds = Rectangle<float> (p1, p2).expanded (4.0f).getSmallestIntegerContainer();
  468. if (newBounds != getBounds())
  469. setBounds (newBounds);
  470. else
  471. resized();
  472. repaint();
  473. }
  474. void getPoints (Point<float>& p1, Point<float>& p2) const
  475. {
  476. p1 = lastInputPos;
  477. p2 = lastOutputPos;
  478. if (auto* src = panel.getComponentForPlugin (connection.source.nodeID))
  479. p1 = src->getPinPos (connection.source.channelIndex, false);
  480. if (auto* dest = panel.getComponentForPlugin (connection.destination.nodeID))
  481. p2 = dest->getPinPos (connection.destination.channelIndex, true);
  482. }
  483. void paint (Graphics& g) override
  484. {
  485. if (connection.source.isMIDI() || connection.destination.isMIDI())
  486. g.setColour (Colours::red);
  487. else
  488. g.setColour (Colours::green);
  489. g.fillPath (linePath);
  490. }
  491. bool hitTest (int x, int y) override
  492. {
  493. auto pos = Point<int> (x, y).toFloat();
  494. if (hitPath.contains (pos))
  495. {
  496. double distanceFromStart, distanceFromEnd;
  497. getDistancesFromEnds (pos, distanceFromStart, distanceFromEnd);
  498. // avoid clicking the connector when over a pin
  499. return distanceFromStart > 7.0 && distanceFromEnd > 7.0;
  500. }
  501. return false;
  502. }
  503. void mouseDown (const MouseEvent&) override
  504. {
  505. dragging = false;
  506. }
  507. void mouseDrag (const MouseEvent& e) override
  508. {
  509. if (dragging)
  510. {
  511. panel.dragConnector (e);
  512. }
  513. else if (e.mouseWasDraggedSinceMouseDown())
  514. {
  515. dragging = true;
  516. graph.graph.removeConnection (connection);
  517. double distanceFromStart, distanceFromEnd;
  518. getDistancesFromEnds (getPosition().toFloat() + e.position, distanceFromStart, distanceFromEnd);
  519. const bool isNearerSource = (distanceFromStart < distanceFromEnd);
  520. AudioProcessorGraph::NodeAndChannel dummy { {}, 0 };
  521. panel.beginConnectorDrag (isNearerSource ? dummy : connection.source,
  522. isNearerSource ? connection.destination : dummy,
  523. e);
  524. }
  525. }
  526. void mouseUp (const MouseEvent& e) override
  527. {
  528. if (dragging)
  529. panel.endDraggingConnector (e);
  530. }
  531. void resized() override
  532. {
  533. Point<float> p1, p2;
  534. getPoints (p1, p2);
  535. lastInputPos = p1;
  536. lastOutputPos = p2;
  537. p1 -= getPosition().toFloat();
  538. p2 -= getPosition().toFloat();
  539. linePath.clear();
  540. linePath.startNewSubPath (p1);
  541. linePath.cubicTo (p1.x, p1.y + (p2.y - p1.y) * 0.33f,
  542. p2.x, p1.y + (p2.y - p1.y) * 0.66f,
  543. p2.x, p2.y);
  544. PathStrokeType wideStroke (8.0f);
  545. wideStroke.createStrokedPath (hitPath, linePath);
  546. PathStrokeType stroke (2.5f);
  547. stroke.createStrokedPath (linePath, linePath);
  548. auto arrowW = 5.0f;
  549. auto arrowL = 4.0f;
  550. Path arrow;
  551. arrow.addTriangle (-arrowL, arrowW,
  552. -arrowL, -arrowW,
  553. arrowL, 0.0f);
  554. arrow.applyTransform (AffineTransform()
  555. .rotated (MathConstants<float>::halfPi - (float) atan2 (p2.x - p1.x, p2.y - p1.y))
  556. .translated ((p1 + p2) * 0.5f));
  557. linePath.addPath (arrow);
  558. linePath.setUsingNonZeroWinding (true);
  559. }
  560. void getDistancesFromEnds (Point<float> p, double& distanceFromStart, double& distanceFromEnd) const
  561. {
  562. Point<float> p1, p2;
  563. getPoints (p1, p2);
  564. distanceFromStart = p1.getDistanceFrom (p);
  565. distanceFromEnd = p2.getDistanceFrom (p);
  566. }
  567. GraphEditorPanel& panel;
  568. PluginGraph& graph;
  569. AudioProcessorGraph::Connection connection { { {}, 0 }, { {}, 0 } };
  570. Point<float> lastInputPos, lastOutputPos;
  571. Path linePath, hitPath;
  572. bool dragging = false;
  573. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConnectorComponent)
  574. };
  575. //==============================================================================
  576. GraphEditorPanel::GraphEditorPanel (PluginGraph& g) : graph (g)
  577. {
  578. graph.addChangeListener (this);
  579. setOpaque (true);
  580. }
  581. GraphEditorPanel::~GraphEditorPanel()
  582. {
  583. graph.removeChangeListener (this);
  584. draggingConnector = nullptr;
  585. nodes.clear();
  586. connectors.clear();
  587. }
  588. void GraphEditorPanel::paint (Graphics& g)
  589. {
  590. g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
  591. }
  592. void GraphEditorPanel::mouseDown (const MouseEvent& e)
  593. {
  594. if (isOnTouchDevice())
  595. {
  596. originalTouchPos = e.position.toInt();
  597. startTimer (750);
  598. }
  599. if (e.mods.isPopupMenu())
  600. showPopupMenu (e.position.toInt());
  601. }
  602. void GraphEditorPanel::mouseUp (const MouseEvent&)
  603. {
  604. if (isOnTouchDevice())
  605. {
  606. stopTimer();
  607. callAfterDelay (250, []() { PopupMenu::dismissAllActiveMenus(); });
  608. }
  609. }
  610. void GraphEditorPanel::mouseDrag (const MouseEvent& e)
  611. {
  612. if (isOnTouchDevice() && e.getDistanceFromDragStart() > 5)
  613. stopTimer();
  614. }
  615. void GraphEditorPanel::createNewPlugin (const PluginDescription& desc, Point<int> position)
  616. {
  617. graph.addPlugin (desc, position.toDouble() / Point<double> ((double) getWidth(), (double) getHeight()));
  618. }
  619. GraphEditorPanel::PluginComponent* GraphEditorPanel::getComponentForPlugin (AudioProcessorGraph::NodeID nodeID) const
  620. {
  621. for (auto* fc : nodes)
  622. if (fc->pluginID == nodeID)
  623. return fc;
  624. return nullptr;
  625. }
  626. GraphEditorPanel::ConnectorComponent* GraphEditorPanel::getComponentForConnection (const AudioProcessorGraph::Connection& conn) const
  627. {
  628. for (auto* cc : connectors)
  629. if (cc->connection == conn)
  630. return cc;
  631. return nullptr;
  632. }
  633. GraphEditorPanel::PinComponent* GraphEditorPanel::findPinAt (Point<float> pos) const
  634. {
  635. for (auto* fc : nodes)
  636. {
  637. // NB: A Visual Studio optimiser error means we have to put this Component* in a local
  638. // variable before trying to cast it, or it gets mysteriously optimised away..
  639. auto* comp = fc->getComponentAt (pos.toInt() - fc->getPosition());
  640. if (auto* pin = dynamic_cast<PinComponent*> (comp))
  641. return pin;
  642. }
  643. return nullptr;
  644. }
  645. void GraphEditorPanel::resized()
  646. {
  647. updateComponents();
  648. }
  649. void GraphEditorPanel::changeListenerCallback (ChangeBroadcaster*)
  650. {
  651. updateComponents();
  652. }
  653. void GraphEditorPanel::updateComponents()
  654. {
  655. for (int i = nodes.size(); --i >= 0;)
  656. if (graph.graph.getNodeForId (nodes.getUnchecked(i)->pluginID) == nullptr)
  657. nodes.remove (i);
  658. for (int i = connectors.size(); --i >= 0;)
  659. if (! graph.graph.isConnected (connectors.getUnchecked(i)->connection))
  660. connectors.remove (i);
  661. for (auto* fc : nodes)
  662. fc->update();
  663. for (auto* cc : connectors)
  664. cc->update();
  665. for (auto* f : graph.graph.getNodes())
  666. {
  667. if (getComponentForPlugin (f->nodeID) == nullptr)
  668. {
  669. auto* comp = nodes.add (new PluginComponent (*this, f->nodeID));
  670. addAndMakeVisible (comp);
  671. comp->update();
  672. }
  673. }
  674. for (auto& c : graph.graph.getConnections())
  675. {
  676. if (getComponentForConnection (c) == nullptr)
  677. {
  678. auto* comp = connectors.add (new ConnectorComponent (*this));
  679. addAndMakeVisible (comp);
  680. comp->setInput (c.source);
  681. comp->setOutput (c.destination);
  682. }
  683. }
  684. }
  685. void GraphEditorPanel::showPopupMenu (Point<int> mousePos)
  686. {
  687. menu.reset (new PopupMenu);
  688. if (auto* mainWindow = findParentComponentOfClass<MainHostWindow>())
  689. {
  690. mainWindow->addPluginsToMenu (*menu);
  691. menu->showMenuAsync ({},
  692. ModalCallbackFunction::create ([this, mousePos] (int r)
  693. {
  694. if (r > 0)
  695. if (auto* mainWin = findParentComponentOfClass<MainHostWindow>())
  696. createNewPlugin (mainWin->getChosenType (r), mousePos);
  697. }));
  698. }
  699. }
  700. void GraphEditorPanel::beginConnectorDrag (AudioProcessorGraph::NodeAndChannel source,
  701. AudioProcessorGraph::NodeAndChannel dest,
  702. const MouseEvent& e)
  703. {
  704. auto* c = dynamic_cast<ConnectorComponent*> (e.originalComponent);
  705. connectors.removeObject (c, false);
  706. draggingConnector.reset (c);
  707. if (draggingConnector == nullptr)
  708. draggingConnector.reset (new ConnectorComponent (*this));
  709. draggingConnector->setInput (source);
  710. draggingConnector->setOutput (dest);
  711. addAndMakeVisible (draggingConnector.get());
  712. draggingConnector->toFront (false);
  713. dragConnector (e);
  714. }
  715. void GraphEditorPanel::dragConnector (const MouseEvent& e)
  716. {
  717. auto e2 = e.getEventRelativeTo (this);
  718. if (draggingConnector != nullptr)
  719. {
  720. draggingConnector->setTooltip ({});
  721. auto pos = e2.position;
  722. if (auto* pin = findPinAt (pos))
  723. {
  724. auto connection = draggingConnector->connection;
  725. if (connection.source.nodeID == AudioProcessorGraph::NodeID() && ! pin->isInput)
  726. {
  727. connection.source = pin->pin;
  728. }
  729. else if (connection.destination.nodeID == AudioProcessorGraph::NodeID() && pin->isInput)
  730. {
  731. connection.destination = pin->pin;
  732. }
  733. if (graph.graph.canConnect (connection))
  734. {
  735. pos = (pin->getParentComponent()->getPosition() + pin->getBounds().getCentre()).toFloat();
  736. draggingConnector->setTooltip (pin->getTooltip());
  737. }
  738. }
  739. if (draggingConnector->connection.source.nodeID == AudioProcessorGraph::NodeID())
  740. draggingConnector->dragStart (pos);
  741. else
  742. draggingConnector->dragEnd (pos);
  743. }
  744. }
  745. void GraphEditorPanel::endDraggingConnector (const MouseEvent& e)
  746. {
  747. if (draggingConnector == nullptr)
  748. return;
  749. draggingConnector->setTooltip ({});
  750. auto e2 = e.getEventRelativeTo (this);
  751. auto connection = draggingConnector->connection;
  752. draggingConnector = nullptr;
  753. if (auto* pin = findPinAt (e2.position))
  754. {
  755. if (connection.source.nodeID == AudioProcessorGraph::NodeID())
  756. {
  757. if (pin->isInput)
  758. return;
  759. connection.source = pin->pin;
  760. }
  761. else
  762. {
  763. if (! pin->isInput)
  764. return;
  765. connection.destination = pin->pin;
  766. }
  767. graph.graph.addConnection (connection);
  768. }
  769. }
  770. void GraphEditorPanel::timerCallback()
  771. {
  772. // this should only be called on touch devices
  773. jassert (isOnTouchDevice());
  774. stopTimer();
  775. showPopupMenu (originalTouchPos);
  776. }
  777. //==============================================================================
  778. struct GraphDocumentComponent::TooltipBar : public Component,
  779. private Timer
  780. {
  781. TooltipBar()
  782. {
  783. startTimer (100);
  784. }
  785. void paint (Graphics& g) override
  786. {
  787. g.setFont (Font ((float) getHeight() * 0.7f, Font::bold));
  788. g.setColour (Colours::black);
  789. g.drawFittedText (tip, 10, 0, getWidth() - 12, getHeight(), Justification::centredLeft, 1);
  790. }
  791. void timerCallback() override
  792. {
  793. String newTip;
  794. if (auto* underMouse = Desktop::getInstance().getMainMouseSource().getComponentUnderMouse())
  795. if (auto* ttc = dynamic_cast<TooltipClient*> (underMouse))
  796. if (! (underMouse->isMouseButtonDown() || underMouse->isCurrentlyBlockedByAnotherModalComponent()))
  797. newTip = ttc->getTooltip();
  798. if (newTip != tip)
  799. {
  800. tip = newTip;
  801. repaint();
  802. }
  803. }
  804. String tip;
  805. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TooltipBar)
  806. };
  807. //==============================================================================
  808. class GraphDocumentComponent::TitleBarComponent : public Component,
  809. private Button::Listener
  810. {
  811. public:
  812. explicit TitleBarComponent (GraphDocumentComponent& graphDocumentComponent)
  813. : owner (graphDocumentComponent)
  814. {
  815. static const unsigned char burgerMenuPathData[]
  816. = { 110,109,0,0,128,64,0,0,32,65,108,0,0,224,65,0,0,32,65,98,254,212,232,65,0,0,32,65,0,0,240,65,252,
  817. 169,17,65,0,0,240,65,0,0,0,65,98,0,0,240,65,8,172,220,64,254,212,232,65,0,0,192,64,0,0,224,65,0,0,
  818. 192,64,108,0,0,128,64,0,0,192,64,98,16,88,57,64,0,0,192,64,0,0,0,64,8,172,220,64,0,0,0,64,0,0,0,65,
  819. 98,0,0,0,64,252,169,17,65,16,88,57,64,0,0,32,65,0,0,128,64,0,0,32,65,99,109,0,0,224,65,0,0,96,65,108,
  820. 0,0,128,64,0,0,96,65,98,16,88,57,64,0,0,96,65,0,0,0,64,4,86,110,65,0,0,0,64,0,0,128,65,98,0,0,0,64,
  821. 254,212,136,65,16,88,57,64,0,0,144,65,0,0,128,64,0,0,144,65,108,0,0,224,65,0,0,144,65,98,254,212,232,
  822. 65,0,0,144,65,0,0,240,65,254,212,136,65,0,0,240,65,0,0,128,65,98,0,0,240,65,4,86,110,65,254,212,232,
  823. 65,0,0,96,65,0,0,224,65,0,0,96,65,99,109,0,0,224,65,0,0,176,65,108,0,0,128,64,0,0,176,65,98,16,88,57,
  824. 64,0,0,176,65,0,0,0,64,2,43,183,65,0,0,0,64,0,0,192,65,98,0,0,0,64,254,212,200,65,16,88,57,64,0,0,208,
  825. 65,0,0,128,64,0,0,208,65,108,0,0,224,65,0,0,208,65,98,254,212,232,65,0,0,208,65,0,0,240,65,254,212,
  826. 200,65,0,0,240,65,0,0,192,65,98,0,0,240,65,2,43,183,65,254,212,232,65,0,0,176,65,0,0,224,65,0,0,176,
  827. 65,99,101,0,0 };
  828. static const unsigned char pluginListPathData[]
  829. = { 110,109,193,202,222,64,80,50,21,64,108,0,0,48,65,0,0,0,0,108,160,154,112,65,80,50,21,64,108,0,0,48,65,80,
  830. 50,149,64,108,193,202,222,64,80,50,21,64,99,109,0,0,192,64,251,220,127,64,108,160,154,32,65,165,135,202,
  831. 64,108,160,154,32,65,250,220,47,65,108,0,0,192,64,102,144,10,65,108,0,0,192,64,251,220,127,64,99,109,0,0,
  832. 128,65,251,220,127,64,108,0,0,128,65,103,144,10,65,108,96,101,63,65,251,220,47,65,108,96,101,63,65,166,135,
  833. 202,64,108,0,0,128,65,251,220,127,64,99,109,96,101,79,65,148,76,69,65,108,0,0,136,65,0,0,32,65,108,80,
  834. 77,168,65,148,76,69,65,108,0,0,136,65,40,153,106,65,108,96,101,79,65,148,76,69,65,99,109,0,0,64,65,63,247,
  835. 95,65,108,80,77,128,65,233,161,130,65,108,80,77,128,65,125,238,167,65,108,0,0,64,65,51,72,149,65,108,0,0,64,
  836. 65,63,247,95,65,99,109,0,0,176,65,63,247,95,65,108,0,0,176,65,51,72,149,65,108,176,178,143,65,125,238,167,65,
  837. 108,176,178,143,65,233,161,130,65,108,0,0,176,65,63,247,95,65,99,109,12,86,118,63,148,76,69,65,108,0,0,160,
  838. 64,0,0,32,65,108,159,154,16,65,148,76,69,65,108,0,0,160,64,40,153,106,65,108,12,86,118,63,148,76,69,65,99,
  839. 109,0,0,0,0,63,247,95,65,108,62,53,129,64,233,161,130,65,108,62,53,129,64,125,238,167,65,108,0,0,0,0,51,
  840. 72,149,65,108,0,0,0,0,63,247,95,65,99,109,0,0,32,65,63,247,95,65,108,0,0,32,65,51,72,149,65,108,193,202,190,
  841. 64,125,238,167,65,108,193,202,190,64,233,161,130,65,108,0,0,32,65,63,247,95,65,99,101,0,0 };
  842. {
  843. Path p;
  844. p.loadPathFromData (burgerMenuPathData, sizeof (burgerMenuPathData));
  845. burgerButton.setShape (p, true, true, false);
  846. }
  847. {
  848. Path p;
  849. p.loadPathFromData (pluginListPathData, sizeof (pluginListPathData));
  850. pluginButton.setShape (p, true, true, false);
  851. }
  852. burgerButton.addListener (this);
  853. addAndMakeVisible (burgerButton);
  854. pluginButton.addListener (this);
  855. addAndMakeVisible (pluginButton);
  856. titleLabel.setJustificationType (Justification::centredLeft);
  857. addAndMakeVisible (titleLabel);
  858. setOpaque (true);
  859. }
  860. private:
  861. void paint (Graphics& g) override
  862. {
  863. auto titleBarBackgroundColour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker();
  864. g.setColour (titleBarBackgroundColour);
  865. g.fillRect (getLocalBounds());
  866. }
  867. void resized() override
  868. {
  869. auto r = getLocalBounds();
  870. burgerButton.setBounds (r.removeFromLeft (40).withSizeKeepingCentre (20, 20));
  871. pluginButton.setBounds (r.removeFromRight (40).withSizeKeepingCentre (20, 20));
  872. titleLabel.setFont (Font (static_cast<float> (getHeight()) * 0.5f, Font::plain));
  873. titleLabel.setBounds (r);
  874. }
  875. void buttonClicked (Button* b) override
  876. {
  877. owner.showSidePanel (b == &burgerButton);
  878. }
  879. GraphDocumentComponent& owner;
  880. Label titleLabel {"titleLabel", "Plugin Host"};
  881. ShapeButton burgerButton {"burgerButton", Colours::lightgrey, Colours::lightgrey, Colours::white};
  882. ShapeButton pluginButton {"pluginButton", Colours::lightgrey, Colours::lightgrey, Colours::white};
  883. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TitleBarComponent)
  884. };
  885. //==============================================================================
  886. struct GraphDocumentComponent::PluginListBoxModel : public ListBoxModel,
  887. public ChangeListener,
  888. public MouseListener
  889. {
  890. PluginListBoxModel (ListBox& lb, KnownPluginList& kpl)
  891. : owner (lb),
  892. knownPlugins (kpl)
  893. {
  894. knownPlugins.addChangeListener (this);
  895. owner.addMouseListener (this, true);
  896. #if JUCE_IOS
  897. scanner.reset (new AUScanner (knownPlugins));
  898. #endif
  899. }
  900. int getNumRows() override
  901. {
  902. return knownPlugins.getNumTypes();
  903. }
  904. void paintListBoxItem (int rowNumber, Graphics& g,
  905. int width, int height, bool rowIsSelected) override
  906. {
  907. g.fillAll (rowIsSelected ? Colour (0xff42A2C8)
  908. : Colour (0xff263238));
  909. g.setColour (rowIsSelected ? Colours::black : Colours::white);
  910. if (rowNumber < knownPlugins.getNumTypes())
  911. g.drawFittedText (knownPlugins.getTypes()[rowNumber].name, { 0, 0, width, height - 2 }, Justification::centred, 1);
  912. g.setColour (Colours::black.withAlpha (0.4f));
  913. g.drawRect (0, height - 1, width, 1);
  914. }
  915. var getDragSourceDescription (const SparseSet<int>& selectedRows) override
  916. {
  917. if (! isOverSelectedRow)
  918. return var();
  919. return String ("PLUGIN: " + String (selectedRows[0]));
  920. }
  921. void changeListenerCallback (ChangeBroadcaster*) override
  922. {
  923. owner.updateContent();
  924. }
  925. void mouseDown (const MouseEvent& e) override
  926. {
  927. isOverSelectedRow = owner.getRowPosition (owner.getSelectedRow(), true)
  928. .contains (e.getEventRelativeTo (&owner).getMouseDownPosition());
  929. }
  930. ListBox& owner;
  931. KnownPluginList& knownPlugins;
  932. bool isOverSelectedRow = false;
  933. #if JUCE_IOS
  934. std::unique_ptr<AUScanner> scanner;
  935. #endif
  936. JUCE_DECLARE_NON_COPYABLE (PluginListBoxModel)
  937. };
  938. //==============================================================================
  939. GraphDocumentComponent::GraphDocumentComponent (AudioPluginFormatManager& fm,
  940. AudioDeviceManager& dm,
  941. KnownPluginList& kpl)
  942. : graph (new PluginGraph (fm, kpl)),
  943. deviceManager (dm),
  944. pluginList (kpl),
  945. graphPlayer (getAppProperties().getUserSettings()->getBoolValue ("doublePrecisionProcessing", false))
  946. {
  947. init();
  948. deviceManager.addChangeListener (graphPanel.get());
  949. deviceManager.addAudioCallback (&graphPlayer);
  950. deviceManager.addMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector());
  951. deviceManager.addChangeListener (this);
  952. }
  953. void GraphDocumentComponent::init()
  954. {
  955. updateMidiOutput();
  956. graphPanel.reset (new GraphEditorPanel (*graph));
  957. addAndMakeVisible (graphPanel.get());
  958. graphPlayer.setProcessor (&graph->graph);
  959. keyState.addListener (&graphPlayer.getMidiMessageCollector());
  960. keyboardComp.reset (new MidiKeyboardComponent (keyState, MidiKeyboardComponent::horizontalKeyboard));
  961. addAndMakeVisible (keyboardComp.get());
  962. statusBar.reset (new TooltipBar());
  963. addAndMakeVisible (statusBar.get());
  964. graphPanel->updateComponents();
  965. if (isOnTouchDevice())
  966. {
  967. titleBarComponent.reset (new TitleBarComponent (*this));
  968. addAndMakeVisible (titleBarComponent.get());
  969. pluginListBoxModel.reset (new PluginListBoxModel (pluginListBox, pluginList));
  970. pluginListBox.setModel (pluginListBoxModel.get());
  971. pluginListBox.setRowHeight (40);
  972. pluginListSidePanel.setContent (&pluginListBox, false);
  973. mobileSettingsSidePanel.setContent (new AudioDeviceSelectorComponent (deviceManager,
  974. 0, 2, 0, 2,
  975. true, true, true, false));
  976. addAndMakeVisible (pluginListSidePanel);
  977. addAndMakeVisible (mobileSettingsSidePanel);
  978. }
  979. }
  980. GraphDocumentComponent::~GraphDocumentComponent()
  981. {
  982. if (midiOutput != nullptr)
  983. midiOutput->stopBackgroundThread();
  984. releaseGraph();
  985. keyState.removeListener (&graphPlayer.getMidiMessageCollector());
  986. }
  987. void GraphDocumentComponent::resized()
  988. {
  989. auto r = [this]
  990. {
  991. auto bounds = getLocalBounds();
  992. if (auto* display = Desktop::getInstance().getDisplays().getDisplayForRect (getScreenBounds()))
  993. return display->safeAreaInsets.subtractedFrom (bounds);
  994. return bounds;
  995. }();
  996. const int titleBarHeight = 40;
  997. const int keysHeight = 60;
  998. const int statusHeight = 20;
  999. if (isOnTouchDevice())
  1000. titleBarComponent->setBounds (r.removeFromTop(titleBarHeight));
  1001. keyboardComp->setBounds (r.removeFromBottom (keysHeight));
  1002. statusBar->setBounds (r.removeFromBottom (statusHeight));
  1003. graphPanel->setBounds (r);
  1004. checkAvailableWidth();
  1005. }
  1006. void GraphDocumentComponent::createNewPlugin (const PluginDescription& desc, Point<int> pos)
  1007. {
  1008. graphPanel->createNewPlugin (desc, pos);
  1009. }
  1010. void GraphDocumentComponent::unfocusKeyboardComponent()
  1011. {
  1012. keyboardComp->unfocusAllComponents();
  1013. }
  1014. void GraphDocumentComponent::releaseGraph()
  1015. {
  1016. deviceManager.removeAudioCallback (&graphPlayer);
  1017. deviceManager.removeMidiInputDeviceCallback ({}, &graphPlayer.getMidiMessageCollector());
  1018. if (graphPanel != nullptr)
  1019. {
  1020. deviceManager.removeChangeListener (graphPanel.get());
  1021. graphPanel = nullptr;
  1022. }
  1023. keyboardComp = nullptr;
  1024. statusBar = nullptr;
  1025. graphPlayer.setProcessor (nullptr);
  1026. graph = nullptr;
  1027. }
  1028. bool GraphDocumentComponent::isInterestedInDragSource (const SourceDetails& details)
  1029. {
  1030. return ((dynamic_cast<ListBox*> (details.sourceComponent.get()) != nullptr)
  1031. && details.description.toString().startsWith ("PLUGIN"));
  1032. }
  1033. void GraphDocumentComponent::itemDropped (const SourceDetails& details)
  1034. {
  1035. // don't allow items to be dropped behind the sidebar
  1036. if (pluginListSidePanel.getBounds().contains (details.localPosition))
  1037. return;
  1038. auto pluginTypeIndex = details.description.toString()
  1039. .fromFirstOccurrenceOf ("PLUGIN: ", false, false)
  1040. .getIntValue();
  1041. // must be a valid index!
  1042. jassert (isPositiveAndBelow (pluginTypeIndex, pluginList.getNumTypes()));
  1043. createNewPlugin (pluginList.getTypes()[pluginTypeIndex], details.localPosition);
  1044. }
  1045. void GraphDocumentComponent::showSidePanel (bool showSettingsPanel)
  1046. {
  1047. if (showSettingsPanel)
  1048. mobileSettingsSidePanel.showOrHide (true);
  1049. else
  1050. pluginListSidePanel.showOrHide (true);
  1051. checkAvailableWidth();
  1052. lastOpenedSidePanel = showSettingsPanel ? &mobileSettingsSidePanel
  1053. : &pluginListSidePanel;
  1054. }
  1055. void GraphDocumentComponent::hideLastSidePanel()
  1056. {
  1057. if (lastOpenedSidePanel != nullptr)
  1058. lastOpenedSidePanel->showOrHide (false);
  1059. if (mobileSettingsSidePanel.isPanelShowing()) lastOpenedSidePanel = &mobileSettingsSidePanel;
  1060. else if (pluginListSidePanel.isPanelShowing()) lastOpenedSidePanel = &pluginListSidePanel;
  1061. else lastOpenedSidePanel = nullptr;
  1062. }
  1063. void GraphDocumentComponent::checkAvailableWidth()
  1064. {
  1065. if (mobileSettingsSidePanel.isPanelShowing() && pluginListSidePanel.isPanelShowing())
  1066. {
  1067. if (getWidth() - (mobileSettingsSidePanel.getWidth() + pluginListSidePanel.getWidth()) < 150)
  1068. hideLastSidePanel();
  1069. }
  1070. }
  1071. void GraphDocumentComponent::setDoublePrecision (bool doublePrecision)
  1072. {
  1073. graphPlayer.setDoublePrecisionProcessing (doublePrecision);
  1074. }
  1075. bool GraphDocumentComponent::closeAnyOpenPluginWindows()
  1076. {
  1077. return graphPanel->graph.closeAnyOpenPluginWindows();
  1078. }
  1079. void GraphDocumentComponent::changeListenerCallback (ChangeBroadcaster*)
  1080. {
  1081. updateMidiOutput();
  1082. }
  1083. void GraphDocumentComponent::updateMidiOutput()
  1084. {
  1085. auto* defaultMidiOutput = deviceManager.getDefaultMidiOutput();
  1086. if (midiOutput != defaultMidiOutput)
  1087. {
  1088. midiOutput = defaultMidiOutput;
  1089. if (midiOutput != nullptr)
  1090. midiOutput->startBackgroundThread();
  1091. graphPlayer.setMidiOutput (midiOutput);
  1092. }
  1093. }