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.

538 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. AlertWindow::showMessageBoxAsync (MessageBoxIconType::WarningIcon,
  85. TRANS("Couldn't create plugin"),
  86. error);
  87. }
  88. else
  89. {
  90. #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS)
  91. if (useARA == PluginDescriptionAndPreference::UseARA::yes
  92. && instance->getPluginDescription().hasARAExtension)
  93. {
  94. instance = std::make_unique<ARAPluginInstanceWrapper> (std::move (instance));
  95. }
  96. #endif
  97. instance->enableAllBuses();
  98. if (auto node = graph.addNode (std::move (instance)))
  99. {
  100. node->properties.set ("x", pos.x);
  101. node->properties.set ("y", pos.y);
  102. node->properties.set ("useARA", useARA == PluginDescriptionAndPreference::UseARA::yes);
  103. changed();
  104. }
  105. }
  106. }
  107. void PluginGraph::setNodePosition (NodeID nodeID, Point<double> pos)
  108. {
  109. if (auto* n = graph.getNodeForId (nodeID))
  110. {
  111. n->properties.set ("x", jlimit (0.0, 1.0, pos.x));
  112. n->properties.set ("y", jlimit (0.0, 1.0, pos.y));
  113. }
  114. }
  115. Point<double> PluginGraph::getNodePosition (NodeID nodeID) const
  116. {
  117. if (auto* n = graph.getNodeForId (nodeID))
  118. return { static_cast<double> (n->properties ["x"]),
  119. static_cast<double> (n->properties ["y"]) };
  120. return {};
  121. }
  122. //==============================================================================
  123. void PluginGraph::clear()
  124. {
  125. closeAnyOpenPluginWindows();
  126. graph.clear();
  127. changed();
  128. }
  129. PluginWindow* PluginGraph::getOrCreateWindowFor (AudioProcessorGraph::Node* node, PluginWindow::Type type)
  130. {
  131. jassert (node != nullptr);
  132. #if JUCE_IOS || JUCE_ANDROID
  133. closeAnyOpenPluginWindows();
  134. #else
  135. for (auto* w : activePluginWindows)
  136. if (w->node.get() == node && w->type == type)
  137. return w;
  138. #endif
  139. if (auto* processor = node->getProcessor())
  140. {
  141. if (auto* plugin = dynamic_cast<AudioPluginInstance*> (processor))
  142. {
  143. auto description = plugin->getPluginDescription();
  144. if (! plugin->hasEditor() && description.pluginFormatName == "Internal")
  145. {
  146. getCommandManager().invokeDirectly (CommandIDs::showAudioSettings, false);
  147. return nullptr;
  148. }
  149. auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (description);
  150. return activePluginWindows.add (new PluginWindow (node, type, activePluginWindows));
  151. }
  152. }
  153. return nullptr;
  154. }
  155. bool PluginGraph::closeAnyOpenPluginWindows()
  156. {
  157. bool wasEmpty = activePluginWindows.isEmpty();
  158. activePluginWindows.clear();
  159. return ! wasEmpty;
  160. }
  161. //==============================================================================
  162. String PluginGraph::getDocumentTitle()
  163. {
  164. if (! getFile().exists())
  165. return "Unnamed";
  166. return getFile().getFileNameWithoutExtension();
  167. }
  168. void PluginGraph::newDocument()
  169. {
  170. clear();
  171. setFile ({});
  172. graph.removeChangeListener (this);
  173. InternalPluginFormat internalFormat;
  174. jassert (internalFormat.getAllTypes().size() > 3);
  175. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[0] }, { 0.5, 0.1 });
  176. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[1] }, { 0.25, 0.1 });
  177. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[2] }, { 0.5, 0.9 });
  178. addPlugin (PluginDescriptionAndPreference { internalFormat.getAllTypes()[3] }, { 0.25, 0.9 });
  179. MessageManager::callAsync ([this]
  180. {
  181. setChangedFlag (false);
  182. graph.addChangeListener (this);
  183. });
  184. }
  185. Result PluginGraph::loadDocument (const File& file)
  186. {
  187. if (auto xml = parseXMLIfTagMatches (file, "FILTERGRAPH"))
  188. {
  189. graph.removeChangeListener (this);
  190. restoreFromXml (*xml);
  191. MessageManager::callAsync ([this]
  192. {
  193. setChangedFlag (false);
  194. graph.addChangeListener (this);
  195. });
  196. return Result::ok();
  197. }
  198. return Result::fail ("Not a valid graph file");
  199. }
  200. Result PluginGraph::saveDocument (const File& file)
  201. {
  202. auto xml = createXml();
  203. if (! xml->writeTo (file, {}))
  204. return Result::fail ("Couldn't write to the file");
  205. return Result::ok();
  206. }
  207. File PluginGraph::getLastDocumentOpened()
  208. {
  209. RecentlyOpenedFilesList recentFiles;
  210. recentFiles.restoreFromString (getAppProperties().getUserSettings()
  211. ->getValue ("recentFilterGraphFiles"));
  212. return recentFiles.getFile (0);
  213. }
  214. void PluginGraph::setLastDocumentOpened (const File& file)
  215. {
  216. RecentlyOpenedFilesList recentFiles;
  217. recentFiles.restoreFromString (getAppProperties().getUserSettings()
  218. ->getValue ("recentFilterGraphFiles"));
  219. recentFiles.addFile (file);
  220. getAppProperties().getUserSettings()
  221. ->setValue ("recentFilterGraphFiles", recentFiles.toString());
  222. }
  223. //==============================================================================
  224. static void readBusLayoutFromXml (AudioProcessor::BusesLayout& busesLayout, AudioProcessor& plugin,
  225. const XmlElement& xml, bool isInput)
  226. {
  227. auto& targetBuses = (isInput ? busesLayout.inputBuses
  228. : busesLayout.outputBuses);
  229. int maxNumBuses = 0;
  230. if (auto* buses = xml.getChildByName (isInput ? "INPUTS" : "OUTPUTS"))
  231. {
  232. for (auto* e : buses->getChildWithTagNameIterator ("BUS"))
  233. {
  234. const int busIdx = e->getIntAttribute ("index");
  235. maxNumBuses = jmax (maxNumBuses, busIdx + 1);
  236. // the number of buses on busesLayout may not be in sync with the plugin after adding buses
  237. // because adding an input bus could also add an output bus
  238. for (int actualIdx = plugin.getBusCount (isInput) - 1; actualIdx < busIdx; ++actualIdx)
  239. if (! plugin.addBus (isInput))
  240. return;
  241. for (int actualIdx = targetBuses.size() - 1; actualIdx < busIdx; ++actualIdx)
  242. targetBuses.add (plugin.getChannelLayoutOfBus (isInput, busIdx));
  243. auto layout = e->getStringAttribute ("layout");
  244. if (layout.isNotEmpty())
  245. targetBuses.getReference (busIdx) = AudioChannelSet::fromAbbreviatedString (layout);
  246. }
  247. }
  248. // if the plugin has more buses than specified in the xml, then try to remove them!
  249. while (maxNumBuses < targetBuses.size())
  250. {
  251. if (! plugin.removeBus (isInput))
  252. return;
  253. targetBuses.removeLast();
  254. }
  255. }
  256. //==============================================================================
  257. static XmlElement* createBusLayoutXml (const AudioProcessor::BusesLayout& layout, const bool isInput)
  258. {
  259. auto& buses = isInput ? layout.inputBuses
  260. : layout.outputBuses;
  261. auto* xml = new XmlElement (isInput ? "INPUTS" : "OUTPUTS");
  262. for (int busIdx = 0; busIdx < buses.size(); ++busIdx)
  263. {
  264. auto& set = buses.getReference (busIdx);
  265. auto* bus = xml->createNewChildElement ("BUS");
  266. bus->setAttribute ("index", busIdx);
  267. bus->setAttribute ("layout", set.isDisabled() ? "disabled" : set.getSpeakerArrangementAsString());
  268. }
  269. return xml;
  270. }
  271. static XmlElement* createNodeXml (AudioProcessorGraph::Node* const node) noexcept
  272. {
  273. if (auto* plugin = dynamic_cast<AudioPluginInstance*> (node->getProcessor()))
  274. {
  275. auto e = new XmlElement ("FILTER");
  276. e->setAttribute ("uid", (int) node->nodeID.uid);
  277. e->setAttribute ("x", node->properties ["x"].toString());
  278. e->setAttribute ("y", node->properties ["y"].toString());
  279. e->setAttribute ("useARA", node->properties ["useARA"].toString());
  280. for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i)
  281. {
  282. auto type = (PluginWindow::Type) i;
  283. if (node->properties.contains (PluginWindow::getOpenProp (type)))
  284. {
  285. e->setAttribute (PluginWindow::getLastXProp (type), node->properties[PluginWindow::getLastXProp (type)].toString());
  286. e->setAttribute (PluginWindow::getLastYProp (type), node->properties[PluginWindow::getLastYProp (type)].toString());
  287. e->setAttribute (PluginWindow::getOpenProp (type), node->properties[PluginWindow::getOpenProp (type)].toString());
  288. }
  289. }
  290. {
  291. PluginDescription pd;
  292. plugin->fillInPluginDescription (pd);
  293. e->addChildElement (pd.createXml().release());
  294. }
  295. {
  296. MemoryBlock m;
  297. node->getProcessor()->getStateInformation (m);
  298. e->createNewChildElement ("STATE")->addTextElement (m.toBase64Encoding());
  299. }
  300. auto layout = plugin->getBusesLayout();
  301. auto layouts = e->createNewChildElement ("LAYOUT");
  302. layouts->addChildElement (createBusLayoutXml (layout, true));
  303. layouts->addChildElement (createBusLayoutXml (layout, false));
  304. return e;
  305. }
  306. jassertfalse;
  307. return nullptr;
  308. }
  309. void PluginGraph::createNodeFromXml (const XmlElement& xml)
  310. {
  311. PluginDescriptionAndPreference pd;
  312. const auto nodeUsesARA = xml.getBoolAttribute ("useARA");
  313. for (auto* e : xml.getChildIterator())
  314. {
  315. if (pd.pluginDescription.loadFromXml (*e))
  316. {
  317. pd.useARA = nodeUsesARA ? PluginDescriptionAndPreference::UseARA::yes
  318. : PluginDescriptionAndPreference::UseARA::no;
  319. break;
  320. }
  321. }
  322. auto createInstanceWithFallback = [&]() -> std::unique_ptr<AudioPluginInstance>
  323. {
  324. auto createInstance = [this] (const PluginDescriptionAndPreference& description) -> std::unique_ptr<AudioPluginInstance>
  325. {
  326. String errorMessage;
  327. auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (description.pluginDescription);
  328. auto instance = formatManager.createPluginInstance (description.pluginDescription,
  329. graph.getSampleRate(),
  330. graph.getBlockSize(),
  331. errorMessage);
  332. #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS)
  333. if (instance
  334. && description.useARA == PluginDescriptionAndPreference::UseARA::yes
  335. && description.pluginDescription.hasARAExtension)
  336. {
  337. return std::make_unique<ARAPluginInstanceWrapper> (std::move (instance));
  338. }
  339. #endif
  340. return instance;
  341. };
  342. if (auto instance = createInstance (pd))
  343. return instance;
  344. const auto allFormats = formatManager.getFormats();
  345. const auto matchingFormat = std::find_if (allFormats.begin(), allFormats.end(),
  346. [&] (const AudioPluginFormat* f) { return f->getName() == pd.pluginDescription.pluginFormatName; });
  347. if (matchingFormat == allFormats.end())
  348. return nullptr;
  349. const auto plugins = knownPlugins.getTypesForFormat (**matchingFormat);
  350. const auto matchingPlugin = std::find_if (plugins.begin(), plugins.end(),
  351. [&] (const PluginDescription& desc) { return pd.pluginDescription.uniqueId == desc.uniqueId; });
  352. if (matchingPlugin == plugins.end())
  353. return nullptr;
  354. return createInstance (PluginDescriptionAndPreference { *matchingPlugin });
  355. };
  356. if (auto instance = createInstanceWithFallback())
  357. {
  358. if (auto* layoutEntity = xml.getChildByName ("LAYOUT"))
  359. {
  360. auto layout = instance->getBusesLayout();
  361. readBusLayoutFromXml (layout, *instance, *layoutEntity, true);
  362. readBusLayoutFromXml (layout, *instance, *layoutEntity, false);
  363. instance->setBusesLayout (layout);
  364. }
  365. if (auto node = graph.addNode (std::move (instance), NodeID ((uint32) xml.getIntAttribute ("uid"))))
  366. {
  367. if (auto* state = xml.getChildByName ("STATE"))
  368. {
  369. MemoryBlock m;
  370. m.fromBase64Encoding (state->getAllSubText());
  371. node->getProcessor()->setStateInformation (m.getData(), (int) m.getSize());
  372. }
  373. node->properties.set ("x", xml.getDoubleAttribute ("x"));
  374. node->properties.set ("y", xml.getDoubleAttribute ("y"));
  375. node->properties.set ("useARA", xml.getBoolAttribute ("useARA"));
  376. for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i)
  377. {
  378. auto type = (PluginWindow::Type) i;
  379. if (xml.hasAttribute (PluginWindow::getOpenProp (type)))
  380. {
  381. node->properties.set (PluginWindow::getLastXProp (type), xml.getIntAttribute (PluginWindow::getLastXProp (type)));
  382. node->properties.set (PluginWindow::getLastYProp (type), xml.getIntAttribute (PluginWindow::getLastYProp (type)));
  383. node->properties.set (PluginWindow::getOpenProp (type), xml.getIntAttribute (PluginWindow::getOpenProp (type)));
  384. if (node->properties[PluginWindow::getOpenProp (type)])
  385. {
  386. jassert (node->getProcessor() != nullptr);
  387. if (auto w = getOrCreateWindowFor (node, type))
  388. w->toFront (true);
  389. }
  390. }
  391. }
  392. }
  393. }
  394. }
  395. std::unique_ptr<XmlElement> PluginGraph::createXml() const
  396. {
  397. auto xml = std::make_unique<XmlElement> ("FILTERGRAPH");
  398. for (auto* node : graph.getNodes())
  399. xml->addChildElement (createNodeXml (node));
  400. for (auto& connection : graph.getConnections())
  401. {
  402. auto e = xml->createNewChildElement ("CONNECTION");
  403. e->setAttribute ("srcFilter", (int) connection.source.nodeID.uid);
  404. e->setAttribute ("srcChannel", connection.source.channelIndex);
  405. e->setAttribute ("dstFilter", (int) connection.destination.nodeID.uid);
  406. e->setAttribute ("dstChannel", connection.destination.channelIndex);
  407. }
  408. return xml;
  409. }
  410. void PluginGraph::restoreFromXml (const XmlElement& xml)
  411. {
  412. clear();
  413. for (auto* e : xml.getChildWithTagNameIterator ("FILTER"))
  414. {
  415. createNodeFromXml (*e);
  416. changed();
  417. }
  418. for (auto* e : xml.getChildWithTagNameIterator ("CONNECTION"))
  419. {
  420. graph.addConnection ({ { NodeID ((uint32) e->getIntAttribute ("srcFilter")), e->getIntAttribute ("srcChannel") },
  421. { NodeID ((uint32) e->getIntAttribute ("dstFilter")), e->getIntAttribute ("dstChannel") } });
  422. }
  423. graph.removeIllegalConnections();
  424. }
  425. File PluginGraph::getDefaultGraphDocumentOnMobile()
  426. {
  427. auto persistantStorageLocation = File::getSpecialLocation (File::userApplicationDataDirectory);
  428. return persistantStorageLocation.getChildFile ("state.filtergraph");
  429. }