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.

539 lines
19KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. #include <JuceHeader.h>
  19. #include "../UI/MainHostWindow.h"
  20. #include "PluginGraph.h"
  21. #include "InternalPlugins.h"
  22. #include "../UI/GraphEditorPanel.h"
  23. static std::unique_ptr<ScopedDPIAwarenessDisabler> makeDPIAwarenessDisablerForPlugin (const PluginDescription& desc)
  24. {
  25. return shouldAutoScalePlugin (desc) ? std::make_unique<ScopedDPIAwarenessDisabler>()
  26. : nullptr;
  27. }
  28. //==============================================================================
  29. PluginGraph::PluginGraph (AudioPluginFormatManager& fm, KnownPluginList& kpl)
  30. : FileBasedDocument (getFilenameSuffix(),
  31. getFilenameWildcard(),
  32. "Load a graph",
  33. "Save a graph"),
  34. formatManager (fm),
  35. knownPlugins (kpl)
  36. {
  37. newDocument();
  38. graph.addListener (this);
  39. }
  40. PluginGraph::~PluginGraph()
  41. {
  42. graph.removeListener (this);
  43. graph.removeChangeListener (this);
  44. graph.clear();
  45. }
  46. PluginGraph::NodeID PluginGraph::getNextUID() noexcept
  47. {
  48. return PluginGraph::NodeID (++(lastUID.uid));
  49. }
  50. //==============================================================================
  51. void PluginGraph::changeListenerCallback (ChangeBroadcaster*)
  52. {
  53. changed();
  54. for (int i = activePluginWindows.size(); --i >= 0;)
  55. if (! graph.getNodes().contains (activePluginWindows.getUnchecked(i)->node))
  56. activePluginWindows.remove (i);
  57. }
  58. AudioProcessorGraph::Node::Ptr PluginGraph::getNodeForName (const String& name) const
  59. {
  60. for (auto* node : graph.getNodes())
  61. if (auto p = node->getProcessor())
  62. if (p->getName().equalsIgnoreCase (name))
  63. return node;
  64. return nullptr;
  65. }
  66. void PluginGraph::addPlugin (const PluginDescriptionAndPreference& desc, Point<double> pos)
  67. {
  68. std::shared_ptr<ScopedDPIAwarenessDisabler> dpiDisabler = makeDPIAwarenessDisablerForPlugin (desc.pluginDescription);
  69. formatManager.createPluginInstanceAsync (desc.pluginDescription,
  70. graph.getSampleRate(),
  71. graph.getBlockSize(),
  72. [this, pos, dpiDisabler, useARA = desc.useARA] (std::unique_ptr<AudioPluginInstance> instance, const String& error)
  73. {
  74. addPluginCallback (std::move (instance), error, pos, useARA);
  75. });
  76. }
  77. void PluginGraph::addPluginCallback (std::unique_ptr<AudioPluginInstance> instance,
  78. const String& error,
  79. Point<double> pos,
  80. PluginDescriptionAndPreference::UseARA useARA)
  81. {
  82. if (instance == nullptr)
  83. {
  84. auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
  85. TRANS ("Couldn't create plugin"),
  86. error);
  87. messageBox = AlertWindow::showScopedAsync (options, nullptr);
  88. }
  89. else
  90. {
  91. #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
  92. if (useARA == PluginDescriptionAndPreference::UseARA::yes
  93. && instance->getPluginDescription().hasARAExtension)
  94. {
  95. instance = std::make_unique<ARAPluginInstanceWrapper> (std::move (instance));
  96. }
  97. #endif
  98. instance->enableAllBuses();
  99. if (auto node = graph.addNode (std::move (instance)))
  100. {
  101. node->properties.set ("x", pos.x);
  102. node->properties.set ("y", pos.y);
  103. node->properties.set ("useARA", useARA == PluginDescriptionAndPreference::UseARA::yes);
  104. changed();
  105. }
  106. }
  107. }
  108. void PluginGraph::setNodePosition (NodeID nodeID, Point<double> pos)
  109. {
  110. if (auto* n = graph.getNodeForId (nodeID))
  111. {
  112. n->properties.set ("x", jlimit (0.0, 1.0, pos.x));
  113. n->properties.set ("y", jlimit (0.0, 1.0, pos.y));
  114. }
  115. }
  116. Point<double> PluginGraph::getNodePosition (NodeID nodeID) const
  117. {
  118. if (auto* n = graph.getNodeForId (nodeID))
  119. return { static_cast<double> (n->properties ["x"]),
  120. static_cast<double> (n->properties ["y"]) };
  121. return {};
  122. }
  123. //==============================================================================
  124. void PluginGraph::clear()
  125. {
  126. closeAnyOpenPluginWindows();
  127. graph.clear();
  128. changed();
  129. }
  130. PluginWindow* PluginGraph::getOrCreateWindowFor (AudioProcessorGraph::Node* node, PluginWindow::Type type)
  131. {
  132. jassert (node != nullptr);
  133. #if JUCE_IOS || JUCE_ANDROID
  134. closeAnyOpenPluginWindows();
  135. #else
  136. for (auto* w : activePluginWindows)
  137. if (w->node.get() == node && w->type == type)
  138. return w;
  139. #endif
  140. if (auto* processor = node->getProcessor())
  141. {
  142. if (auto* plugin = dynamic_cast<AudioPluginInstance*> (processor))
  143. {
  144. auto description = plugin->getPluginDescription();
  145. if (! plugin->hasEditor() && description.pluginFormatName == "Internal")
  146. {
  147. getCommandManager().invokeDirectly (CommandIDs::showAudioSettings, false);
  148. return nullptr;
  149. }
  150. auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (description);
  151. return activePluginWindows.add (new PluginWindow (node, type, activePluginWindows));
  152. }
  153. }
  154. return nullptr;
  155. }
  156. bool PluginGraph::closeAnyOpenPluginWindows()
  157. {
  158. bool wasEmpty = activePluginWindows.isEmpty();
  159. activePluginWindows.clear();
  160. return ! wasEmpty;
  161. }
  162. //==============================================================================
  163. String PluginGraph::getDocumentTitle()
  164. {
  165. if (! getFile().exists())
  166. return "Unnamed";
  167. return getFile().getFileNameWithoutExtension();
  168. }
  169. void PluginGraph::newDocument()
  170. {
  171. clear();
  172. setFile ({});
  173. graph.removeChangeListener (this);
  174. InternalPluginFormat internalFormat;
  175. jassert (internalFormat.getAllTypes().size() > 3);
  176. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[0] }, { 0.5, 0.1 });
  177. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[1] }, { 0.25, 0.1 });
  178. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[2] }, { 0.5, 0.9 });
  179. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[3] }, { 0.25, 0.9 });
  180. MessageManager::callAsync ([this]
  181. {
  182. setChangedFlag (false);
  183. graph.addChangeListener (this);
  184. });
  185. }
  186. Result PluginGraph::loadDocument (const File& file)
  187. {
  188. if (auto xml = parseXMLIfTagMatches (file, "FILTERGRAPH"))
  189. {
  190. graph.removeChangeListener (this);
  191. restoreFromXml (*xml);
  192. MessageManager::callAsync ([this]
  193. {
  194. setChangedFlag (false);
  195. graph.addChangeListener (this);
  196. });
  197. return Result::ok();
  198. }
  199. return Result::fail ("Not a valid graph file");
  200. }
  201. Result PluginGraph::saveDocument (const File& file)
  202. {
  203. auto xml = createXml();
  204. if (! xml->writeTo (file, {}))
  205. return Result::fail ("Couldn't write to the file");
  206. return Result::ok();
  207. }
  208. File PluginGraph::getLastDocumentOpened()
  209. {
  210. RecentlyOpenedFilesList recentFiles;
  211. recentFiles.restoreFromString (getAppProperties().getUserSettings()
  212. ->getValue ("recentFilterGraphFiles"));
  213. return recentFiles.getFile (0);
  214. }
  215. void PluginGraph::setLastDocumentOpened (const File& file)
  216. {
  217. RecentlyOpenedFilesList recentFiles;
  218. recentFiles.restoreFromString (getAppProperties().getUserSettings()
  219. ->getValue ("recentFilterGraphFiles"));
  220. recentFiles.addFile (file);
  221. getAppProperties().getUserSettings()
  222. ->setValue ("recentFilterGraphFiles", recentFiles.toString());
  223. }
  224. //==============================================================================
  225. static void readBusLayoutFromXml (AudioProcessor::BusesLayout& busesLayout, AudioProcessor& plugin,
  226. const XmlElement& xml, bool isInput)
  227. {
  228. auto& targetBuses = (isInput ? busesLayout.inputBuses
  229. : busesLayout.outputBuses);
  230. int maxNumBuses = 0;
  231. if (auto* buses = xml.getChildByName (isInput ? "INPUTS" : "OUTPUTS"))
  232. {
  233. for (auto* e : buses->getChildWithTagNameIterator ("BUS"))
  234. {
  235. const int busIdx = e->getIntAttribute ("index");
  236. maxNumBuses = jmax (maxNumBuses, busIdx + 1);
  237. // the number of buses on busesLayout may not be in sync with the plugin after adding buses
  238. // because adding an input bus could also add an output bus
  239. for (int actualIdx = plugin.getBusCount (isInput) - 1; actualIdx < busIdx; ++actualIdx)
  240. if (! plugin.addBus (isInput))
  241. return;
  242. for (int actualIdx = targetBuses.size() - 1; actualIdx < busIdx; ++actualIdx)
  243. targetBuses.add (plugin.getChannelLayoutOfBus (isInput, busIdx));
  244. auto layout = e->getStringAttribute ("layout");
  245. if (layout.isNotEmpty())
  246. targetBuses.getReference (busIdx) = AudioChannelSet::fromAbbreviatedString (layout);
  247. }
  248. }
  249. // if the plugin has more buses than specified in the xml, then try to remove them!
  250. while (maxNumBuses < targetBuses.size())
  251. {
  252. if (! plugin.removeBus (isInput))
  253. return;
  254. targetBuses.removeLast();
  255. }
  256. }
  257. //==============================================================================
  258. static XmlElement* createBusLayoutXml (const AudioProcessor::BusesLayout& layout, const bool isInput)
  259. {
  260. auto& buses = isInput ? layout.inputBuses
  261. : layout.outputBuses;
  262. auto* xml = new XmlElement (isInput ? "INPUTS" : "OUTPUTS");
  263. for (int busIdx = 0; busIdx < buses.size(); ++busIdx)
  264. {
  265. auto& set = buses.getReference (busIdx);
  266. auto* bus = xml->createNewChildElement ("BUS");
  267. bus->setAttribute ("index", busIdx);
  268. bus->setAttribute ("layout", set.isDisabled() ? "disabled" : set.getSpeakerArrangementAsString());
  269. }
  270. return xml;
  271. }
  272. static XmlElement* createNodeXml (AudioProcessorGraph::Node* const node) noexcept
  273. {
  274. if (auto* plugin = dynamic_cast<AudioPluginInstance*> (node->getProcessor()))
  275. {
  276. auto e = new XmlElement ("FILTER");
  277. e->setAttribute ("uid", (int) node->nodeID.uid);
  278. e->setAttribute ("x", node->properties ["x"].toString());
  279. e->setAttribute ("y", node->properties ["y"].toString());
  280. e->setAttribute ("useARA", node->properties ["useARA"].toString());
  281. for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i)
  282. {
  283. auto type = (PluginWindow::Type) i;
  284. if (node->properties.contains (PluginWindow::getOpenProp (type)))
  285. {
  286. e->setAttribute (PluginWindow::getLastXProp (type), node->properties[PluginWindow::getLastXProp (type)].toString());
  287. e->setAttribute (PluginWindow::getLastYProp (type), node->properties[PluginWindow::getLastYProp (type)].toString());
  288. e->setAttribute (PluginWindow::getOpenProp (type), node->properties[PluginWindow::getOpenProp (type)].toString());
  289. }
  290. }
  291. {
  292. PluginDescription pd;
  293. plugin->fillInPluginDescription (pd);
  294. e->addChildElement (pd.createXml().release());
  295. }
  296. {
  297. MemoryBlock m;
  298. node->getProcessor()->getStateInformation (m);
  299. e->createNewChildElement ("STATE")->addTextElement (m.toBase64Encoding());
  300. }
  301. auto layout = plugin->getBusesLayout();
  302. auto layouts = e->createNewChildElement ("LAYOUT");
  303. layouts->addChildElement (createBusLayoutXml (layout, true));
  304. layouts->addChildElement (createBusLayoutXml (layout, false));
  305. return e;
  306. }
  307. jassertfalse;
  308. return nullptr;
  309. }
  310. void PluginGraph::createNodeFromXml (const XmlElement& xml)
  311. {
  312. PluginDescriptionAndPreference pd;
  313. const auto nodeUsesARA = xml.getBoolAttribute ("useARA");
  314. for (auto* e : xml.getChildIterator())
  315. {
  316. if (pd.pluginDescription.loadFromXml (*e))
  317. {
  318. pd.useARA = nodeUsesARA ? PluginDescriptionAndPreference::UseARA::yes
  319. : PluginDescriptionAndPreference::UseARA::no;
  320. break;
  321. }
  322. }
  323. auto createInstanceWithFallback = [&]() -> std::unique_ptr<AudioPluginInstance>
  324. {
  325. auto createInstance = [this] (const PluginDescriptionAndPreference& description) -> std::unique_ptr<AudioPluginInstance>
  326. {
  327. String errorMessage;
  328. auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (description.pluginDescription);
  329. auto instance = formatManager.createPluginInstance (description.pluginDescription,
  330. graph.getSampleRate(),
  331. graph.getBlockSize(),
  332. errorMessage);
  333. #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
  334. if (instance
  335. && description.useARA == PluginDescriptionAndPreference::UseARA::yes
  336. && description.pluginDescription.hasARAExtension)
  337. {
  338. return std::make_unique<ARAPluginInstanceWrapper> (std::move (instance));
  339. }
  340. #endif
  341. return instance;
  342. };
  343. if (auto instance = createInstance (pd))
  344. return instance;
  345. const auto allFormats = formatManager.getFormats();
  346. const auto matchingFormat = std::find_if (allFormats.begin(), allFormats.end(),
  347. [&] (const AudioPluginFormat* f) { return f->getName() == pd.pluginDescription.pluginFormatName; });
  348. if (matchingFormat == allFormats.end())
  349. return nullptr;
  350. const auto plugins = knownPlugins.getTypesForFormat (**matchingFormat);
  351. const auto matchingPlugin = std::find_if (plugins.begin(), plugins.end(),
  352. [&] (const PluginDescription& desc) { return pd.pluginDescription.uniqueId == desc.uniqueId; });
  353. if (matchingPlugin == plugins.end())
  354. return nullptr;
  355. return createInstance (PluginDescriptionAndPreference { *matchingPlugin });
  356. };
  357. if (auto instance = createInstanceWithFallback())
  358. {
  359. if (auto* layoutEntity = xml.getChildByName ("LAYOUT"))
  360. {
  361. auto layout = instance->getBusesLayout();
  362. readBusLayoutFromXml (layout, *instance, *layoutEntity, true);
  363. readBusLayoutFromXml (layout, *instance, *layoutEntity, false);
  364. instance->setBusesLayout (layout);
  365. }
  366. if (auto node = graph.addNode (std::move (instance), NodeID ((uint32) xml.getIntAttribute ("uid"))))
  367. {
  368. if (auto* state = xml.getChildByName ("STATE"))
  369. {
  370. MemoryBlock m;
  371. m.fromBase64Encoding (state->getAllSubText());
  372. node->getProcessor()->setStateInformation (m.getData(), (int) m.getSize());
  373. }
  374. node->properties.set ("x", xml.getDoubleAttribute ("x"));
  375. node->properties.set ("y", xml.getDoubleAttribute ("y"));
  376. node->properties.set ("useARA", xml.getBoolAttribute ("useARA"));
  377. for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i)
  378. {
  379. auto type = (PluginWindow::Type) i;
  380. if (xml.hasAttribute (PluginWindow::getOpenProp (type)))
  381. {
  382. node->properties.set (PluginWindow::getLastXProp (type), xml.getIntAttribute (PluginWindow::getLastXProp (type)));
  383. node->properties.set (PluginWindow::getLastYProp (type), xml.getIntAttribute (PluginWindow::getLastYProp (type)));
  384. node->properties.set (PluginWindow::getOpenProp (type), xml.getIntAttribute (PluginWindow::getOpenProp (type)));
  385. if (node->properties[PluginWindow::getOpenProp (type)])
  386. {
  387. jassert (node->getProcessor() != nullptr);
  388. if (auto w = getOrCreateWindowFor (node, type))
  389. w->toFront (true);
  390. }
  391. }
  392. }
  393. }
  394. }
  395. }
  396. std::unique_ptr<XmlElement> PluginGraph::createXml() const
  397. {
  398. auto xml = std::make_unique<XmlElement> ("FILTERGRAPH");
  399. for (auto* node : graph.getNodes())
  400. xml->addChildElement (createNodeXml (node));
  401. for (auto& connection : graph.getConnections())
  402. {
  403. auto e = xml->createNewChildElement ("CONNECTION");
  404. e->setAttribute ("srcFilter", (int) connection.source.nodeID.uid);
  405. e->setAttribute ("srcChannel", connection.source.channelIndex);
  406. e->setAttribute ("dstFilter", (int) connection.destination.nodeID.uid);
  407. e->setAttribute ("dstChannel", connection.destination.channelIndex);
  408. }
  409. return xml;
  410. }
  411. void PluginGraph::restoreFromXml (const XmlElement& xml)
  412. {
  413. clear();
  414. for (auto* e : xml.getChildWithTagNameIterator ("FILTER"))
  415. {
  416. createNodeFromXml (*e);
  417. changed();
  418. }
  419. for (auto* e : xml.getChildWithTagNameIterator ("CONNECTION"))
  420. {
  421. graph.addConnection ({ { NodeID ((uint32) e->getIntAttribute ("srcFilter")), e->getIntAttribute ("srcChannel") },
  422. { NodeID ((uint32) e->getIntAttribute ("dstFilter")), e->getIntAttribute ("dstChannel") } });
  423. }
  424. graph.removeIllegalConnections();
  425. }
  426. File PluginGraph::getDefaultGraphDocumentOnMobile()
  427. {
  428. auto persistantStorageLocation = File::getSpecialLocation (File::userApplicationDataDirectory);
  429. return persistantStorageLocation.getChildFile ("state.filtergraph");
  430. }