mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-02-03 03:30:06 +00:00
AudioPluginHost: Add subprocess plugin scanning feature
This commit is contained in:
parent
aabd65b0fd
commit
7da8b73a96
6 changed files with 326 additions and 18 deletions
|
|
@ -31,16 +31,96 @@
|
|||
#error "If you're building the audio plugin host, you probably want to enable VST and/or AU support"
|
||||
#endif
|
||||
|
||||
class PluginScannerSubprocess : private ChildProcessSlave,
|
||||
private AsyncUpdater
|
||||
{
|
||||
public:
|
||||
PluginScannerSubprocess()
|
||||
{
|
||||
formatManager.addDefaultFormats();
|
||||
}
|
||||
|
||||
using ChildProcessSlave::initialiseFromCommandLine;
|
||||
|
||||
private:
|
||||
void handleMessageFromMaster (const MemoryBlock& mb) override
|
||||
{
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock (mutex);
|
||||
pendingBlocks.emplace (mb);
|
||||
}
|
||||
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
void handleConnectionLost() override
|
||||
{
|
||||
JUCEApplicationBase::quit();
|
||||
}
|
||||
|
||||
// It's important to run the plugin scan on the main thread!
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
const auto block = [&]() -> MemoryBlock
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock (mutex);
|
||||
|
||||
if (pendingBlocks.empty())
|
||||
return {};
|
||||
|
||||
auto out = std::move (pendingBlocks.front());
|
||||
pendingBlocks.pop();
|
||||
return out;
|
||||
}();
|
||||
|
||||
if (block.isEmpty())
|
||||
return;
|
||||
|
||||
MemoryInputStream stream { block, false };
|
||||
const auto formatName = stream.readString();
|
||||
const auto identifier = stream.readString();
|
||||
|
||||
OwnedArray<PluginDescription> results;
|
||||
|
||||
for (auto* format : formatManager.getFormats())
|
||||
if (format->getName() == formatName)
|
||||
format->findAllTypesForFile (results, identifier);
|
||||
|
||||
XmlElement xml ("LIST");
|
||||
|
||||
for (const auto& desc : results)
|
||||
xml.addChildElement (desc->createXml().release());
|
||||
|
||||
const auto str = xml.toString();
|
||||
sendMessageToMaster ({ str.toRawUTF8(), str.getNumBytesAsUTF8() });
|
||||
}
|
||||
}
|
||||
|
||||
AudioPluginFormatManager formatManager;
|
||||
|
||||
std::mutex mutex;
|
||||
std::queue<MemoryBlock> pendingBlocks;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class PluginHostApp : public JUCEApplication,
|
||||
private AsyncUpdater
|
||||
{
|
||||
public:
|
||||
PluginHostApp() {}
|
||||
PluginHostApp() = default;
|
||||
|
||||
void initialise (const String&) override
|
||||
void initialise (const String& commandLine) override
|
||||
{
|
||||
auto scannerSubprocess = std::make_unique<PluginScannerSubprocess>();
|
||||
|
||||
if (scannerSubprocess->initialiseFromCommandLine (commandLine, processUID))
|
||||
{
|
||||
storedScannerSubprocess = std::move (scannerSubprocess);
|
||||
return;
|
||||
}
|
||||
|
||||
// initialise our settings file..
|
||||
|
||||
PropertiesFile::Options options;
|
||||
|
|
@ -142,6 +222,7 @@ public:
|
|||
|
||||
private:
|
||||
std::unique_ptr<MainHostWindow> mainWindow;
|
||||
std::unique_ptr<PluginScannerSubprocess> storedScannerSubprocess;
|
||||
};
|
||||
|
||||
static PluginHostApp& getApp() { return *dynamic_cast<PluginHostApp*>(JUCEApplication::getInstance()); }
|
||||
|
|
|
|||
|
|
@ -27,6 +27,201 @@
|
|||
#include "MainHostWindow.h"
|
||||
#include "../Plugins/InternalPlugins.h"
|
||||
|
||||
constexpr const char* scanModeKey = "pluginScanMode";
|
||||
|
||||
class CustomPluginScanner : public KnownPluginList::CustomScanner,
|
||||
private ChangeListener
|
||||
{
|
||||
public:
|
||||
CustomPluginScanner()
|
||||
{
|
||||
if (auto* file = getAppProperties().getUserSettings())
|
||||
file->addChangeListener (this);
|
||||
|
||||
changeListenerCallback (nullptr);
|
||||
}
|
||||
|
||||
~CustomPluginScanner() override
|
||||
{
|
||||
if (auto* file = getAppProperties().getUserSettings())
|
||||
file->removeChangeListener (this);
|
||||
}
|
||||
|
||||
bool findPluginTypesFor (AudioPluginFormat& format,
|
||||
OwnedArray<PluginDescription>& result,
|
||||
const String& fileOrIdentifier) override
|
||||
{
|
||||
if (scanInProcess)
|
||||
{
|
||||
superprocess = nullptr;
|
||||
format.findAllTypesForFile (result, fileOrIdentifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (superprocess == nullptr)
|
||||
{
|
||||
superprocess = std::make_unique<Superprocess> (*this);
|
||||
|
||||
std::unique_lock<std::mutex> lock (mutex);
|
||||
connectionLost = false;
|
||||
}
|
||||
|
||||
MemoryBlock block;
|
||||
MemoryOutputStream stream { block, true };
|
||||
stream.writeString (format.getName());
|
||||
stream.writeString (fileOrIdentifier);
|
||||
|
||||
if (superprocess->sendMessageToSlave (block))
|
||||
{
|
||||
std::unique_lock<std::mutex> lock (mutex);
|
||||
gotResponse = false;
|
||||
pluginDescription = nullptr;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (condvar.wait_for (lock,
|
||||
std::chrono::milliseconds (50),
|
||||
[this] { return gotResponse || shouldExit(); }))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldExit())
|
||||
{
|
||||
superprocess = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (connectionLost)
|
||||
{
|
||||
superprocess = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pluginDescription != nullptr)
|
||||
{
|
||||
for (const auto* item : pluginDescription->getChildIterator())
|
||||
{
|
||||
auto desc = std::make_unique<PluginDescription>();
|
||||
|
||||
if (desc->loadFromXml (*item))
|
||||
result.add (std::move (desc));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
superprocess = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
void scanFinished() override
|
||||
{
|
||||
superprocess = nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
class Superprocess : public ChildProcessMaster
|
||||
{
|
||||
public:
|
||||
explicit Superprocess (CustomPluginScanner& o)
|
||||
: owner (o)
|
||||
{
|
||||
launchSlaveProcess (File::getSpecialLocation (File::currentExecutableFile), processUID, 0, 0);
|
||||
}
|
||||
|
||||
private:
|
||||
void handleMessageFromSlave (const MemoryBlock& mb) override
|
||||
{
|
||||
auto xml = parseXML (mb.toString());
|
||||
|
||||
const std::lock_guard<std::mutex> lock (owner.mutex);
|
||||
owner.pluginDescription = std::move (xml);
|
||||
owner.gotResponse = true;
|
||||
owner.condvar.notify_one();
|
||||
}
|
||||
|
||||
void handleConnectionLost() override
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock (owner.mutex);
|
||||
owner.pluginDescription = nullptr;
|
||||
owner.gotResponse = true;
|
||||
owner.connectionLost = true;
|
||||
owner.condvar.notify_one();
|
||||
}
|
||||
|
||||
CustomPluginScanner& owner;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Superprocess)
|
||||
};
|
||||
|
||||
void changeListenerCallback (ChangeBroadcaster*) override
|
||||
{
|
||||
if (auto* file = getAppProperties().getUserSettings())
|
||||
scanInProcess = (file->getIntValue (scanModeKey) == 0);
|
||||
}
|
||||
|
||||
std::unique_ptr<Superprocess> superprocess;
|
||||
std::mutex mutex;
|
||||
std::condition_variable condvar;
|
||||
std::unique_ptr<XmlElement> pluginDescription;
|
||||
bool gotResponse = false;
|
||||
bool connectionLost = false;
|
||||
|
||||
std::atomic<bool> scanInProcess { true };
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomPluginScanner)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class CustomPluginListComponent : public PluginListComponent
|
||||
{
|
||||
public:
|
||||
CustomPluginListComponent (AudioPluginFormatManager& manager,
|
||||
KnownPluginList& listToRepresent,
|
||||
const File& pedal,
|
||||
PropertiesFile* props,
|
||||
bool async)
|
||||
: PluginListComponent (manager, listToRepresent, pedal, props, async)
|
||||
{
|
||||
addAndMakeVisible (validationModeLabel);
|
||||
addAndMakeVisible (validationModeBox);
|
||||
|
||||
validationModeLabel.attachToComponent (&validationModeBox, true);
|
||||
validationModeLabel.setJustificationType (Justification::right);
|
||||
validationModeLabel.setSize (100, 30);
|
||||
|
||||
auto unusedId = 1;
|
||||
|
||||
for (const auto mode : { "In-process", "Out-of-process" })
|
||||
validationModeBox.addItem (mode, unusedId++);
|
||||
|
||||
validationModeBox.setSelectedItemIndex (getAppProperties().getUserSettings()->getIntValue (scanModeKey));
|
||||
|
||||
validationModeBox.onChange = [this]
|
||||
{
|
||||
getAppProperties().getUserSettings()->setValue (scanModeKey, validationModeBox.getSelectedItemIndex());
|
||||
};
|
||||
|
||||
resized();
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
PluginListComponent::resized();
|
||||
|
||||
const auto& buttonBounds = getOptionsButton().getBounds();
|
||||
validationModeBox.setBounds (buttonBounds.withWidth (130).withRightX (getWidth() - buttonBounds.getX()));
|
||||
}
|
||||
|
||||
private:
|
||||
Label validationModeLabel { {}, "Scan mode" };
|
||||
ComboBox validationModeBox;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomPluginListComponent)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class MainHostWindow::PluginListWindow : public DocumentWindow
|
||||
|
|
@ -41,10 +236,11 @@ public:
|
|||
auto deadMansPedalFile = getAppProperties().getUserSettings()
|
||||
->getFile().getSiblingFile ("RecentlyCrashedPluginsList");
|
||||
|
||||
setContentOwned (new PluginListComponent (pluginFormatManager,
|
||||
owner.knownPluginList,
|
||||
deadMansPedalFile,
|
||||
getAppProperties().getUserSettings(), true), true);
|
||||
setContentOwned (new CustomPluginListComponent (pluginFormatManager,
|
||||
owner.knownPluginList,
|
||||
deadMansPedalFile,
|
||||
getAppProperties().getUserSettings(),
|
||||
true), true);
|
||||
|
||||
setResizable (true, false);
|
||||
setResizeLimits (300, 400, 800, 1500);
|
||||
|
|
@ -96,6 +292,8 @@ MainHostWindow::MainHostWindow()
|
|||
centreWithSize (800, 600);
|
||||
#endif
|
||||
|
||||
knownPluginList.setCustomScanner (std::make_unique<CustomPluginScanner>());
|
||||
|
||||
graphHolder.reset (new GraphDocumentComponent (formatManager, deviceManager, knownPluginList));
|
||||
|
||||
setContentNonOwned (graphHolder.get(), false);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ void setAutoScaleValueForPlugin (const String&, AutoScale);
|
|||
bool shouldAutoScalePlugin (const PluginDescription&);
|
||||
void addPluginAutoScaleOptionsSubMenu (AudioPluginInstance*, PopupMenu&);
|
||||
|
||||
constexpr const char* processUID = "juceaudiopluginhost";
|
||||
|
||||
//==============================================================================
|
||||
class MainHostWindow : public DocumentWindow,
|
||||
public MenuBarModel,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
#include "juce_audio_processors.h"
|
||||
#include <juce_gui_extra/juce_gui_extra.h>
|
||||
#include <set>
|
||||
|
||||
//==============================================================================
|
||||
#if JUCE_MAC
|
||||
|
|
|
|||
|
|
@ -396,6 +396,9 @@ public:
|
|||
numThreads (threads),
|
||||
allowAsync (allowPluginsWhichRequireAsynchronousInstantiation)
|
||||
{
|
||||
const auto blacklisted = owner.list.getBlacklistedFiles();
|
||||
initiallyBlacklistedFiles = std::set<String> (blacklisted.begin(), blacklisted.end());
|
||||
|
||||
FileSearchPath path (formatToScan.getDefaultLocationsToSearch());
|
||||
|
||||
// You need to use at least one thread when scanning plug-ins asynchronously
|
||||
|
|
@ -450,6 +453,7 @@ private:
|
|||
const int numThreads;
|
||||
bool allowAsync, finished = false, timerReentrancyCheck = false;
|
||||
std::unique_ptr<ThreadPool> pool;
|
||||
std::set<String> initiallyBlacklistedFiles;
|
||||
|
||||
static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner)
|
||||
{
|
||||
|
|
@ -561,8 +565,16 @@ private:
|
|||
|
||||
void finishedScan()
|
||||
{
|
||||
owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles()
|
||||
: StringArray());
|
||||
const auto blacklisted = owner.list.getBlacklistedFiles();
|
||||
std::set<String> allBlacklistedFiles (blacklisted.begin(), blacklisted.end());
|
||||
|
||||
std::vector<String> newBlacklistedFiles;
|
||||
std::set_difference (allBlacklistedFiles.begin(), allBlacklistedFiles.end(),
|
||||
initiallyBlacklistedFiles.begin(), initiallyBlacklistedFiles.end(),
|
||||
std::back_inserter (newBlacklistedFiles));
|
||||
|
||||
owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles() : StringArray(),
|
||||
newBlacklistedFiles);
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
|
|
@ -636,21 +648,33 @@ bool PluginListComponent::isScanning() const noexcept
|
|||
return currentScanner != nullptr;
|
||||
}
|
||||
|
||||
void PluginListComponent::scanFinished (const StringArray& failedFiles)
|
||||
void PluginListComponent::scanFinished (const StringArray& failedFiles,
|
||||
const std::vector<String>& newBlacklistedFiles)
|
||||
{
|
||||
StringArray shortNames;
|
||||
StringArray warnings;
|
||||
|
||||
for (auto& f : failedFiles)
|
||||
shortNames.add (File::createFileWithoutCheckingPath (f).getFileName());
|
||||
const auto addWarningText = [&warnings] (const auto& range, const auto& prefix)
|
||||
{
|
||||
if (range.size() == 0)
|
||||
return;
|
||||
|
||||
StringArray names;
|
||||
|
||||
for (auto& f : range)
|
||||
names.add (File::createFileWithoutCheckingPath (f).getFileName());
|
||||
|
||||
warnings.add (prefix + ":\n\n" + names.joinIntoString (", "));
|
||||
};
|
||||
|
||||
addWarningText (newBlacklistedFiles, TRANS ("The following files encountered fatal errors during validation"));
|
||||
addWarningText (failedFiles, TRANS ("The following files appeared to be plugin files, but failed to load correctly"));
|
||||
|
||||
currentScanner.reset(); // mustn't delete this before using the failed files array
|
||||
|
||||
if (shortNames.size() > 0)
|
||||
if (! warnings.isEmpty())
|
||||
AlertWindow::showMessageBoxAsync (MessageBoxIconType::InfoIcon,
|
||||
TRANS("Scan complete"),
|
||||
TRANS("Note that the following files appeared to be plugin files, but failed to load correctly")
|
||||
+ ":\n\n"
|
||||
+ shortNames.joinIntoString (", "));
|
||||
warnings.joinIntoString ("\n\n"));
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -109,6 +109,9 @@ public:
|
|||
*/
|
||||
TextButton& getOptionsButton() { return optionsButton; }
|
||||
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AudioPluginFormatManager& formatManager;
|
||||
|
|
@ -127,12 +130,11 @@ private:
|
|||
class Scanner;
|
||||
std::unique_ptr<Scanner> currentScanner;
|
||||
|
||||
void scanFinished (const StringArray&);
|
||||
void scanFinished (const StringArray&, const std::vector<String>&);
|
||||
void updateList();
|
||||
void removeMissingPlugins();
|
||||
void removePluginItem (int index);
|
||||
|
||||
void resized() override;
|
||||
bool isInterestedInFileDrag (const StringArray&) override;
|
||||
void filesDropped (const StringArray&, int, int) override;
|
||||
void changeListenerCallback (ChangeBroadcaster*) override;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue