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.

502 lines
17KB

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