Signed-off-by: falkTX <falktx@falktx.com>master
| @@ -165,11 +165,7 @@ | |||
| @see VSTPluginFormat, VST3PluginFormat, AudioPluginFormat, AudioPluginFormatManager, JUCE_PLUGINHOST_VST, JUCE_PLUGINHOST_AU | |||
| */ | |||
| #if 0 //MAC || WINDOWS | |||
| #define JUCE_PLUGINHOST_VST3 1 | |||
| #else | |||
| #define JUCE_PLUGINHOST_VST3 0 | |||
| #endif | |||
| #define JUCE_PLUGINHOST_VST3 1 | |||
| /** Config: JUCE_PLUGINHOST_AU | |||
| Enables the AudioUnit plugin hosting classes. This is Mac-only, of course. | |||
| @@ -188,6 +184,13 @@ | |||
| #define JUCE_PLUGINHOST_LADSPA 0 | |||
| #endif | |||
| /** Config: JUCE_PLUGINHOST_LV2 | |||
| * Enables the LV2 plugin hosting classes. | |||
| */ | |||
| #ifndef JUCE_PLUGINHOST_LV2 | |||
| #define JUCE_PLUGINHOST_LV2 0 | |||
| #endif | |||
| //============================================================================= | |||
| // juce_audio_utils | |||
| @@ -5,6 +5,7 @@ if linux_headless | |||
| 'source/modules/juce_audio_basics/juce_audio_basics.cpp', | |||
| 'source/modules/juce_audio_formats/juce_audio_formats.cpp', | |||
| 'source/modules/juce_audio_processors/juce_audio_processors.cpp', | |||
| # 'source/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp', | |||
| 'source/modules/juce_audio_utils/juce_audio_utils.cpp', | |||
| 'source/modules/juce_core/juce_core.cpp', | |||
| 'source/modules/juce_cryptography/juce_cryptography.cpp', | |||
| @@ -17,6 +18,7 @@ else | |||
| 'source/modules/juce_audio_basics/juce_audio_basics.cpp', | |||
| 'source/modules/juce_audio_formats/juce_audio_formats.cpp', | |||
| 'source/modules/juce_audio_processors/juce_audio_processors.cpp', | |||
| # 'source/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp', | |||
| 'source/modules/juce_audio_utils/juce_audio_utils.cpp', | |||
| 'source/modules/juce_core/juce_core.cpp', | |||
| 'source/modules/juce_cryptography/juce_cryptography.cpp', | |||
| @@ -35,6 +37,7 @@ juce7_devices_srcs = [ | |||
| ] | |||
| juce7_extra_cpp_args = [ | |||
| # '-DJUCE_PLUGINHOST_LV2=1', | |||
| '-std=gnu++17', | |||
| '-Wno-non-virtual-dtor', | |||
| ] | |||
| @@ -61,6 +64,14 @@ lib_juce7 = static_library('juce7', | |||
| include_directories('.'), | |||
| include_directories('source'), | |||
| include_directories('source/modules'), | |||
| # include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK'), | |||
| # include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/serd'), | |||
| # include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/sord'), | |||
| # include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/sord/src'), | |||
| # include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/sratom'), | |||
| # include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/lilv'), | |||
| # include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/lilv/src'), | |||
| include_directories('source/modules/juce_audio_processors/format_types/VST3_SDK'), | |||
| include_directories('../juced/source/dependancies/ladspa_sdk/src'), | |||
| juce7_extra_include_dirs | |||
| ], | |||
| @@ -366,6 +366,10 @@ endif | |||
| ############################################################################### | |||
| # extra files to install | |||
| if 'syndicate' in get_option('plugins') | |||
| meson.add_install_script('scripts/install-syndicate-scanner.sh') | |||
| endif | |||
| if 'tal-noisemaker' in get_option('plugins') | |||
| extra_lv2_preset_files = [ | |||
| 'TAL-NoiseMaker-Noise4U.lv2/manifest.ttl', | |||
| @@ -119,5 +119,6 @@ option('plugins', | |||
| 'roth-air', | |||
| 'swankyamp', | |||
| # juce7 | |||
| 'syndicate', | |||
| ], | |||
| ) | |||
| @@ -0,0 +1,185 @@ | |||
| diff -U3 -r a/AllCommon/AllUtils.h b/AllCommon/AllUtils.h | |||
| --- a/AllCommon/AllUtils.h 2025-09-15 00:11:06.845287312 +0200 | |||
| +++ b/AllCommon/AllUtils.h 2025-09-15 00:03:25.552521917 +0200 | |||
| @@ -10,23 +10,41 @@ | |||
| inline const char* SCAN_CONFIGURATION_FILE_NAME = "ScanConfiguration.txt"; | |||
| inline const char* CONFIG_FILE_NAME = "Config.json"; | |||
| + static inline juce::File getPluginScanServerBinary(juce::AudioProcessor::WrapperType wrapperType) | |||
| + { | |||
| + juce::File bin(juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory()); | |||
| + switch (wrapperType) | |||
| + { | |||
| + case juce::AudioProcessor::wrapperType_LV2: | |||
| + break; | |||
| + case juce::AudioProcessor::wrapperType_VST: | |||
| + bin = bin.getChildFile("Resources"); | |||
| + break; | |||
| + default: | |||
| + bin = bin.getSiblingFile("Resources"); | |||
| + break; | |||
| + } | |||
| +#if _WIN32 | |||
| + return bin.getChildFile("PluginScanServer.exe"); | |||
| +#else | |||
| + return bin.getChildFile("PluginScanServer"); | |||
| +#endif | |||
| + } | |||
| + | |||
| #ifdef __APPLE__ | |||
| const juce::File DataDirectory(juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("WhiteElephantAudio/Syndicate")); | |||
| const juce::File PluginLogDirectory(juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Logs/WhiteElephantAudio/Syndicate/Syndicate")); | |||
| const juce::File PluginScanServerLogDirectory(juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Logs/WhiteElephantAudio/Syndicate/PluginScanServer")); | |||
| - const juce::File PluginScanServerBinary(juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory().getSiblingFile("Resources").getChildFile("PluginScanServer")); | |||
| #elif _WIN32 | |||
| const juce::File ApplicationDirectory(juce::File::getSpecialLocation(juce::File::userDocumentsDirectory).getChildFile("WhiteElephantAudio/Syndicate")); | |||
| const juce::File DataDirectory(ApplicationDirectory.getChildFile("Data")); | |||
| const juce::File PluginLogDirectory(ApplicationDirectory.getChildFile("Logs/Syndicate")); | |||
| const juce::File PluginScanServerLogDirectory(ApplicationDirectory.getChildFile("Logs/PluginScanServer")); | |||
| - const juce::File PluginScanServerBinary(juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory().getSiblingFile("Resources").getChildFile("PluginScanServer.exe")); | |||
| #elif __linux__ | |||
| const juce::File ApplicationDirectory(juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("WhiteElephantAudio/Syndicate")); | |||
| const juce::File DataDirectory(ApplicationDirectory.getChildFile("Data")); | |||
| const juce::File PluginLogDirectory(ApplicationDirectory.getChildFile("Logs/Syndicate")); | |||
| const juce::File PluginScanServerLogDirectory(ApplicationDirectory.getChildFile("Logs/PluginScanServer")); | |||
| - const juce::File PluginScanServerBinary(juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory().getSiblingFile("Resources").getChildFile("PluginScanServer")); | |||
| #else | |||
| #error Unsupported OS | |||
| #endif | |||
| diff -U3 -r a/PluginCommon/PluginScanning/CustomScanner.hpp b/PluginCommon/PluginScanning/CustomScanner.hpp | |||
| --- a/PluginCommon/PluginScanning/CustomScanner.hpp 2025-09-15 00:11:06.845287312 +0200 | |||
| +++ b/PluginCommon/PluginScanning/CustomScanner.hpp 2025-09-15 00:07:31.353453853 +0200 | |||
| @@ -5,9 +5,9 @@ | |||
| class Superprocess : private juce::ChildProcessCoordinator { | |||
| public: | |||
| - Superprocess() { | |||
| + Superprocess(juce::AudioProcessor::WrapperType wrapperType) { | |||
| juce::Logger::writeToLog("Launching scan"); | |||
| - launchWorkerProcess(Utils::PluginScanServerBinary, Utils::PLUGIN_SCAN_SERVER_UID, 0, 0); | |||
| + launchWorkerProcess(Utils::getPluginScanServerBinary(wrapperType), Utils::PLUGIN_SCAN_SERVER_UID, 0, 0); | |||
| } | |||
| enum class State { | |||
| @@ -64,7 +64,8 @@ | |||
| class CustomPluginScanner : public juce::KnownPluginList::CustomScanner { | |||
| public: | |||
| - CustomPluginScanner() { } | |||
| + CustomPluginScanner(juce::AudioProcessor::WrapperType wrapperType) | |||
| + : _wrapperType(wrapperType) { } | |||
| ~CustomPluginScanner() override { } | |||
| @@ -95,7 +96,7 @@ | |||
| const juce::String& fileOrIdentifier, | |||
| juce::OwnedArray<juce::PluginDescription>& result) { | |||
| if (superprocess == nullptr) { | |||
| - superprocess = std::make_unique<Superprocess>(); | |||
| + superprocess = std::make_unique<Superprocess>(_wrapperType); | |||
| } | |||
| juce::MemoryBlock block; | |||
| @@ -132,6 +133,7 @@ | |||
| } | |||
| } | |||
| + const juce::AudioProcessor::WrapperType _wrapperType; | |||
| std::unique_ptr<Superprocess> superprocess; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomPluginScanner) | |||
| diff -U3 -r a/PluginCommon/PluginScanning/PluginScanClient.cpp b/PluginCommon/PluginScanning/PluginScanClient.cpp | |||
| --- a/PluginCommon/PluginScanning/PluginScanClient.cpp 2025-09-15 00:11:06.845287312 +0200 | |||
| +++ b/PluginCommon/PluginScanning/PluginScanClient.cpp 2025-09-15 00:08:49.852307156 +0200 | |||
| @@ -1,7 +1,8 @@ | |||
| #include "PluginScanClient.h" | |||
| #include "CustomScanner.hpp" | |||
| -PluginScanClient::PluginScanClient() : juce::Thread("Scan Client"), | |||
| +PluginScanClient::PluginScanClient(juce::AudioProcessor::WrapperType wrapperType) : juce::Thread("Scan Client"), | |||
| + _wrapperType(wrapperType), | |||
| _hasAttemptedRestore(false), | |||
| _shouldRestart(false), | |||
| _shouldExit(false), | |||
| @@ -18,7 +19,7 @@ | |||
| _hasAttemptedRestore = true; | |||
| _pluginList = std::make_unique<juce::KnownPluginList>(); | |||
| - _pluginList->setCustomScanner(std::make_unique<CustomPluginScanner>()); | |||
| + _pluginList->setCustomScanner(std::make_unique<CustomPluginScanner>(_wrapperType)); | |||
| const juce::File scannedPluginsFile(Utils::DataDirectory.getChildFile(Utils::SCANNED_PLUGINS_FILE_NAME)); | |||
| @@ -61,7 +62,7 @@ | |||
| } | |||
| // Check the scanner binary exists | |||
| - if (Utils::PluginScanServerBinary.existsAsFile()) { | |||
| + if (Utils::getPluginScanServerBinary(_wrapperType).existsAsFile()) { | |||
| _errorMessage = ""; | |||
| } else { | |||
| juce::Logger::writeToLog("PluginScanServer binary is missing"); | |||
| diff -U3 -r a/PluginCommon/PluginScanning/PluginScanClient.h b/PluginCommon/PluginScanning/PluginScanClient.h | |||
| --- a/PluginCommon/PluginScanning/PluginScanClient.h 2025-09-15 00:11:06.845287312 +0200 | |||
| +++ b/PluginCommon/PluginScanning/PluginScanClient.h 2025-09-15 00:08:24.611389656 +0200 | |||
| @@ -17,7 +17,7 @@ | |||
| public: | |||
| ScanConfiguration config; | |||
| - PluginScanClient(); | |||
| + PluginScanClient(juce::AudioProcessor::WrapperType wrapperType); | |||
| juce::Array<juce::PluginDescription> getPluginTypes() const; | |||
| @@ -64,6 +64,7 @@ | |||
| void changeListenerCallback(juce::ChangeBroadcaster* changed) override; | |||
| private: | |||
| + const juce::AudioProcessor::WrapperType _wrapperType; | |||
| std::unique_ptr<juce::KnownPluginList> _pluginList; | |||
| juce::File _scannedPluginsFile; | |||
| std::vector<juce::MessageListener*> _listeners; | |||
| diff -U3 -r a/Syndicate/PluginProcessor.cpp b/Syndicate/PluginProcessor.cpp | |||
| --- a/Syndicate/PluginProcessor.cpp 2025-09-15 00:11:06.849287455 +0200 | |||
| +++ b/Syndicate/PluginProcessor.cpp 2025-09-15 00:10:12.475310687 +0200 | |||
| @@ -57,6 +57,7 @@ | |||
| WECore::JUCEPlugin::CoreAudioProcessor(BusesProperties().withInput("Input", juce::AudioChannelSet::stereo(), true) | |||
| .withOutput("Output", juce::AudioChannelSet::stereo(), true) | |||
| .withInput("Sidechain", juce::AudioChannelSet::stereo(), true)), | |||
| + pluginScanClient(wrapperType), | |||
| manager({getBusesLayout(), getSampleRate(), getBlockSize()}, | |||
| [&](int id, MODULATION_TYPE type) { return getModulationValueForSource(id, type); }, | |||
| [&](int newLatencySamples) { onLatencyChange(newLatencySamples); }), | |||
| @@ -231,7 +232,7 @@ | |||
| buffer.clear (i, 0, buffer.getNumSamples()); | |||
| // Send tempo and playhead information to the LFOs | |||
| - juce::AudioPlayHead::CurrentPositionInfo mTempoInfo; | |||
| + juce::AudioPlayHead::CurrentPositionInfo mTempoInfo{}; | |||
| getPlayHead()->getCurrentPosition(mTempoInfo); | |||
| // Pass the audio through the splitter (this is also the only safe place to pass the playhead through) | |||
| diff -U3 -r a/Syndicate/UI/ModulationBar/ModulationBar.cpp b/Syndicate/UI/ModulationBar/ModulationBar.cpp | |||
| --- a/Syndicate/UI/ModulationBar/ModulationBar.cpp 2025-09-15 00:11:06.849287455 +0200 | |||
| +++ b/Syndicate/UI/ModulationBar/ModulationBar.cpp 2025-09-14 23:21:28.077252227 +0200 | |||
| @@ -355,6 +355,8 @@ | |||
| return _rndButtons[buttonIndex]->definition; | |||
| } | |||
| } | |||
| + | |||
| + return selectedDefinition; | |||
| } | |||
| void ModulationBar::_attemptToSelectByDefinition(ModulationSourceDefinition definition) { | |||
| diff -U3 -r a/Syndicate/UI/UIUtils.cpp b/Syndicate/UI/UIUtils.cpp | |||
| --- a/Syndicate/UI/UIUtils.cpp 2025-09-15 00:11:06.853287603 +0200 | |||
| +++ b/Syndicate/UI/UIUtils.cpp 2025-09-14 23:15:42.904133494 +0200 | |||
| @@ -189,7 +189,7 @@ | |||
| return lfoColour; | |||
| case MODULATION_TYPE::ENVELOPE: | |||
| return envelopeColour; | |||
| - case MODULATION_TYPE::RANDOM: | |||
| + default: | |||
| return randomColour; | |||
| } | |||
| } | |||
| @@ -5,6 +5,7 @@ if linux_headless | |||
| ] | |||
| else | |||
| plugins = [ | |||
| 'syndicate' | |||
| ] | |||
| endif | |||
| @@ -0,0 +1,85 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| namespace Utils { | |||
| inline const char* PLUGIN_SCAN_SERVER_UID = "pluginScanServer"; | |||
| inline const char* SCANNED_PLUGINS_FILE_NAME = "ScannedPlugins.txt"; | |||
| inline const char* CRASHED_PLUGINS_FILE_NAME = "CrashedPlugins.txt"; | |||
| inline const char* SCAN_CONFIGURATION_FILE_NAME = "ScanConfiguration.txt"; | |||
| inline const char* CONFIG_FILE_NAME = "Config.json"; | |||
| static inline juce::File getPluginScanServerBinary(juce::AudioProcessor::WrapperType wrapperType) | |||
| { | |||
| juce::File bin(juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory()); | |||
| switch (wrapperType) | |||
| { | |||
| case juce::AudioProcessor::wrapperType_LV2: | |||
| break; | |||
| case juce::AudioProcessor::wrapperType_VST: | |||
| bin = bin.getChildFile("Resources"); | |||
| break; | |||
| default: | |||
| bin = bin.getSiblingFile("Resources"); | |||
| break; | |||
| } | |||
| #if _WIN32 | |||
| return bin.getChildFile("PluginScanServer.exe"); | |||
| #else | |||
| return bin.getChildFile("PluginScanServer"); | |||
| #endif | |||
| } | |||
| #ifdef __APPLE__ | |||
| const juce::File DataDirectory(juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("WhiteElephantAudio/Syndicate")); | |||
| const juce::File PluginLogDirectory(juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Logs/WhiteElephantAudio/Syndicate/Syndicate")); | |||
| const juce::File PluginScanServerLogDirectory(juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Logs/WhiteElephantAudio/Syndicate/PluginScanServer")); | |||
| #elif _WIN32 | |||
| const juce::File ApplicationDirectory(juce::File::getSpecialLocation(juce::File::userDocumentsDirectory).getChildFile("WhiteElephantAudio/Syndicate")); | |||
| const juce::File DataDirectory(ApplicationDirectory.getChildFile("Data")); | |||
| const juce::File PluginLogDirectory(ApplicationDirectory.getChildFile("Logs/Syndicate")); | |||
| const juce::File PluginScanServerLogDirectory(ApplicationDirectory.getChildFile("Logs/PluginScanServer")); | |||
| #elif __linux__ | |||
| const juce::File ApplicationDirectory(juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("WhiteElephantAudio/Syndicate")); | |||
| const juce::File DataDirectory(ApplicationDirectory.getChildFile("Data")); | |||
| const juce::File PluginLogDirectory(ApplicationDirectory.getChildFile("Logs/Syndicate")); | |||
| const juce::File PluginScanServerLogDirectory(ApplicationDirectory.getChildFile("Logs/PluginScanServer")); | |||
| #else | |||
| #error Unsupported OS | |||
| #endif | |||
| struct Config { | |||
| bool enableLogFile; | |||
| Config() : enableLogFile(false) { } | |||
| }; | |||
| inline Config LoadConfig() { | |||
| Config config; | |||
| juce::File configFile(DataDirectory.getChildFile(CONFIG_FILE_NAME)); | |||
| if (!configFile.exists()) { | |||
| return config; | |||
| } | |||
| juce::FileInputStream input(configFile); | |||
| if (!input.openedOk()) { | |||
| return config; | |||
| } | |||
| const juce::var json = juce::JSON::parse(input.readEntireStreamAsString()); | |||
| if (!json.isObject()) { | |||
| return config; | |||
| } | |||
| if (json.hasProperty("enableLogFile")) { | |||
| const juce::var& enableLogFile = json["enableLogFile"]; | |||
| if (enableLogFile.isBool()) { | |||
| config.enableLogFile = enableLogFile; | |||
| } | |||
| } | |||
| return config; | |||
| } | |||
| } | |||
| @@ -0,0 +1,62 @@ | |||
| #include "MainLogger.h" | |||
| #include "AllUtils.h" | |||
| namespace { | |||
| juce::String getTimestamp() { | |||
| const juce::Time currentTime(juce::Time::getCurrentTime()); | |||
| juce::String milliseconds(currentTime.getMilliseconds()); | |||
| while (milliseconds.length() < 3) { | |||
| milliseconds = "0" + milliseconds; | |||
| } | |||
| return currentTime.formatted("%Y-%m-%d_%H-%M-%S.") + milliseconds; | |||
| } | |||
| } | |||
| MainLogger::MainLogger(const char* appName, const char* appVersion, const juce::File& logDirectory) { | |||
| // Open log file | |||
| _logFile = logDirectory.getNonexistentChildFile(getTimestamp(), ".txt", true); | |||
| _logFile.create(); | |||
| _logEnvironment(appName, appVersion); | |||
| } | |||
| void MainLogger::logMessage(const juce::String& message) { | |||
| const juce::String outputMessage = getTimestamp() + " : " + message + "\n"; | |||
| juce::FileOutputStream output(_logFile); | |||
| if (output.openedOk()) { | |||
| output.writeText(outputMessage, false, false, "\n"); | |||
| } | |||
| } | |||
| void MainLogger::_logEnvironment(const char* appName, const char* appVersion) { | |||
| juce::FileOutputStream output(_logFile); | |||
| if (output.openedOk()) { | |||
| const juce::String archString( | |||
| #if defined(__x86_64__) || defined(_M_AMD64) | |||
| "x86_64" | |||
| #elif defined(__aarch64__) || defined(_M_ARM64) | |||
| "arm64" | |||
| #else | |||
| #error "Unknown arch" | |||
| #endif | |||
| ); | |||
| const juce::String outputMessage( | |||
| "******************************************************\n" + | |||
| juce::String(appName) + ": " + juce::String(appVersion) + "\n" | |||
| "OS: " + juce::SystemStats::getOperatingSystemName() + "\n" | |||
| "ARCH: " + archString + "\n" | |||
| "CPUs: " + juce::String(juce::SystemStats::getNumPhysicalCpus()) + " (" + juce::String(juce::SystemStats::getNumCpus()) + ")\n" | |||
| "RAM: " + juce::String(juce::SystemStats::getMemorySizeInMegabytes()) + "MB\n" | |||
| "******************************************************\n\n"); | |||
| output.writeText(outputMessage, false, false, "\n"); | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| class MainLogger : public juce::Logger { | |||
| public: | |||
| MainLogger(const char* appName, const char* appVersion, const juce::File& logDirectory); | |||
| virtual ~MainLogger() = default; | |||
| private: | |||
| juce::File _logFile; | |||
| virtual void logMessage(const juce::String& message) override; | |||
| void _logEnvironment(const char* appName, const char* appVersion); | |||
| }; | |||
| @@ -0,0 +1,12 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| class NullLogger : public juce::Logger { | |||
| public: | |||
| NullLogger() = default; | |||
| virtual ~NullLogger() = default; | |||
| private: | |||
| virtual void logMessage(const juce::String& message) override { } | |||
| }; | |||
| @@ -0,0 +1 @@ | |||
| Contains code common to both plugins and the plugin scanning server. | |||
| @@ -0,0 +1,21 @@ | |||
| #pragma once | |||
| #include "JucePluginMain.h" | |||
| // #include "BinaryData.h" | |||
| #if ! DONT_SET_USING_JUCE_NAMESPACE | |||
| // If your code uses a lot of JUCE classes, then this will obviously save you | |||
| // a lot of typing, but can be disabled by setting DONT_SET_USING_JUCE_NAMESPACE. | |||
| using namespace juce; | |||
| #endif | |||
| #if ! JUCE_DONT_DECLARE_PROJECTINFO | |||
| namespace ProjectInfo | |||
| { | |||
| const char* const projectName = "Syndicate"; | |||
| const char* const companyName = "White Elephant Audio"; | |||
| const char* const versionString = "1.5.0"; | |||
| const int versionNumber = 0x10500; | |||
| } | |||
| #endif | |||
| @@ -0,0 +1,28 @@ | |||
| #pragma once | |||
| #define JucePlugin_Name "Syndicate" | |||
| #define JucePlugin_Desc "Hosts your favourite plugins to allow you to create complex parallel chains of effects" | |||
| #define JucePlugin_Manufacturer "White Elephant Audio" | |||
| #define JucePlugin_ManufacturerWebsite "www.whiteelephantaudio.com" | |||
| #define JucePlugin_ManufacturerEmail "info@whiteelephantaudio.com" | |||
| #define JucePlugin_ManufacturerCode 'Wele' | |||
| #define JucePlugin_PluginCode 'Wsyn' | |||
| #define JucePlugin_IsSynth 0 | |||
| #define JucePlugin_WantsMidiInput 0 | |||
| #define JucePlugin_ProducesMidiOutput 0 | |||
| #define JucePlugin_IsMidiEffect 0 | |||
| #define JucePlugin_EditorRequiresKeyboardFocus 0 | |||
| #define JucePlugin_Version 1.5.0 | |||
| #define JucePlugin_VersionCode 0x10500 | |||
| #define JucePlugin_VersionString "1.5.0" | |||
| #define JucePlugin_VSTUniqueID JucePlugin_PluginCode | |||
| #define JucePlugin_VSTCategory kPlugCategEffect | |||
| #define JucePlugin_Vst3Category "Fx" | |||
| #define JucePlugin_AUMainType 'aufx' | |||
| #define JucePlugin_AUSubType JucePlugin_PluginCode | |||
| #define JucePlugin_AUExportPrefix SyndicateAU | |||
| #define JucePlugin_AUExportPrefixQuoted "SyndicateAU" | |||
| #define JucePlugin_AUManufacturerCode JucePlugin_ManufacturerCode | |||
| #define JucePlugin_CFBundleIdentifier com.whiteelephantaudio.Syndicate | |||
| #define JucePlugin_LV2URI "https://whiteelephantaudio.com/plugins/syndicate" | |||
| @@ -0,0 +1,674 @@ | |||
| GNU GENERAL PUBLIC LICENSE | |||
| Version 3, 29 June 2007 | |||
| Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | |||
| Everyone is permitted to copy and distribute verbatim copies | |||
| of this license document, but changing it is not allowed. | |||
| Preamble | |||
| The GNU General Public License is a free, copyleft license for | |||
| software and other kinds of works. | |||
| The licenses for most software and other practical works are designed | |||
| to take away your freedom to share and change the works. By contrast, | |||
| the GNU General Public License is intended to guarantee your freedom to | |||
| share and change all versions of a program--to make sure it remains free | |||
| software for all its users. We, the Free Software Foundation, use the | |||
| GNU General Public License for most of our software; it applies also to | |||
| any other work released this way by its authors. You can apply it to | |||
| your programs, too. | |||
| When we speak of free software, we are referring to freedom, not | |||
| price. Our General Public Licenses are designed to make sure that you | |||
| have the freedom to distribute copies of free software (and charge for | |||
| them if you wish), that you receive source code or can get it if you | |||
| want it, that you can change the software or use pieces of it in new | |||
| free programs, and that you know you can do these things. | |||
| To protect your rights, we need to prevent others from denying you | |||
| these rights or asking you to surrender the rights. Therefore, you have | |||
| certain responsibilities if you distribute copies of the software, or if | |||
| you modify it: responsibilities to respect the freedom of others. | |||
| For example, if you distribute copies of such a program, whether | |||
| gratis or for a fee, you must pass on to the recipients the same | |||
| freedoms that you received. You must make sure that they, too, receive | |||
| or can get the source code. And you must show them these terms so they | |||
| know their rights. | |||
| Developers that use the GNU GPL protect your rights with two steps: | |||
| (1) assert copyright on the software, and (2) offer you this License | |||
| giving you legal permission to copy, distribute and/or modify it. | |||
| For the developers' and authors' protection, the GPL clearly explains | |||
| that there is no warranty for this free software. For both users' and | |||
| authors' sake, the GPL requires that modified versions be marked as | |||
| changed, so that their problems will not be attributed erroneously to | |||
| authors of previous versions. | |||
| Some devices are designed to deny users access to install or run | |||
| modified versions of the software inside them, although the manufacturer | |||
| can do so. This is fundamentally incompatible with the aim of | |||
| protecting users' freedom to change the software. The systematic | |||
| pattern of such abuse occurs in the area of products for individuals to | |||
| use, which is precisely where it is most unacceptable. Therefore, we | |||
| have designed this version of the GPL to prohibit the practice for those | |||
| products. If such problems arise substantially in other domains, we | |||
| stand ready to extend this provision to those domains in future versions | |||
| of the GPL, as needed to protect the freedom of users. | |||
| Finally, every program is threatened constantly by software patents. | |||
| States should not allow patents to restrict development and use of | |||
| software on general-purpose computers, but in those that do, we wish to | |||
| avoid the special danger that patents applied to a free program could | |||
| make it effectively proprietary. To prevent this, the GPL assures that | |||
| patents cannot be used to render the program non-free. | |||
| The precise terms and conditions for copying, distribution and | |||
| modification follow. | |||
| TERMS AND CONDITIONS | |||
| 0. Definitions. | |||
| "This License" refers to version 3 of the GNU General Public License. | |||
| "Copyright" also means copyright-like laws that apply to other kinds of | |||
| works, such as semiconductor masks. | |||
| "The Program" refers to any copyrightable work licensed under this | |||
| License. Each licensee is addressed as "you". "Licensees" and | |||
| "recipients" may be individuals or organizations. | |||
| To "modify" a work means to copy from or adapt all or part of the work | |||
| in a fashion requiring copyright permission, other than the making of an | |||
| exact copy. The resulting work is called a "modified version" of the | |||
| earlier work or a work "based on" the earlier work. | |||
| A "covered work" means either the unmodified Program or a work based | |||
| on the Program. | |||
| To "propagate" a work means to do anything with it that, without | |||
| permission, would make you directly or secondarily liable for | |||
| infringement under applicable copyright law, except executing it on a | |||
| computer or modifying a private copy. Propagation includes copying, | |||
| distribution (with or without modification), making available to the | |||
| public, and in some countries other activities as well. | |||
| To "convey" a work means any kind of propagation that enables other | |||
| parties to make or receive copies. Mere interaction with a user through | |||
| a computer network, with no transfer of a copy, is not conveying. | |||
| An interactive user interface displays "Appropriate Legal Notices" | |||
| to the extent that it includes a convenient and prominently visible | |||
| feature that (1) displays an appropriate copyright notice, and (2) | |||
| tells the user that there is no warranty for the work (except to the | |||
| extent that warranties are provided), that licensees may convey the | |||
| work under this License, and how to view a copy of this License. If | |||
| the interface presents a list of user commands or options, such as a | |||
| menu, a prominent item in the list meets this criterion. | |||
| 1. Source Code. | |||
| The "source code" for a work means the preferred form of the work | |||
| for making modifications to it. "Object code" means any non-source | |||
| form of a work. | |||
| A "Standard Interface" means an interface that either is an official | |||
| standard defined by a recognized standards body, or, in the case of | |||
| interfaces specified for a particular programming language, one that | |||
| is widely used among developers working in that language. | |||
| The "System Libraries" of an executable work include anything, other | |||
| than the work as a whole, that (a) is included in the normal form of | |||
| packaging a Major Component, but which is not part of that Major | |||
| Component, and (b) serves only to enable use of the work with that | |||
| Major Component, or to implement a Standard Interface for which an | |||
| implementation is available to the public in source code form. A | |||
| "Major Component", in this context, means a major essential component | |||
| (kernel, window system, and so on) of the specific operating system | |||
| (if any) on which the executable work runs, or a compiler used to | |||
| produce the work, or an object code interpreter used to run it. | |||
| The "Corresponding Source" for a work in object code form means all | |||
| the source code needed to generate, install, and (for an executable | |||
| work) run the object code and to modify the work, including scripts to | |||
| control those activities. However, it does not include the work's | |||
| System Libraries, or general-purpose tools or generally available free | |||
| programs which are used unmodified in performing those activities but | |||
| which are not part of the work. For example, Corresponding Source | |||
| includes interface definition files associated with source files for | |||
| the work, and the source code for shared libraries and dynamically | |||
| linked subprograms that the work is specifically designed to require, | |||
| such as by intimate data communication or control flow between those | |||
| subprograms and other parts of the work. | |||
| The Corresponding Source need not include anything that users | |||
| can regenerate automatically from other parts of the Corresponding | |||
| Source. | |||
| The Corresponding Source for a work in source code form is that | |||
| same work. | |||
| 2. Basic Permissions. | |||
| All rights granted under this License are granted for the term of | |||
| copyright on the Program, and are irrevocable provided the stated | |||
| conditions are met. This License explicitly affirms your unlimited | |||
| permission to run the unmodified Program. The output from running a | |||
| covered work is covered by this License only if the output, given its | |||
| content, constitutes a covered work. This License acknowledges your | |||
| rights of fair use or other equivalent, as provided by copyright law. | |||
| You may make, run and propagate covered works that you do not | |||
| convey, without conditions so long as your license otherwise remains | |||
| in force. You may convey covered works to others for the sole purpose | |||
| of having them make modifications exclusively for you, or provide you | |||
| with facilities for running those works, provided that you comply with | |||
| the terms of this License in conveying all material for which you do | |||
| not control copyright. Those thus making or running the covered works | |||
| for you must do so exclusively on your behalf, under your direction | |||
| and control, on terms that prohibit them from making any copies of | |||
| your copyrighted material outside their relationship with you. | |||
| Conveying under any other circumstances is permitted solely under | |||
| the conditions stated below. Sublicensing is not allowed; section 10 | |||
| makes it unnecessary. | |||
| 3. Protecting Users' Legal Rights From Anti-Circumvention Law. | |||
| No covered work shall be deemed part of an effective technological | |||
| measure under any applicable law fulfilling obligations under article | |||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | |||
| similar laws prohibiting or restricting circumvention of such | |||
| measures. | |||
| When you convey a covered work, you waive any legal power to forbid | |||
| circumvention of technological measures to the extent such circumvention | |||
| is effected by exercising rights under this License with respect to | |||
| the covered work, and you disclaim any intention to limit operation or | |||
| modification of the work as a means of enforcing, against the work's | |||
| users, your or third parties' legal rights to forbid circumvention of | |||
| technological measures. | |||
| 4. Conveying Verbatim Copies. | |||
| You may convey verbatim copies of the Program's source code as you | |||
| receive it, in any medium, provided that you conspicuously and | |||
| appropriately publish on each copy an appropriate copyright notice; | |||
| keep intact all notices stating that this License and any | |||
| non-permissive terms added in accord with section 7 apply to the code; | |||
| keep intact all notices of the absence of any warranty; and give all | |||
| recipients a copy of this License along with the Program. | |||
| You may charge any price or no price for each copy that you convey, | |||
| and you may offer support or warranty protection for a fee. | |||
| 5. Conveying Modified Source Versions. | |||
| You may convey a work based on the Program, or the modifications to | |||
| produce it from the Program, in the form of source code under the | |||
| terms of section 4, provided that you also meet all of these conditions: | |||
| a) The work must carry prominent notices stating that you modified | |||
| it, and giving a relevant date. | |||
| b) The work must carry prominent notices stating that it is | |||
| released under this License and any conditions added under section | |||
| 7. This requirement modifies the requirement in section 4 to | |||
| "keep intact all notices". | |||
| c) You must license the entire work, as a whole, under this | |||
| License to anyone who comes into possession of a copy. This | |||
| License will therefore apply, along with any applicable section 7 | |||
| additional terms, to the whole of the work, and all its parts, | |||
| regardless of how they are packaged. This License gives no | |||
| permission to license the work in any other way, but it does not | |||
| invalidate such permission if you have separately received it. | |||
| d) If the work has interactive user interfaces, each must display | |||
| Appropriate Legal Notices; however, if the Program has interactive | |||
| interfaces that do not display Appropriate Legal Notices, your | |||
| work need not make them do so. | |||
| A compilation of a covered work with other separate and independent | |||
| works, which are not by their nature extensions of the covered work, | |||
| and which are not combined with it such as to form a larger program, | |||
| in or on a volume of a storage or distribution medium, is called an | |||
| "aggregate" if the compilation and its resulting copyright are not | |||
| used to limit the access or legal rights of the compilation's users | |||
| beyond what the individual works permit. Inclusion of a covered work | |||
| in an aggregate does not cause this License to apply to the other | |||
| parts of the aggregate. | |||
| 6. Conveying Non-Source Forms. | |||
| You may convey a covered work in object code form under the terms | |||
| of sections 4 and 5, provided that you also convey the | |||
| machine-readable Corresponding Source under the terms of this License, | |||
| in one of these ways: | |||
| a) Convey the object code in, or embodied in, a physical product | |||
| (including a physical distribution medium), accompanied by the | |||
| Corresponding Source fixed on a durable physical medium | |||
| customarily used for software interchange. | |||
| b) Convey the object code in, or embodied in, a physical product | |||
| (including a physical distribution medium), accompanied by a | |||
| written offer, valid for at least three years and valid for as | |||
| long as you offer spare parts or customer support for that product | |||
| model, to give anyone who possesses the object code either (1) a | |||
| copy of the Corresponding Source for all the software in the | |||
| product that is covered by this License, on a durable physical | |||
| medium customarily used for software interchange, for a price no | |||
| more than your reasonable cost of physically performing this | |||
| conveying of source, or (2) access to copy the | |||
| Corresponding Source from a network server at no charge. | |||
| c) Convey individual copies of the object code with a copy of the | |||
| written offer to provide the Corresponding Source. This | |||
| alternative is allowed only occasionally and noncommercially, and | |||
| only if you received the object code with such an offer, in accord | |||
| with subsection 6b. | |||
| d) Convey the object code by offering access from a designated | |||
| place (gratis or for a charge), and offer equivalent access to the | |||
| Corresponding Source in the same way through the same place at no | |||
| further charge. You need not require recipients to copy the | |||
| Corresponding Source along with the object code. If the place to | |||
| copy the object code is a network server, the Corresponding Source | |||
| may be on a different server (operated by you or a third party) | |||
| that supports equivalent copying facilities, provided you maintain | |||
| clear directions next to the object code saying where to find the | |||
| Corresponding Source. Regardless of what server hosts the | |||
| Corresponding Source, you remain obligated to ensure that it is | |||
| available for as long as needed to satisfy these requirements. | |||
| e) Convey the object code using peer-to-peer transmission, provided | |||
| you inform other peers where the object code and Corresponding | |||
| Source of the work are being offered to the general public at no | |||
| charge under subsection 6d. | |||
| A separable portion of the object code, whose source code is excluded | |||
| from the Corresponding Source as a System Library, need not be | |||
| included in conveying the object code work. | |||
| A "User Product" is either (1) a "consumer product", which means any | |||
| tangible personal property which is normally used for personal, family, | |||
| or household purposes, or (2) anything designed or sold for incorporation | |||
| into a dwelling. In determining whether a product is a consumer product, | |||
| doubtful cases shall be resolved in favor of coverage. For a particular | |||
| product received by a particular user, "normally used" refers to a | |||
| typical or common use of that class of product, regardless of the status | |||
| of the particular user or of the way in which the particular user | |||
| actually uses, or expects or is expected to use, the product. A product | |||
| is a consumer product regardless of whether the product has substantial | |||
| commercial, industrial or non-consumer uses, unless such uses represent | |||
| the only significant mode of use of the product. | |||
| "Installation Information" for a User Product means any methods, | |||
| procedures, authorization keys, or other information required to install | |||
| and execute modified versions of a covered work in that User Product from | |||
| a modified version of its Corresponding Source. The information must | |||
| suffice to ensure that the continued functioning of the modified object | |||
| code is in no case prevented or interfered with solely because | |||
| modification has been made. | |||
| If you convey an object code work under this section in, or with, or | |||
| specifically for use in, a User Product, and the conveying occurs as | |||
| part of a transaction in which the right of possession and use of the | |||
| User Product is transferred to the recipient in perpetuity or for a | |||
| fixed term (regardless of how the transaction is characterized), the | |||
| Corresponding Source conveyed under this section must be accompanied | |||
| by the Installation Information. But this requirement does not apply | |||
| if neither you nor any third party retains the ability to install | |||
| modified object code on the User Product (for example, the work has | |||
| been installed in ROM). | |||
| The requirement to provide Installation Information does not include a | |||
| requirement to continue to provide support service, warranty, or updates | |||
| for a work that has been modified or installed by the recipient, or for | |||
| the User Product in which it has been modified or installed. Access to a | |||
| network may be denied when the modification itself materially and | |||
| adversely affects the operation of the network or violates the rules and | |||
| protocols for communication across the network. | |||
| Corresponding Source conveyed, and Installation Information provided, | |||
| in accord with this section must be in a format that is publicly | |||
| documented (and with an implementation available to the public in | |||
| source code form), and must require no special password or key for | |||
| unpacking, reading or copying. | |||
| 7. Additional Terms. | |||
| "Additional permissions" are terms that supplement the terms of this | |||
| License by making exceptions from one or more of its conditions. | |||
| Additional permissions that are applicable to the entire Program shall | |||
| be treated as though they were included in this License, to the extent | |||
| that they are valid under applicable law. If additional permissions | |||
| apply only to part of the Program, that part may be used separately | |||
| under those permissions, but the entire Program remains governed by | |||
| this License without regard to the additional permissions. | |||
| When you convey a copy of a covered work, you may at your option | |||
| remove any additional permissions from that copy, or from any part of | |||
| it. (Additional permissions may be written to require their own | |||
| removal in certain cases when you modify the work.) You may place | |||
| additional permissions on material, added by you to a covered work, | |||
| for which you have or can give appropriate copyright permission. | |||
| Notwithstanding any other provision of this License, for material you | |||
| add to a covered work, you may (if authorized by the copyright holders of | |||
| that material) supplement the terms of this License with terms: | |||
| a) Disclaiming warranty or limiting liability differently from the | |||
| terms of sections 15 and 16 of this License; or | |||
| b) Requiring preservation of specified reasonable legal notices or | |||
| author attributions in that material or in the Appropriate Legal | |||
| Notices displayed by works containing it; or | |||
| c) Prohibiting misrepresentation of the origin of that material, or | |||
| requiring that modified versions of such material be marked in | |||
| reasonable ways as different from the original version; or | |||
| d) Limiting the use for publicity purposes of names of licensors or | |||
| authors of the material; or | |||
| e) Declining to grant rights under trademark law for use of some | |||
| trade names, trademarks, or service marks; or | |||
| f) Requiring indemnification of licensors and authors of that | |||
| material by anyone who conveys the material (or modified versions of | |||
| it) with contractual assumptions of liability to the recipient, for | |||
| any liability that these contractual assumptions directly impose on | |||
| those licensors and authors. | |||
| All other non-permissive additional terms are considered "further | |||
| restrictions" within the meaning of section 10. If the Program as you | |||
| received it, or any part of it, contains a notice stating that it is | |||
| governed by this License along with a term that is a further | |||
| restriction, you may remove that term. If a license document contains | |||
| a further restriction but permits relicensing or conveying under this | |||
| License, you may add to a covered work material governed by the terms | |||
| of that license document, provided that the further restriction does | |||
| not survive such relicensing or conveying. | |||
| If you add terms to a covered work in accord with this section, you | |||
| must place, in the relevant source files, a statement of the | |||
| additional terms that apply to those files, or a notice indicating | |||
| where to find the applicable terms. | |||
| Additional terms, permissive or non-permissive, may be stated in the | |||
| form of a separately written license, or stated as exceptions; | |||
| the above requirements apply either way. | |||
| 8. Termination. | |||
| You may not propagate or modify a covered work except as expressly | |||
| provided under this License. Any attempt otherwise to propagate or | |||
| modify it is void, and will automatically terminate your rights under | |||
| this License (including any patent licenses granted under the third | |||
| paragraph of section 11). | |||
| However, if you cease all violation of this License, then your | |||
| license from a particular copyright holder is reinstated (a) | |||
| provisionally, unless and until the copyright holder explicitly and | |||
| finally terminates your license, and (b) permanently, if the copyright | |||
| holder fails to notify you of the violation by some reasonable means | |||
| prior to 60 days after the cessation. | |||
| Moreover, your license from a particular copyright holder is | |||
| reinstated permanently if the copyright holder notifies you of the | |||
| violation by some reasonable means, this is the first time you have | |||
| received notice of violation of this License (for any work) from that | |||
| copyright holder, and you cure the violation prior to 30 days after | |||
| your receipt of the notice. | |||
| Termination of your rights under this section does not terminate the | |||
| licenses of parties who have received copies or rights from you under | |||
| this License. If your rights have been terminated and not permanently | |||
| reinstated, you do not qualify to receive new licenses for the same | |||
| material under section 10. | |||
| 9. Acceptance Not Required for Having Copies. | |||
| You are not required to accept this License in order to receive or | |||
| run a copy of the Program. Ancillary propagation of a covered work | |||
| occurring solely as a consequence of using peer-to-peer transmission | |||
| to receive a copy likewise does not require acceptance. However, | |||
| nothing other than this License grants you permission to propagate or | |||
| modify any covered work. These actions infringe copyright if you do | |||
| not accept this License. Therefore, by modifying or propagating a | |||
| covered work, you indicate your acceptance of this License to do so. | |||
| 10. Automatic Licensing of Downstream Recipients. | |||
| Each time you convey a covered work, the recipient automatically | |||
| receives a license from the original licensors, to run, modify and | |||
| propagate that work, subject to this License. You are not responsible | |||
| for enforcing compliance by third parties with this License. | |||
| An "entity transaction" is a transaction transferring control of an | |||
| organization, or substantially all assets of one, or subdividing an | |||
| organization, or merging organizations. If propagation of a covered | |||
| work results from an entity transaction, each party to that | |||
| transaction who receives a copy of the work also receives whatever | |||
| licenses to the work the party's predecessor in interest had or could | |||
| give under the previous paragraph, plus a right to possession of the | |||
| Corresponding Source of the work from the predecessor in interest, if | |||
| the predecessor has it or can get it with reasonable efforts. | |||
| You may not impose any further restrictions on the exercise of the | |||
| rights granted or affirmed under this License. For example, you may | |||
| not impose a license fee, royalty, or other charge for exercise of | |||
| rights granted under this License, and you may not initiate litigation | |||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | |||
| any patent claim is infringed by making, using, selling, offering for | |||
| sale, or importing the Program or any portion of it. | |||
| 11. Patents. | |||
| A "contributor" is a copyright holder who authorizes use under this | |||
| License of the Program or a work on which the Program is based. The | |||
| work thus licensed is called the contributor's "contributor version". | |||
| A contributor's "essential patent claims" are all patent claims | |||
| owned or controlled by the contributor, whether already acquired or | |||
| hereafter acquired, that would be infringed by some manner, permitted | |||
| by this License, of making, using, or selling its contributor version, | |||
| but do not include claims that would be infringed only as a | |||
| consequence of further modification of the contributor version. For | |||
| purposes of this definition, "control" includes the right to grant | |||
| patent sublicenses in a manner consistent with the requirements of | |||
| this License. | |||
| Each contributor grants you a non-exclusive, worldwide, royalty-free | |||
| patent license under the contributor's essential patent claims, to | |||
| make, use, sell, offer for sale, import and otherwise run, modify and | |||
| propagate the contents of its contributor version. | |||
| In the following three paragraphs, a "patent license" is any express | |||
| agreement or commitment, however denominated, not to enforce a patent | |||
| (such as an express permission to practice a patent or covenant not to | |||
| sue for patent infringement). To "grant" such a patent license to a | |||
| party means to make such an agreement or commitment not to enforce a | |||
| patent against the party. | |||
| If you convey a covered work, knowingly relying on a patent license, | |||
| and the Corresponding Source of the work is not available for anyone | |||
| to copy, free of charge and under the terms of this License, through a | |||
| publicly available network server or other readily accessible means, | |||
| then you must either (1) cause the Corresponding Source to be so | |||
| available, or (2) arrange to deprive yourself of the benefit of the | |||
| patent license for this particular work, or (3) arrange, in a manner | |||
| consistent with the requirements of this License, to extend the patent | |||
| license to downstream recipients. "Knowingly relying" means you have | |||
| actual knowledge that, but for the patent license, your conveying the | |||
| covered work in a country, or your recipient's use of the covered work | |||
| in a country, would infringe one or more identifiable patents in that | |||
| country that you have reason to believe are valid. | |||
| If, pursuant to or in connection with a single transaction or | |||
| arrangement, you convey, or propagate by procuring conveyance of, a | |||
| covered work, and grant a patent license to some of the parties | |||
| receiving the covered work authorizing them to use, propagate, modify | |||
| or convey a specific copy of the covered work, then the patent license | |||
| you grant is automatically extended to all recipients of the covered | |||
| work and works based on it. | |||
| A patent license is "discriminatory" if it does not include within | |||
| the scope of its coverage, prohibits the exercise of, or is | |||
| conditioned on the non-exercise of one or more of the rights that are | |||
| specifically granted under this License. You may not convey a covered | |||
| work if you are a party to an arrangement with a third party that is | |||
| in the business of distributing software, under which you make payment | |||
| to the third party based on the extent of your activity of conveying | |||
| the work, and under which the third party grants, to any of the | |||
| parties who would receive the covered work from you, a discriminatory | |||
| patent license (a) in connection with copies of the covered work | |||
| conveyed by you (or copies made from those copies), or (b) primarily | |||
| for and in connection with specific products or compilations that | |||
| contain the covered work, unless you entered into that arrangement, | |||
| or that patent license was granted, prior to 28 March 2007. | |||
| Nothing in this License shall be construed as excluding or limiting | |||
| any implied license or other defenses to infringement that may | |||
| otherwise be available to you under applicable patent law. | |||
| 12. No Surrender of Others' Freedom. | |||
| If conditions are imposed on you (whether by court order, agreement or | |||
| otherwise) that contradict the conditions of this License, they do not | |||
| excuse you from the conditions of this License. If you cannot convey a | |||
| covered work so as to satisfy simultaneously your obligations under this | |||
| License and any other pertinent obligations, then as a consequence you may | |||
| not convey it at all. For example, if you agree to terms that obligate you | |||
| to collect a royalty for further conveying from those to whom you convey | |||
| the Program, the only way you could satisfy both those terms and this | |||
| License would be to refrain entirely from conveying the Program. | |||
| 13. Use with the GNU Affero General Public License. | |||
| Notwithstanding any other provision of this License, you have | |||
| permission to link or combine any covered work with a work licensed | |||
| under version 3 of the GNU Affero General Public License into a single | |||
| combined work, and to convey the resulting work. The terms of this | |||
| License will continue to apply to the part which is the covered work, | |||
| but the special requirements of the GNU Affero General Public License, | |||
| section 13, concerning interaction through a network will apply to the | |||
| combination as such. | |||
| 14. Revised Versions of this License. | |||
| The Free Software Foundation may publish revised and/or new versions of | |||
| the GNU General Public License from time to time. Such new versions will | |||
| be similar in spirit to the present version, but may differ in detail to | |||
| address new problems or concerns. | |||
| Each version is given a distinguishing version number. If the | |||
| Program specifies that a certain numbered version of the GNU General | |||
| Public License "or any later version" applies to it, you have the | |||
| option of following the terms and conditions either of that numbered | |||
| version or of any later version published by the Free Software | |||
| Foundation. If the Program does not specify a version number of the | |||
| GNU General Public License, you may choose any version ever published | |||
| by the Free Software Foundation. | |||
| If the Program specifies that a proxy can decide which future | |||
| versions of the GNU General Public License can be used, that proxy's | |||
| public statement of acceptance of a version permanently authorizes you | |||
| to choose that version for the Program. | |||
| Later license versions may give you additional or different | |||
| permissions. However, no additional obligations are imposed on any | |||
| author or copyright holder as a result of your choosing to follow a | |||
| later version. | |||
| 15. Disclaimer of Warranty. | |||
| THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | |||
| APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | |||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | |||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | |||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |||
| PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | |||
| IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | |||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | |||
| 16. Limitation of Liability. | |||
| IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | |||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | |||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | |||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | |||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | |||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | |||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | |||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | |||
| SUCH DAMAGES. | |||
| 17. Interpretation of Sections 15 and 16. | |||
| If the disclaimer of warranty and limitation of liability provided | |||
| above cannot be given local legal effect according to their terms, | |||
| reviewing courts shall apply local law that most closely approximates | |||
| an absolute waiver of all civil liability in connection with the | |||
| Program, unless a warranty or assumption of liability accompanies a | |||
| copy of the Program in return for a fee. | |||
| END OF TERMS AND CONDITIONS | |||
| How to Apply These Terms to Your New Programs | |||
| If you develop a new program, and you want it to be of the greatest | |||
| possible use to the public, the best way to achieve this is to make it | |||
| free software which everyone can redistribute and change under these terms. | |||
| To do so, attach the following notices to the program. It is safest | |||
| to attach them to the start of each source file to most effectively | |||
| state the exclusion of warranty; and each file should have at least | |||
| the "copyright" line and a pointer to where the full notice is found. | |||
| <one line to give the program's name and a brief idea of what it does.> | |||
| Copyright (C) <year> <name of author> | |||
| This program is free software: you can redistribute it and/or modify | |||
| it under the terms of the GNU General Public License as published by | |||
| the Free Software Foundation, either version 3 of the License, or | |||
| (at your option) any later version. | |||
| This program is distributed in the hope that it will be useful, | |||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| GNU General Public License for more details. | |||
| You should have received a copy of the GNU General Public License | |||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| Also add information on how to contact you by electronic and paper mail. | |||
| If the program does terminal interaction, make it output a short | |||
| notice like this when it starts in an interactive mode: | |||
| <program> Copyright (C) <year> <name of author> | |||
| This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | |||
| This is free software, and you are welcome to redistribute it | |||
| under certain conditions; type `show c' for details. | |||
| The hypothetical commands `show w' and `show c' should show the appropriate | |||
| parts of the General Public License. Of course, your program's commands | |||
| might be different; for a GUI interface, you would use an "about box". | |||
| You should also get your employer (if you work as a programmer) or school, | |||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | |||
| For more information on this, and how to apply and follow the GNU GPL, see | |||
| <https://www.gnu.org/licenses/>. | |||
| The GNU General Public License does not permit incorporating your program | |||
| into proprietary programs. If your program is a subroutine library, you | |||
| may consider it more useful to permit linking proprietary applications with | |||
| the library. If this is what you want to do, use the GNU Lesser General | |||
| Public License instead of this License. But first, please read | |||
| <https://www.gnu.org/licenses/why-not-lgpl.html>. | |||
| @@ -0,0 +1,77 @@ | |||
| #include "GuestPluginWindow.h" | |||
| namespace { | |||
| const juce::Colour BACKGROUND_COLOUR(0, 0, 0); | |||
| constexpr int TITLE_BAR_BUTTONS { | |||
| juce::DocumentWindow::TitleBarButtons::minimiseButton | | |||
| juce::DocumentWindow::TitleBarButtons::closeButton | |||
| }; | |||
| } | |||
| GuestPluginWindow::GuestPluginWindow(std::function<void()> onCloseCallback, | |||
| std::shared_ptr<juce::AudioPluginInstance> newPlugin, | |||
| std::shared_ptr<PluginEditorBounds> editorBounds) | |||
| : DocumentWindow(newPlugin->getPluginDescription().name, BACKGROUND_COLOUR, TITLE_BAR_BUTTONS), | |||
| plugin(newPlugin), | |||
| _onCloseCallback(onCloseCallback), | |||
| _editorBounds(editorBounds) { | |||
| juce::AudioProcessorEditor* editor = plugin->createEditorIfNeeded(); | |||
| if (editor != nullptr) { | |||
| setContentOwned(editor, true); | |||
| setResizable(editor->isResizable(), true); | |||
| } else { | |||
| juce::Logger::writeToLog("GuestPluginWindow failed to create editor"); | |||
| } | |||
| // Attempt to restore the previous editor bounds | |||
| if (_editorBounds != nullptr && _editorBounds->has_value()) { | |||
| // The user may have unplugged the display they were using or made some other change to | |||
| // their displays since last opening the plugin - we need to make sure the editor bounds | |||
| // are still within one of the displays so that the UI doesn't appear off screen | |||
| // (which is really annoying) | |||
| // Find the display the editor should be on | |||
| const juce::Rectangle<int> nearestDisplayArea = | |||
| juce::Desktop::getInstance().getDisplays().getDisplayForRect(_editorBounds->value().editorBounds)->userArea; | |||
| // If this is different to the one used last time just set the top left corner to 0, 0 | |||
| if (nearestDisplayArea != _editorBounds->value().displayArea) { | |||
| _editorBounds->value().editorBounds.setPosition(0, 0); | |||
| } | |||
| setBounds(_editorBounds->value().editorBounds); | |||
| } | |||
| // Can't use setUsingNativeTitleBar(true) as it prevents some plugin (ie. NI) UIs from loading | |||
| // for some reason | |||
| setVisible(true); | |||
| #if defined(__APPLE__) || defined(_WIN32) | |||
| setAlwaysOnTop(true); | |||
| #elif __linux__ | |||
| // TODO find a way to make this work on Linux | |||
| setAlwaysOnTop(false); | |||
| #else | |||
| #error Unsupported OS | |||
| #endif | |||
| juce::Logger::writeToLog("Created GuestPluginWindow"); | |||
| } | |||
| GuestPluginWindow::~GuestPluginWindow() { | |||
| juce::Logger::writeToLog("Closing GuestPluginWindow"); | |||
| clearContentComponent(); | |||
| } | |||
| void GuestPluginWindow::closeButtonPressed() { | |||
| PluginEditorBoundsContainer newBounds( | |||
| getBounds(), | |||
| juce::Desktop::getInstance().getDisplays().getDisplayForRect(getBounds())->userArea | |||
| ); | |||
| *_editorBounds.get() = newBounds; | |||
| _onCloseCallback(); | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "ChainSlots.hpp" | |||
| class GuestPluginWindow : public juce::DocumentWindow | |||
| { | |||
| public: | |||
| const std::shared_ptr<juce::AudioPluginInstance> plugin; | |||
| GuestPluginWindow(std::function<void()> onCloseCallback, | |||
| std::shared_ptr<juce::AudioPluginInstance> newPlugin, | |||
| std::shared_ptr<PluginEditorBounds> editorBounds); | |||
| ~GuestPluginWindow(); | |||
| void closeButtonPressed() override; | |||
| private: | |||
| std::function<void()> _onCloseCallback; | |||
| std::shared_ptr<PluginEditorBounds> _editorBounds; | |||
| }; | |||
| @@ -0,0 +1,246 @@ | |||
| #include "ConfigurePopover.hpp" | |||
| namespace { | |||
| constexpr int TEXT_HEIGHT {16}; | |||
| juce::Colour stateToLabelColour(bool state) { | |||
| if (state) { | |||
| return UIUtils::highlightColour; | |||
| } | |||
| return UIUtils::highlightColour.withAlpha(0.5f); | |||
| } | |||
| int getHeightForLabelText(juce::String text) { | |||
| const int numLines = juce::StringArray::fromTokens(text, "\n").size(); | |||
| return TEXT_HEIGHT * numLines; | |||
| } | |||
| void removePathFromSearchPaths(juce::FileSearchPath& searchPaths, juce::String path) { | |||
| for (int pathNumber {0}; pathNumber < searchPaths.getNumPaths(); pathNumber++) { | |||
| if (searchPaths[pathNumber].getFullPathName() == path) { | |||
| searchPaths.remove(pathNumber); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| CustomPathsList::CustomPathsList(juce::FileSearchPath& customPaths) : _customPaths(customPaths) { | |||
| rebuild(); | |||
| } | |||
| CustomPathsList::~CustomPathsList() { | |||
| } | |||
| void CustomPathsList::resized() { | |||
| juce::Rectangle<int> availableArea = getLocalBounds(); | |||
| for (auto& label : _customPathsLabels) { | |||
| label->setBounds(availableArea.removeFromTop(TEXT_HEIGHT)); | |||
| } | |||
| } | |||
| void CustomPathsList::mouseDown(const juce::MouseEvent& event) { | |||
| if (!event.mods.isRightButtonDown()) { | |||
| return; | |||
| } | |||
| if (auto label = dynamic_cast<juce::Label*>(event.eventComponent); label != nullptr) { | |||
| removePathFromSearchPaths(_customPaths, label->getText()); | |||
| rebuild(); | |||
| resized(); | |||
| } | |||
| } | |||
| int CustomPathsList::getRequiredHeight() const { | |||
| return TEXT_HEIGHT * _customPathsLabels.size(); | |||
| } | |||
| void CustomPathsList::rebuild() { | |||
| _customPathsLabels.clear(); | |||
| for (int pathNumber {0}; pathNumber < _customPaths.getNumPaths(); pathNumber++) { | |||
| auto thisLabel = std::make_unique<juce::Label>( | |||
| "Path " + juce::String(pathNumber) + " Label", | |||
| _customPaths[pathNumber].getFullPathName() | |||
| ); | |||
| addAndMakeVisible(thisLabel.get()); | |||
| thisLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); | |||
| thisLabel->setJustificationType(juce::Justification::verticallyCentred | juce::Justification::horizontallyCentred); | |||
| thisLabel->setEditable(false, false, false); | |||
| thisLabel->setColour(juce::Label::textColourId, UIUtils::highlightColour); | |||
| thisLabel->addMouseListener(this, false); | |||
| thisLabel->setTooltip("Right click to remove"); | |||
| _customPathsLabels.push_back(std::move(thisLabel)); | |||
| } | |||
| } | |||
| FormatConfigureComponent::FormatConfigureComponent( | |||
| juce::AudioPluginFormat* format, | |||
| bool& defaultPathsEnabled, | |||
| juce::FileSearchPath& customPaths) { | |||
| const juce::String formatName = format->getName(); | |||
| _defaultPathsListLabel.reset(new juce::Label(formatName + " Default Paths List Label", format->getDefaultLocationsToSearch().toStringWithSeparator("\n"))); | |||
| addAndMakeVisible(_defaultPathsListLabel.get()); | |||
| _defaultPathsListLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); | |||
| _defaultPathsListLabel->setJustificationType(juce::Justification::verticallyCentred | juce::Justification::horizontallyCentred); | |||
| _defaultPathsListLabel->setEditable(false, false, false); | |||
| _defaultPathsListLabel->setColour(juce::Label::textColourId, stateToLabelColour(defaultPathsEnabled)); | |||
| _defaultPathsButton.reset(new juce::TextButton(formatName + " Default Paths Button")); | |||
| addAndMakeVisible(_defaultPathsButton.get()); | |||
| _defaultPathsButton->setButtonText(TRANS("Include " + formatName + " Default Paths")); | |||
| _defaultPathsButton->setLookAndFeel(&_toggleButtonLookAndFeel); | |||
| _defaultPathsButton->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); | |||
| _defaultPathsButton->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, UIUtils::highlightColour); | |||
| _defaultPathsButton->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); | |||
| _defaultPathsButton->setToggleState(defaultPathsEnabled, juce::NotificationType::dontSendNotification); | |||
| _defaultPathsButton->onClick = [&]() { | |||
| defaultPathsEnabled = !defaultPathsEnabled; | |||
| _defaultPathsButton->setToggleState(defaultPathsEnabled, juce::NotificationType::dontSendNotification); | |||
| _defaultPathsListLabel->setColour(juce::Label::textColourId, stateToLabelColour(defaultPathsEnabled)); | |||
| }; | |||
| _customPathsListComponent.reset(new CustomPathsList(customPaths)); | |||
| addAndMakeVisible(_customPathsListComponent.get()); | |||
| _customPathsButton.reset(new juce::TextButton(formatName + " Custom Paths Button")); | |||
| addAndMakeVisible(_customPathsButton.get()); | |||
| _customPathsButton->setButtonText(TRANS("Add Custom " + formatName + " Paths")); | |||
| _customPathsButton->setLookAndFeel(&_staticButtonLookAndFeel); | |||
| _customPathsButton->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); | |||
| _customPathsButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); | |||
| _customPathsButton->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); | |||
| _customPathsButton->onClick = [&, formatName]() { | |||
| const int flags {juce::FileBrowserComponent::canSelectDirectories | juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems}; | |||
| _fileChooser.reset(new juce::FileChooser("Add " + formatName + " Directories")); | |||
| #if _WIN32 | |||
| // Workaround on windows to stop the file chooser from appearing behind the plugin selector window | |||
| getTopLevelComponent()->setAlwaysOnTop(false); | |||
| #endif | |||
| _fileChooser->launchAsync(flags, [&](const juce::FileChooser& chooser) { | |||
| for (juce::File selectedDirectory : chooser.getResults()) { | |||
| customPaths.addIfNotAlreadyThere(selectedDirectory); | |||
| } | |||
| customPaths.removeNonExistentPaths(); | |||
| _customPathsListComponent->rebuild(); | |||
| resized(); | |||
| #if _WIN32 | |||
| // Other half of the workaround | |||
| // Restore the always on top state of the plugin selector window after the file chooser has closed | |||
| getTopLevelComponent()->setAlwaysOnTop(true); | |||
| #endif | |||
| }); | |||
| }; | |||
| } | |||
| FormatConfigureComponent::~FormatConfigureComponent() { | |||
| _defaultPathsButton->setLookAndFeel(nullptr); | |||
| _defaultPathsListLabel->setLookAndFeel(nullptr); | |||
| _customPathsButton->setLookAndFeel(nullptr); | |||
| } | |||
| void FormatConfigureComponent::resized() { | |||
| constexpr int BUTTON_WIDTH {180}; | |||
| constexpr int BUTTON_HEIGHT {24}; | |||
| constexpr int MARGIN {5}; | |||
| juce::Rectangle<int> availableArea = getLocalBounds(); | |||
| juce::Rectangle<int> defaultArea = availableArea.removeFromLeft(availableArea.getWidth() / 2).reduced(MARGIN); | |||
| _defaultPathsButton->setBounds(defaultArea.removeFromTop(BUTTON_HEIGHT).withSizeKeepingCentre(BUTTON_WIDTH, BUTTON_HEIGHT)); | |||
| const int defaultPathsHeight {getHeightForLabelText(_defaultPathsListLabel->getText())}; | |||
| defaultArea.removeFromTop(TEXT_HEIGHT); | |||
| _defaultPathsListLabel->setBounds(defaultArea.removeFromTop(defaultPathsHeight)); | |||
| availableArea.reduced(MARGIN); | |||
| _customPathsButton->setBounds(availableArea.removeFromTop(BUTTON_HEIGHT).withSizeKeepingCentre(BUTTON_WIDTH, BUTTON_HEIGHT)); | |||
| const int customPathsHeight {_customPathsListComponent->getRequiredHeight()}; | |||
| availableArea.removeFromTop(TEXT_HEIGHT); | |||
| _customPathsListComponent->setBounds(availableArea.removeFromTop(customPathsHeight)); | |||
| } | |||
| ConfigurePopover::ConfigurePopover(ScanConfiguration& config, std::function<void()> onCloseCallback) : | |||
| _onCloseCallback(onCloseCallback) { | |||
| _vstConfigure.reset(new FormatConfigureComponent( | |||
| &config.vstFormat, | |||
| config.VSTDefaultPaths, | |||
| config.customVSTPaths)); | |||
| addAndMakeVisible(_vstConfigure.get()); | |||
| _vst3Configure.reset(new FormatConfigureComponent( | |||
| &config.vst3Format, | |||
| config.VST3DefaultPaths, | |||
| config.customVST3Paths)); | |||
| addAndMakeVisible(_vst3Configure.get()); | |||
| _okButton.reset(new juce::TextButton("OK button")); | |||
| addAndMakeVisible(_okButton.get()); | |||
| _okButton->setButtonText(TRANS("OK")); | |||
| _okButton->setLookAndFeel(&_buttonLookAndFeel); | |||
| _okButton->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); | |||
| _okButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); | |||
| _okButton->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); | |||
| _okButton->onClick = [&]() { _onCloseCallback(); }; | |||
| _tooltipLabel.reset(new juce::Label("Tooltip Label", "")); | |||
| addAndMakeVisible(_tooltipLabel.get()); | |||
| _tooltipLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); | |||
| _tooltipLabel->setJustificationType(juce::Justification::centred); | |||
| _tooltipLabel->setEditable(false, false, false); | |||
| _tooltipLabel->setColour(juce::Label::textColourId, UIUtils::tooltipColour); | |||
| // Start tooltip label | |||
| _vstConfigure->addMouseListener(this, true); | |||
| _vst3Configure->addMouseListener(this, true); | |||
| } | |||
| ConfigurePopover::~ConfigurePopover() { | |||
| _okButton->setLookAndFeel(nullptr); | |||
| } | |||
| void ConfigurePopover::resized() { | |||
| constexpr int WINDOW_PADDING {20}; | |||
| constexpr int AREA_PADDING {5}; | |||
| juce::Rectangle<int> availableArea = getLocalBounds().reduced(WINDOW_PADDING); | |||
| _tooltipLabel->setBounds(availableArea.removeFromBottom(16)); | |||
| juce::Rectangle<int> okButtonArea = availableArea.removeFromBottom(availableArea.getHeight() / 4); | |||
| _okButton->setBounds(okButtonArea.withSizeKeepingCentre(60, 40)); | |||
| _vstConfigure->setBounds(availableArea.removeFromTop(availableArea.getHeight() / 2).reduced(AREA_PADDING)); | |||
| _vst3Configure->setBounds(availableArea.reduced(AREA_PADDING)); | |||
| } | |||
| void ConfigurePopover::paint(juce::Graphics& g) { | |||
| g.fillAll(juce::Colours::black.withAlpha(0.8f)); | |||
| } | |||
| void ConfigurePopover::mouseEnter(const juce::MouseEvent& event) { | |||
| juce::TooltipClient* tooltipClient = dynamic_cast<juce::TooltipClient*>(event.eventComponent); | |||
| if (tooltipClient != nullptr) { | |||
| const juce::String displayString = tooltipClient->getTooltip(); | |||
| _tooltipLabel->setText(displayString, juce::dontSendNotification); | |||
| } | |||
| } | |||
| void ConfigurePopover::mouseExit(const juce::MouseEvent& /*event*/) { | |||
| if (_tooltipLabel != nullptr) { | |||
| _tooltipLabel->setText("", juce::dontSendNotification); | |||
| } | |||
| } | |||
| @@ -0,0 +1,70 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "UIUtils.h" | |||
| #include "ScanConfiguration.hpp" | |||
| class CustomPathsList : public juce::Component { | |||
| public: | |||
| CustomPathsList(juce::FileSearchPath& customPaths); | |||
| ~CustomPathsList(); | |||
| void resized() override; | |||
| void mouseDown(const juce::MouseEvent& event) override; | |||
| int getRequiredHeight() const; | |||
| void rebuild(); | |||
| private: | |||
| juce::FileSearchPath& _customPaths; | |||
| std::vector<std::unique_ptr<juce::Label>> _customPathsLabels; | |||
| }; | |||
| class FormatConfigureComponent : public juce::Component { | |||
| public: | |||
| FormatConfigureComponent(juce::AudioPluginFormat* format, | |||
| bool& defaultPathsEnabled, | |||
| juce::FileSearchPath& customPaths); | |||
| ~FormatConfigureComponent(); | |||
| void resized() override; | |||
| private: | |||
| UIUtils::ToggleButtonLookAndFeel _toggleButtonLookAndFeel; | |||
| UIUtils::StaticButtonLookAndFeel _staticButtonLookAndFeel; | |||
| std::unique_ptr<juce::TextButton> _defaultPathsButton; | |||
| std::unique_ptr<juce::Label> _defaultPathsListLabel; | |||
| std::unique_ptr<juce::TextButton> _customPathsButton; | |||
| std::unique_ptr<CustomPathsList> _customPathsListComponent; | |||
| std::unique_ptr<juce::FileChooser> _fileChooser; | |||
| }; | |||
| class ConfigurePopover : public juce::Component { | |||
| public: | |||
| ConfigurePopover(ScanConfiguration& config, std::function<void()> onCloseCallback); | |||
| ~ConfigurePopover(); | |||
| void resized() override; | |||
| void paint(juce::Graphics& g) override; | |||
| void mouseEnter(const juce::MouseEvent &event) override; | |||
| void mouseExit(const juce::MouseEvent &event) override; | |||
| private: | |||
| std::function<void()> _onCloseCallback; | |||
| UIUtils::StaticButtonLookAndFeel _buttonLookAndFeel; | |||
| std::unique_ptr<FormatConfigureComponent> _vstConfigure; | |||
| std::unique_ptr<FormatConfigureComponent> _vst3Configure; | |||
| std::unique_ptr<juce::TextButton> _okButton; | |||
| std::unique_ptr<juce::Label> _tooltipLabel; | |||
| }; | |||
| @@ -0,0 +1,140 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "AllUtils.h" | |||
| class Superprocess : private juce::ChildProcessCoordinator { | |||
| public: | |||
| Superprocess(juce::AudioProcessor::WrapperType wrapperType) { | |||
| juce::Logger::writeToLog("Launching scan"); | |||
| launchWorkerProcess(Utils::getPluginScanServerBinary(wrapperType), Utils::PLUGIN_SCAN_SERVER_UID, 0, 0); | |||
| } | |||
| enum class State { | |||
| timeout, | |||
| gotResult, | |||
| connectionLost, | |||
| }; | |||
| struct Response { | |||
| State state; | |||
| std::unique_ptr<juce::XmlElement> xml; | |||
| }; | |||
| Response getResponse() { | |||
| std::unique_lock<std::mutex> lock { mutex }; | |||
| if (! condvar.wait_for (lock, std::chrono::milliseconds { 50 }, [&] { return gotResult || connectionLost; })) { | |||
| return { State::timeout, nullptr }; | |||
| } | |||
| const auto state = connectionLost ? State::connectionLost : State::gotResult; | |||
| connectionLost = false; | |||
| gotResult = false; | |||
| return {state, std::move (pluginDescription)}; | |||
| } | |||
| using ChildProcessCoordinator::sendMessageToWorker; | |||
| private: | |||
| void handleMessageFromWorker(const juce::MemoryBlock& mb) override { | |||
| const std::lock_guard<std::mutex> lock { mutex }; | |||
| pluginDescription = parseXML (mb.toString()); | |||
| gotResult = true; | |||
| condvar.notify_one(); | |||
| } | |||
| void handleConnectionLost() override { | |||
| juce::Logger::writeToLog("Connection lost"); | |||
| const std::lock_guard<std::mutex> lock { mutex }; | |||
| connectionLost = true; | |||
| condvar.notify_one(); | |||
| } | |||
| std::mutex mutex; | |||
| std::condition_variable condvar; | |||
| std::unique_ptr<juce::XmlElement> pluginDescription; | |||
| bool connectionLost = false; | |||
| bool gotResult = false; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Superprocess) | |||
| }; | |||
| class CustomPluginScanner : public juce::KnownPluginList::CustomScanner { | |||
| public: | |||
| CustomPluginScanner(juce::AudioProcessor::WrapperType wrapperType) | |||
| : _wrapperType(wrapperType) { } | |||
| ~CustomPluginScanner() override { } | |||
| bool findPluginTypesFor(juce::AudioPluginFormat& format, | |||
| juce::OwnedArray<juce::PluginDescription>& result, | |||
| const juce::String& fileOrIdentifier) override { | |||
| if (addPluginDescriptions(format.getName(), fileOrIdentifier, result)) { | |||
| return true; | |||
| } | |||
| superprocess = nullptr; | |||
| return false; | |||
| } | |||
| void scanFinished() override { | |||
| superprocess = nullptr; | |||
| } | |||
| private: | |||
| /* Scans for a plugin with format 'formatName' and ID 'fileOrIdentifier' using a subprocess, | |||
| and adds discovered plugin descriptions to 'result'. | |||
| Returns true on success. | |||
| Failure indicates that the subprocess is unrecoverable and should be terminated. | |||
| */ | |||
| bool addPluginDescriptions(const juce::String& formatName, | |||
| const juce::String& fileOrIdentifier, | |||
| juce::OwnedArray<juce::PluginDescription>& result) { | |||
| if (superprocess == nullptr) { | |||
| superprocess = std::make_unique<Superprocess>(_wrapperType); | |||
| } | |||
| juce::MemoryBlock block; | |||
| juce::MemoryOutputStream stream { block, true }; | |||
| stream.writeString(formatName); | |||
| stream.writeString(fileOrIdentifier); | |||
| if (!superprocess->sendMessageToWorker(block)) { | |||
| return false; | |||
| } | |||
| for (;;) { | |||
| if (shouldExit()) { | |||
| return true; | |||
| } | |||
| const auto response = superprocess->getResponse(); | |||
| if (response.state == Superprocess::State::timeout) { | |||
| continue; | |||
| } | |||
| if (response.xml != nullptr) { | |||
| for (const auto* item : response.xml->getChildIterator()) { | |||
| auto desc = std::make_unique<juce::PluginDescription>(); | |||
| if (desc->loadFromXml (*item)) { | |||
| result.add (std::move (desc)); | |||
| } | |||
| } | |||
| } | |||
| return (response.state == Superprocess::State::gotResult); | |||
| } | |||
| } | |||
| const juce::AudioProcessor::WrapperType _wrapperType; | |||
| std::unique_ptr<Superprocess> superprocess; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomPluginScanner) | |||
| }; | |||
| @@ -0,0 +1,303 @@ | |||
| #include "PluginScanClient.h" | |||
| #include "CustomScanner.hpp" | |||
| PluginScanClient::PluginScanClient(juce::AudioProcessor::WrapperType wrapperType) : juce::Thread("Scan Client"), | |||
| _wrapperType(wrapperType), | |||
| _hasAttemptedRestore(false), | |||
| _shouldRestart(false), | |||
| _shouldExit(false), | |||
| _isClearOnlyScan(false) { | |||
| _scannedPluginsFile = Utils::DataDirectory.getChildFile(Utils::SCANNED_PLUGINS_FILE_NAME); | |||
| _scannedPluginsFile.create(); | |||
| } | |||
| juce::Array<juce::PluginDescription> PluginScanClient::getPluginTypes() const { | |||
| return _pluginList->getTypes(); | |||
| } | |||
| void PluginScanClient::restore() { | |||
| _hasAttemptedRestore = true; | |||
| _pluginList = std::make_unique<juce::KnownPluginList>(); | |||
| _pluginList->setCustomScanner(std::make_unique<CustomPluginScanner>(_wrapperType)); | |||
| const juce::File scannedPluginsFile(Utils::DataDirectory.getChildFile(Utils::SCANNED_PLUGINS_FILE_NAME)); | |||
| if (scannedPluginsFile.existsAsFile()) { | |||
| std::unique_ptr<juce::XmlElement> pluginsXml = juce::parseXML(scannedPluginsFile); | |||
| if (pluginsXml.get() != nullptr) { | |||
| _pluginList->recreateFromXml(*pluginsXml); | |||
| } | |||
| // Notify the listeners | |||
| { | |||
| std::scoped_lock lock(_listenersMutex); | |||
| for (juce::MessageListener* listener : _listeners) { | |||
| _notifyListener(listener); | |||
| } | |||
| } | |||
| juce::Logger::writeToLog("Restored " + juce::String(_pluginList->getNumTypes()) + " plugins from disk"); | |||
| } else { | |||
| juce::Logger::writeToLog("Nothing to restore plugins from"); | |||
| } | |||
| } | |||
| void PluginScanClient::startScan() { | |||
| if (_state == ScanState::STARTING) { | |||
| juce::Logger::writeToLog("Can't start scan - already starting"); | |||
| return; | |||
| } | |||
| if (_state == ScanState::RUNNING) { | |||
| juce::Logger::writeToLog("Can't start scan - already running"); | |||
| return; | |||
| } | |||
| if (_state == ScanState::STOPPING) { | |||
| juce::Logger::writeToLog("Can't stop scan - is currently stopping"); | |||
| return; | |||
| } | |||
| // Check the scanner binary exists | |||
| if (Utils::getPluginScanServerBinary(_wrapperType).existsAsFile()) { | |||
| _errorMessage = ""; | |||
| } else { | |||
| juce::Logger::writeToLog("PluginScanServer binary is missing"); | |||
| _errorMessage = "Unable to find plugin scanner - please reinstall"; | |||
| // Notify the listeners | |||
| _notifyAllListeners(); | |||
| return; | |||
| } | |||
| _state = ScanState::STARTING; | |||
| startThread(); | |||
| } | |||
| void PluginScanClient::stopScan() { | |||
| if (_state == ScanState::STOPPED) { | |||
| juce::Logger::writeToLog("Can't stop scan - already stopped"); | |||
| return; | |||
| } | |||
| if (_state == ScanState::STARTING) { | |||
| juce::Logger::writeToLog("Can't stop scan - is currently starting"); | |||
| return; | |||
| } | |||
| if (_state == ScanState::STOPPING) { | |||
| juce::Logger::writeToLog("Can't stop scan - already stopping"); | |||
| return; | |||
| } | |||
| // Only do this if we're currently scanning | |||
| _state = ScanState::STOPPING; | |||
| _shouldExit = true; | |||
| juce::Logger::writeToLog("Stopping plugin scan server"); | |||
| } | |||
| void PluginScanClient::clearMissingPlugins() { | |||
| if (_state == ScanState::STARTING) { | |||
| juce::Logger::writeToLog("Can't clear missing plugins - already starting"); | |||
| return; | |||
| } | |||
| if (_state == ScanState::RUNNING) { | |||
| juce::Logger::writeToLog("Can't clear missing plugins - already running"); | |||
| return; | |||
| } | |||
| if (_state == ScanState::STOPPING) { | |||
| juce::Logger::writeToLog("Can't clear missing plugins - is currently stopping"); | |||
| return; | |||
| } | |||
| _state = ScanState::STARTING; | |||
| _isClearOnlyScan = true; | |||
| startThread(); | |||
| } | |||
| void PluginScanClient::rescanAllPlugins() { | |||
| juce::Logger::writeToLog("Attempting rescan of all plugins"); | |||
| if (_state == ScanState::STOPPED) { | |||
| juce::Logger::writeToLog("Deleting all plugin data files"); | |||
| Utils::DataDirectory.getChildFile(Utils::SCANNED_PLUGINS_FILE_NAME).deleteFile(); | |||
| Utils::DataDirectory.getChildFile(Utils::CRASHED_PLUGINS_FILE_NAME).deleteFile(); | |||
| _hasAttemptedRestore = false; | |||
| startScan(); | |||
| } else { | |||
| juce::Logger::writeToLog("Couldn't delete plugin data files, scan is still running"); | |||
| } | |||
| } | |||
| void PluginScanClient::rescanCrashedPlugins() { | |||
| juce::Logger::writeToLog("Attempting rescan of crashed plugins"); | |||
| if (_state == ScanState::STOPPED) { | |||
| juce::Logger::writeToLog("Deleting crashed plugins file"); | |||
| Utils::DataDirectory.getChildFile(Utils::CRASHED_PLUGINS_FILE_NAME).deleteFile(); | |||
| _hasAttemptedRestore = false; | |||
| startScan(); | |||
| } else { | |||
| juce::Logger::writeToLog("Couldn't delete crashed plugins file, scan is still running"); | |||
| } | |||
| } | |||
| void PluginScanClient::addListener(juce::MessageListener* listener) { | |||
| if (listener != nullptr) { | |||
| std::scoped_lock lock(_listenersMutex); | |||
| if (std::find(_listeners.begin(), _listeners.end(), listener) == _listeners.end()) { | |||
| _listeners.push_back(listener); | |||
| } | |||
| // Notify the listener of the current state | |||
| _notifyListener(listener); | |||
| } | |||
| } | |||
| void PluginScanClient::removeListener(juce::MessageListener* listener) { | |||
| if (listener != nullptr) { | |||
| std::scoped_lock lock(_listenersMutex); | |||
| _listeners.erase(std::remove(_listeners.begin(), _listeners.end(), listener), _listeners.end()); | |||
| } | |||
| } | |||
| void PluginScanClient::run() { | |||
| _shouldExit = false; | |||
| _state = ScanState::RUNNING; | |||
| // We need to force an update after changing state | |||
| _notifyAllListeners(); | |||
| if (!_hasAttemptedRestore) { | |||
| restore(); | |||
| } | |||
| // Just in case the config has been changed via another plugin instance, restore it here | |||
| config.restoreFromXml(); | |||
| { | |||
| const juce::MessageManagerLock mml(juce::Thread::getCurrentThread()); | |||
| if (mml.lockWasGained()) { | |||
| _pluginList->addChangeListener(this); | |||
| } | |||
| } | |||
| if (_isClearOnlyScan) { | |||
| auto checkPluginsForFormat = [](juce::KnownPluginList* pluginList, juce::AudioPluginFormat& format) { | |||
| for (juce::PluginDescription plugin : pluginList->getTypesForFormat(format)) { | |||
| if (!format.doesPluginStillExist(plugin)) { | |||
| juce::Logger::writeToLog("[" + plugin.name + "] is missing, removing from list"); | |||
| pluginList->removeType(plugin); | |||
| } | |||
| } | |||
| }; | |||
| #ifdef __APPLE__ | |||
| checkPluginsForFormat(_pluginList.get(), config.auFormat); | |||
| #endif | |||
| checkPluginsForFormat(_pluginList.get(), config.vstFormat); | |||
| checkPluginsForFormat(_pluginList.get(), config.vst3Format); | |||
| } else { | |||
| #ifdef __APPLE__ | |||
| _scanForFormat(config.auFormat, config.getAUPaths()); | |||
| #endif | |||
| _scanForFormat(config.vstFormat, config.getVSTPaths()); | |||
| _scanForFormat(config.vst3Format, config.getVST3Paths()); | |||
| } | |||
| { | |||
| const juce::MessageManagerLock mml(juce::Thread::getCurrentThread()); | |||
| if (mml.lockWasGained()) { | |||
| _pluginList->removeChangeListener(this); | |||
| } | |||
| } | |||
| _isClearOnlyScan = false; | |||
| _state = ScanState::STOPPED; | |||
| // We need to force an update after changing state | |||
| _notifyAllListeners(); | |||
| juce::Logger::writeToLog("All plugin scan jobs finished"); | |||
| } | |||
| void PluginScanClient::_scanForFormat(juce::AudioPluginFormat& format, juce::FileSearchPath searchPaths) { | |||
| if (_shouldExit) { | |||
| return; | |||
| } | |||
| juce::Logger::writeToLog("[" + format.getName() + "] Scanning with paths: " + searchPaths.toString()); | |||
| juce::File deadMansPedalFile = Utils::DataDirectory.getChildFile(Utils::CRASHED_PLUGINS_FILE_NAME); | |||
| deadMansPedalFile.create(); | |||
| juce::PluginDirectoryScanner scanner(*_pluginList.get(), | |||
| format, | |||
| searchPaths, | |||
| true, | |||
| deadMansPedalFile, | |||
| true); | |||
| bool isFinished {false}; | |||
| while (!_shouldExit && !isFinished) { | |||
| // Prevent the plugin scanning itself or a plugin that previously stalled a scan | |||
| if (scanner.getNextPluginFileThatWillBeScanned() == "Syndicate") { | |||
| if (!scanner.skipNextFile()) { | |||
| return; | |||
| } | |||
| } | |||
| // Scan the plugin | |||
| juce::Logger::writeToLog("[" + format.getName() + "] plugin #" + juce::String(_pluginList->getNumTypes()) + ": " + scanner.getNextPluginFileThatWillBeScanned()); | |||
| juce::String currentPluginName; | |||
| isFinished = !scanner.scanNextFile(true, currentPluginName); | |||
| } | |||
| } | |||
| void PluginScanClient::changeListenerCallback(juce::ChangeBroadcaster* changed) { | |||
| if (changed == _pluginList.get()) { | |||
| std::unique_ptr<juce::XmlElement> pluginsXml = std::unique_ptr<juce::XmlElement>(_pluginList->createXml()); | |||
| if (pluginsXml.get() != nullptr) { | |||
| // Delete and recreate the file so that it's empty | |||
| _scannedPluginsFile.deleteFile(); | |||
| _scannedPluginsFile.create(); | |||
| // Write the Xml to the file | |||
| juce::FileOutputStream output(_scannedPluginsFile); | |||
| if (output.openedOk()) { | |||
| pluginsXml->writeTo(output); | |||
| } | |||
| } | |||
| _notifyAllListeners(); | |||
| } | |||
| } | |||
| void PluginScanClient::_notifyAllListeners() { | |||
| std::scoped_lock lock(_listenersMutex); | |||
| for (juce::MessageListener* listener : _listeners) { | |||
| _notifyListener(listener); | |||
| } | |||
| } | |||
| void PluginScanClient::_notifyListener(juce::MessageListener* listener) { | |||
| listener->postMessage(new PluginScanStatusMessage( | |||
| _pluginList->getNumTypes(), _state == ScanState::RUNNING, _errorMessage)); | |||
| } | |||
| @@ -0,0 +1,96 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "AllUtils.h" | |||
| #include "PluginScanStatusMessage.h" | |||
| #include "ScanConfiguration.hpp" | |||
| enum class ScanState { | |||
| STOPPED, | |||
| STARTING, | |||
| RUNNING, | |||
| STOPPING | |||
| }; | |||
| class PluginScanClient : public juce::ChangeListener, | |||
| public juce::Thread { | |||
| public: | |||
| ScanConfiguration config; | |||
| PluginScanClient(juce::AudioProcessor::WrapperType wrapperType); | |||
| juce::Array<juce::PluginDescription> getPluginTypes() const; | |||
| /** | |||
| * Called to update the internal list of plugins to match what is on disk. Public so that a | |||
| * restore can be done without needing to start a scan. | |||
| */ | |||
| void restore(); | |||
| /** | |||
| * Called when the user starts a scan. | |||
| */ | |||
| void startScan(); | |||
| /** | |||
| * Called when the user has initiated the stop. | |||
| */ | |||
| void stopScan(); | |||
| /** | |||
| * Called to clear plugins that may be uninstalled or missing. | |||
| */ | |||
| void clearMissingPlugins(); | |||
| /** | |||
| * Called when the user has requested a full rescan. | |||
| */ | |||
| void rescanAllPlugins(); | |||
| /** | |||
| * Called when the user has requested a rescan of crashed plugins | |||
| */ | |||
| void rescanCrashedPlugins(); | |||
| void addListener(juce::MessageListener* listener); | |||
| void removeListener(juce::MessageListener* listener); | |||
| /** | |||
| * Performs actions as needed - don't call this manually | |||
| */ | |||
| void run() override; | |||
| void changeListenerCallback(juce::ChangeBroadcaster* changed) override; | |||
| private: | |||
| const juce::AudioProcessor::WrapperType _wrapperType; | |||
| std::unique_ptr<juce::KnownPluginList> _pluginList; | |||
| juce::File _scannedPluginsFile; | |||
| std::vector<juce::MessageListener*> _listeners; | |||
| std::mutex _listenersMutex; | |||
| // True if an attempt has been made to restore from previous scan (whether successful or not) | |||
| bool _hasAttemptedRestore; | |||
| // True if the scan process should be restarted in the event that it exits | |||
| bool _shouldRestart; | |||
| std::atomic<bool> _shouldExit; | |||
| ScanState _state; | |||
| juce::String _errorMessage; | |||
| bool _isClearOnlyScan; | |||
| void _notifyAllListeners(); | |||
| void _notifyListener(juce::MessageListener* listener); | |||
| void _onConnectionLost(); | |||
| void _readScannerFilesForUpdates(); | |||
| void _scanForFormat(juce::AudioPluginFormat& format, juce::FileSearchPath searchPaths); | |||
| }; | |||
| @@ -0,0 +1,210 @@ | |||
| #include "PluginScanStatusMessage.h" | |||
| #include "PluginScanStatusBar.h" | |||
| PluginScanStatusBar::PluginScanStatusBar(PluginScanClient& pluginScanClient, | |||
| const SelectorComponentStyle& style) : | |||
| _pluginScanClient(pluginScanClient) { | |||
| statusLbl.reset(new juce::Label("Status Label", juce::String())); | |||
| addAndMakeVisible(statusLbl.get()); | |||
| statusLbl->setFont(juce::Font (15.00f, juce::Font::plain).withTypefaceStyle ("Regular")); | |||
| statusLbl->setJustificationType(juce::Justification::centredLeft); | |||
| statusLbl->setEditable(false, false, false); | |||
| statusLbl->setColour(juce::Label::textColourId, style.highlightColour); | |||
| auto styleButton = [&style](std::unique_ptr<juce::TextButton>& button) { | |||
| button->setLookAndFeel(style.scanButtonLookAndFeel.get()); | |||
| button->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, style.buttonBackgroundColour); | |||
| button->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, style.highlightColour); | |||
| button->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, style.disabledColour); | |||
| }; | |||
| startScanBtn.reset(new juce::TextButton("Start Scan Button")); | |||
| addAndMakeVisible(startScanBtn.get()); | |||
| startScanBtn->setButtonText(TRANS("Start Scan")); | |||
| startScanBtn->addListener(this); | |||
| styleButton(startScanBtn); | |||
| stopScanBtn.reset(new juce::TextButton("Stop Scan Button")); | |||
| addAndMakeVisible(stopScanBtn.get()); | |||
| stopScanBtn->setButtonText(TRANS("Stop Scan")); | |||
| stopScanBtn->addListener(this); | |||
| styleButton(stopScanBtn); | |||
| rescanAllBtn.reset(new juce::TextButton("Rescan All Button")); | |||
| addAndMakeVisible(rescanAllBtn.get()); | |||
| rescanAllBtn->setButtonText(TRANS("Rescan All Plugins")); | |||
| rescanAllBtn->addListener(this); | |||
| styleButton(rescanAllBtn); | |||
| rescanCrashedBtn.reset(new juce::TextButton("Rescan Crashed Button")); | |||
| addAndMakeVisible(rescanCrashedBtn.get()); | |||
| rescanCrashedBtn->setButtonText(TRANS("Rescan Crashed Plugins")); | |||
| rescanCrashedBtn->addListener(this); | |||
| styleButton(rescanCrashedBtn); | |||
| viewCrashedBtn.reset(new juce::TextButton("View Crashed Button")); | |||
| addAndMakeVisible(viewCrashedBtn.get()); | |||
| viewCrashedBtn->setButtonText(TRANS("View Crashed Plugins")); | |||
| viewCrashedBtn->addListener(this); | |||
| styleButton(viewCrashedBtn); | |||
| configureBtn.reset(new juce::TextButton("Configure Button")); | |||
| addAndMakeVisible(configureBtn.get()); | |||
| configureBtn->setButtonText(TRANS("Configure")); | |||
| configureBtn->addListener(this); | |||
| styleButton(configureBtn); | |||
| _pluginScanClient.addListener(this); | |||
| } | |||
| PluginScanStatusBar::~PluginScanStatusBar() { | |||
| _pluginScanClient.stopScan(); | |||
| _pluginScanClient.removeListener(this); | |||
| statusLbl = nullptr; | |||
| startScanBtn = nullptr; | |||
| stopScanBtn = nullptr; | |||
| rescanAllBtn = nullptr; | |||
| rescanCrashedBtn = nullptr; | |||
| viewCrashedBtn = nullptr; | |||
| configureBtn = nullptr; | |||
| crashedPluginsPopover = nullptr; | |||
| } | |||
| void PluginScanStatusBar::resized() { | |||
| const int buttonsTotalWidth { | |||
| getWidth() < MAX_BUTTONS_WIDTH + MIN_STATUS_WIDTH ? getWidth() - MIN_STATUS_WIDTH : MAX_BUTTONS_WIDTH | |||
| }; | |||
| if (crashedPluginsPopover != nullptr) { | |||
| crashedPluginsPopover->setBounds(getParentComponent()->getLocalBounds()); | |||
| } | |||
| if (configurePopover != nullptr) { | |||
| configurePopover->setBounds(getParentComponent()->getLocalBounds()); | |||
| } | |||
| juce::Rectangle<int> availableArea = getLocalBounds(); | |||
| juce::FlexBox flexBox; | |||
| flexBox.flexDirection = juce::FlexBox::Direction::row; | |||
| flexBox.flexWrap = juce::FlexBox::Wrap::wrap; | |||
| flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; | |||
| flexBox.alignContent = juce::FlexBox::AlignContent::center; | |||
| const juce::FlexItem::Margin margin(0, SPACER_WIDTH, 0, 0); | |||
| flexBox.items.add(juce::FlexItem(*startScanBtn.get()).withMinWidth(NARROW_BUTTON_WIDTH).withMinHeight(ROW_HEIGHT).withMargin(margin)); | |||
| flexBox.items.add(juce::FlexItem(*stopScanBtn.get()).withMinWidth(NARROW_BUTTON_WIDTH).withMinHeight(ROW_HEIGHT).withMargin(margin)); | |||
| flexBox.items.add(juce::FlexItem(*rescanAllBtn.get()).withMinWidth(WIDE_BUTTON_WIDTH).withMinHeight(ROW_HEIGHT).withMargin(margin)); | |||
| flexBox.items.add(juce::FlexItem(*rescanCrashedBtn.get()).withMinWidth(WIDE_BUTTON_WIDTH).withMinHeight(ROW_HEIGHT).withMargin(margin)); | |||
| flexBox.items.add(juce::FlexItem(*viewCrashedBtn.get()).withMinWidth(WIDE_BUTTON_WIDTH).withMinHeight(ROW_HEIGHT).withMargin(margin)); | |||
| flexBox.items.add(juce::FlexItem(*configureBtn.get()).withMinWidth(NARROW_BUTTON_WIDTH).withMinHeight(ROW_HEIGHT)); | |||
| flexBox.performLayout(availableArea.removeFromRight(buttonsTotalWidth).toFloat()); | |||
| availableArea.removeFromRight(SPACER_WIDTH); | |||
| statusLbl->setBounds(availableArea); | |||
| } | |||
| void PluginScanStatusBar::buttonClicked(juce::Button* buttonThatWasClicked) { | |||
| if (buttonThatWasClicked == startScanBtn.get()) { | |||
| _pluginScanClient.startScan(); | |||
| } else if (buttonThatWasClicked == stopScanBtn.get()) { | |||
| _pluginScanClient.stopScan(); | |||
| } else if (buttonThatWasClicked == rescanAllBtn.get()) { | |||
| _pluginScanClient.rescanAllPlugins(); | |||
| } else if (buttonThatWasClicked == rescanCrashedBtn.get()) { | |||
| _pluginScanClient.rescanCrashedPlugins(); | |||
| } else if (buttonThatWasClicked == viewCrashedBtn.get()) { | |||
| _createCrashedPluginsDialogue(); | |||
| } else if (buttonThatWasClicked == configureBtn.get()) { | |||
| _createConfigureDialogue(); | |||
| } | |||
| } | |||
| void PluginScanStatusBar::handleMessage(const juce::Message& message) { | |||
| const PluginScanStatusMessage* statusMessage { | |||
| dynamic_cast<const PluginScanStatusMessage*>(&message) | |||
| }; | |||
| if (statusMessage != nullptr) { | |||
| // Set the status text | |||
| juce::String statusString; | |||
| const juce::String numPlugins(statusMessage->numPluginsScanned); | |||
| if (statusMessage->errorText != "") { | |||
| statusString = statusMessage->errorText; | |||
| } else if (statusMessage->isScanRunning) { | |||
| // Scan currently running | |||
| statusString = "Found " + numPlugins + " plugins, scanning..."; | |||
| } else if (statusMessage->numPluginsScanned == 0) { | |||
| // Couldn't restore any plugins, no scan currently running | |||
| statusString = "No plugins available - click start scan to begin"; | |||
| } else { | |||
| // Successfully restored plugins, no scan currently running | |||
| statusString = "Found " + numPlugins + " plugins"; | |||
| } | |||
| _updateButtonState(statusMessage->isScanRunning); | |||
| statusLbl->setText(statusString, juce::dontSendNotification); | |||
| } | |||
| } | |||
| void PluginScanStatusBar::_updateButtonState(bool isScanRunning) { | |||
| if (isScanRunning) { | |||
| startScanBtn->setEnabled(false); | |||
| stopScanBtn->setEnabled(true); | |||
| rescanAllBtn->setEnabled(false); | |||
| rescanCrashedBtn->setEnabled(false); | |||
| viewCrashedBtn->setEnabled(false); | |||
| configureBtn->setEnabled(false); | |||
| } else { | |||
| startScanBtn->setEnabled(true); | |||
| stopScanBtn->setEnabled(false); | |||
| rescanAllBtn->setEnabled(true); | |||
| rescanCrashedBtn->setEnabled(true); | |||
| viewCrashedBtn->setEnabled(true); | |||
| configureBtn->setEnabled(true); | |||
| } | |||
| } | |||
| void PluginScanStatusBar::_createCrashedPluginsDialogue() { | |||
| // Read the plugins from the crashed file | |||
| juce::String bodyText; | |||
| juce::File crashedPluginsFile = Utils::DataDirectory.getChildFile(Utils::CRASHED_PLUGINS_FILE_NAME); | |||
| juce::String crashedPluginsStr; | |||
| if (crashedPluginsFile.existsAsFile()) { | |||
| crashedPluginsStr = crashedPluginsFile.loadFileAsString(); | |||
| } | |||
| if (!crashedPluginsStr.isEmpty()) { | |||
| bodyText += "The following plugins crashed during validation:\n\n"; | |||
| if (!crashedPluginsStr.isEmpty()) { | |||
| bodyText += crashedPluginsStr; | |||
| } | |||
| } else { | |||
| bodyText += "No plugins failed validation"; | |||
| } | |||
| crashedPluginsPopover.reset(new UIUtils::PopoverComponent("Crashed plugins", bodyText, [&]() {crashedPluginsPopover.reset(); })); | |||
| getParentComponent()->addAndMakeVisible(crashedPluginsPopover.get()); | |||
| crashedPluginsPopover->setBounds(getParentComponent()->getLocalBounds()); | |||
| } | |||
| void PluginScanStatusBar::_createConfigureDialogue() { | |||
| // Restore before opening the component | |||
| _pluginScanClient.config.restoreFromXml(); | |||
| configurePopover.reset(new ConfigurePopover(_pluginScanClient.config, [&]() { | |||
| _pluginScanClient.config.writeToXml(); | |||
| configurePopover.reset(); | |||
| })); | |||
| getParentComponent()->addAndMakeVisible(configurePopover.get()); | |||
| configurePopover->setBounds(getParentComponent()->getLocalBounds()); | |||
| } | |||
| @@ -0,0 +1,55 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginScanClient.h" | |||
| #include "SelectorComponentStyle.h" | |||
| #include "UIUtils.h" | |||
| #include "ConfigurePopover.hpp" | |||
| class PluginScanStatusBar : public juce::Component, | |||
| public juce::MessageListener, | |||
| public juce::Button::Listener { | |||
| public: | |||
| PluginScanStatusBar(PluginScanClient& pluginScanClient, | |||
| const SelectorComponentStyle& style); | |||
| ~PluginScanStatusBar() override; | |||
| void resized() override; | |||
| void handleMessage(const juce::Message& message) override; | |||
| void buttonClicked(juce::Button* buttonThatWasClicked) override; | |||
| static constexpr int SPACER_WIDTH {10}; | |||
| static constexpr int NARROW_BUTTON_WIDTH {80}; | |||
| static constexpr int WIDE_BUTTON_WIDTH {130}; | |||
| static constexpr int ROW_HEIGHT {24}; | |||
| static constexpr int MAX_BUTTONS_WIDTH { | |||
| NARROW_BUTTON_WIDTH + SPACER_WIDTH + | |||
| NARROW_BUTTON_WIDTH + SPACER_WIDTH + | |||
| WIDE_BUTTON_WIDTH + SPACER_WIDTH + | |||
| WIDE_BUTTON_WIDTH + SPACER_WIDTH + | |||
| WIDE_BUTTON_WIDTH + SPACER_WIDTH + | |||
| NARROW_BUTTON_WIDTH + SPACER_WIDTH | |||
| }; | |||
| static constexpr int MIN_STATUS_WIDTH {140}; | |||
| private: | |||
| std::unique_ptr<juce::Label> statusLbl; | |||
| std::unique_ptr<juce::TextButton> startScanBtn; | |||
| std::unique_ptr<juce::TextButton> stopScanBtn; | |||
| std::unique_ptr<juce::TextButton> rescanAllBtn; | |||
| std::unique_ptr<juce::TextButton> rescanCrashedBtn; | |||
| std::unique_ptr<juce::TextButton> viewCrashedBtn; | |||
| std::unique_ptr<juce::TextButton> configureBtn; | |||
| std::unique_ptr<ConfigurePopover> configurePopover; | |||
| std::unique_ptr<UIUtils::PopoverComponent> crashedPluginsPopover; | |||
| PluginScanClient& _pluginScanClient; | |||
| void _updateButtonState(bool isScanRunning); | |||
| void _createCrashedPluginsDialogue(); | |||
| void _createConfigureDialogue(); | |||
| }; | |||
| @@ -0,0 +1,27 @@ | |||
| /* | |||
| ============================================================================== | |||
| PluginScanStatusMessage.h | |||
| Created: 15 Mar 2021 11:19:44pm | |||
| Author: Jack Devlin | |||
| ============================================================================== | |||
| */ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| class PluginScanStatusMessage : public juce::Message { | |||
| public: | |||
| const int numPluginsScanned; | |||
| const bool isScanRunning; | |||
| const juce::String errorText; | |||
| PluginScanStatusMessage(int newNumPluginsScanned, | |||
| bool newIsScanRunning, | |||
| juce::String newErrorText) : | |||
| numPluginsScanned(newNumPluginsScanned), | |||
| isScanRunning(newIsScanRunning), | |||
| errorText(newErrorText) {} | |||
| }; | |||
| @@ -0,0 +1,194 @@ | |||
| #include "PluginSelectorComponent.h" | |||
| #include "PluginUtils.h" | |||
| namespace { | |||
| constexpr int MARGIN_SIZE {10}; | |||
| constexpr int ROW_HEIGHT {24}; | |||
| } | |||
| PluginSelectorComponent::PluginSelectorComponent(PluginSelectorListParameters selectorListParameters, | |||
| std::function<void()> onCloseCallback, | |||
| const SelectorComponentStyle& style) | |||
| : _state(selectorListParameters.state), | |||
| _onCloseCallback(onCloseCallback), | |||
| _backgroundColour(style.backgroundColour) { | |||
| setComponentID(Utils::pluginSelectorComponentID); | |||
| // Position the header row | |||
| _setupHeaderRow(style); | |||
| // Position the status bar | |||
| statusBar.reset(new PluginScanStatusBar(selectorListParameters.scanner, style)); | |||
| addAndMakeVisible(statusBar.get()); | |||
| statusBar->setName("Plugin Scan Status Bar"); | |||
| // Position the table to use the remaining space | |||
| pluginTableListBox.reset(new PluginSelectorTableListBox(selectorListParameters, style)); | |||
| addAndMakeVisible(pluginTableListBox.get()); | |||
| pluginTableListBox->setName("Plugin Table List Box"); | |||
| pluginTableListBox->getHeader().setLookAndFeel(style.tableHeaderLookAndFeel.get()); | |||
| pluginTableListBox->getHeader().setColour(juce::TableHeaderComponent::textColourId, style.highlightColour); | |||
| pluginTableListBox->getHeader().setColour(juce::TableHeaderComponent::outlineColourId, style.highlightColour); | |||
| pluginTableListBox->getHeader().setColour(juce::TableHeaderComponent::backgroundColourId, style.backgroundColour); | |||
| pluginTableListBox->getHeader().setColour(juce::TableHeaderComponent::highlightColourId, style.neutralColour); | |||
| pluginTableListBox->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); | |||
| pluginTableListBox->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, style.neutralColour.withAlpha(0.5f)); | |||
| pluginTableListBox->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); | |||
| pluginTableListBox->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); | |||
| pluginTableListBox->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, style.neutralColour.withAlpha(0.5f)); | |||
| pluginTableListBox->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); | |||
| // Recall UI from state | |||
| searchEdt->setText(_state.filterString, false); | |||
| vstBtn->setToggleState(_state.includeVST, juce::dontSendNotification); | |||
| vst3Btn->setToggleState(_state.includeVST3, juce::dontSendNotification); | |||
| auBtn->setToggleState(_state.includeAU, juce::dontSendNotification); | |||
| } | |||
| PluginSelectorComponent::~PluginSelectorComponent() { | |||
| // Save the scroll position | |||
| _state.scrollPosition = pluginTableListBox->getVerticalPosition(); | |||
| searchEdt->setLookAndFeel(nullptr); | |||
| vstBtn->setLookAndFeel(nullptr); | |||
| vst3Btn->setLookAndFeel(nullptr); | |||
| auBtn->setLookAndFeel(nullptr); | |||
| pluginTableListBox->getHeader().setLookAndFeel(nullptr); | |||
| searchEdt = nullptr; | |||
| vstBtn = nullptr; | |||
| vst3Btn = nullptr; | |||
| auBtn = nullptr; | |||
| pluginTableListBox = nullptr; | |||
| statusBar = nullptr; | |||
| } | |||
| void PluginSelectorComponent::resized() { | |||
| juce::Rectangle<int> availableArea = getLocalBounds().reduced(MARGIN_SIZE); | |||
| // Header | |||
| juce::Rectangle<int> headerArea = availableArea.removeFromTop(ROW_HEIGHT); | |||
| juce::FlexBox flexBox; | |||
| flexBox.flexDirection = juce::FlexBox::Direction::row; | |||
| flexBox.flexWrap = juce::FlexBox::Wrap::wrap; | |||
| flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; | |||
| flexBox.alignContent = juce::FlexBox::AlignContent::center; | |||
| constexpr int MAX_BUTTONS_WIDTH {220}; | |||
| const int buttonsTotalWidth { | |||
| getWidth() < MAX_BUTTONS_WIDTH * 2 ? getWidth() / 2 : MAX_BUTTONS_WIDTH | |||
| }; | |||
| constexpr int buttonWidth {56}; | |||
| flexBox.items.add(juce::FlexItem(*vstBtn.get()).withMinWidth(buttonWidth).withMinHeight(ROW_HEIGHT)); | |||
| flexBox.items.add(juce::FlexItem(*vst3Btn.get()).withMinWidth(buttonWidth).withMinHeight(ROW_HEIGHT)); | |||
| #ifdef __APPLE__ | |||
| flexBox.items.add(juce::FlexItem(*auBtn.get()).withMinWidth(buttonWidth).withMinHeight(ROW_HEIGHT)); | |||
| #endif | |||
| flexBox.performLayout(headerArea.removeFromRight(buttonsTotalWidth).toFloat()); | |||
| headerArea.removeFromRight(MARGIN_SIZE); | |||
| searchEdt->setBounds(headerArea); | |||
| availableArea.removeFromTop(MARGIN_SIZE); | |||
| // Status bar | |||
| constexpr int STATUS_BAR_THRESHOLD_WIDTH {PluginScanStatusBar::MIN_STATUS_WIDTH + PluginScanStatusBar::MAX_BUTTONS_WIDTH}; | |||
| const int statusBarHeight {availableArea.getWidth() < STATUS_BAR_THRESHOLD_WIDTH ? ROW_HEIGHT * 2 : ROW_HEIGHT}; | |||
| statusBar->setBounds(availableArea.removeFromBottom(statusBarHeight)); | |||
| availableArea.removeFromBottom(MARGIN_SIZE); | |||
| // Table | |||
| pluginTableListBox->setBounds(availableArea); | |||
| } | |||
| void PluginSelectorComponent::paint(juce::Graphics& g) { | |||
| g.fillAll(_backgroundColour); | |||
| } | |||
| void PluginSelectorComponent::buttonClicked(juce::Button* buttonThatWasClicked) { | |||
| if (buttonThatWasClicked == vstBtn.get()) { | |||
| vstBtn->setToggleState(!vstBtn->getToggleState(), juce::dontSendNotification); | |||
| _state.includeVST = vstBtn->getToggleState(); | |||
| pluginTableListBox->onFiltersOrSortUpdate(); | |||
| } else if (buttonThatWasClicked == vst3Btn.get()) { | |||
| vst3Btn->setToggleState(!vst3Btn->getToggleState(), juce::dontSendNotification); | |||
| _state.includeVST3 = vst3Btn->getToggleState(); | |||
| pluginTableListBox->onFiltersOrSortUpdate(); | |||
| } else if (buttonThatWasClicked == auBtn.get()) { | |||
| auBtn->setToggleState(!auBtn->getToggleState(), juce::dontSendNotification); | |||
| _state.includeAU = auBtn->getToggleState(); | |||
| pluginTableListBox->onFiltersOrSortUpdate(); | |||
| } | |||
| } | |||
| bool PluginSelectorComponent::keyPressed(const juce::KeyPress& key) { | |||
| if (key.isKeyCode(juce::KeyPress::escapeKey)) { | |||
| // Close the window | |||
| _onCloseCallback(); | |||
| return true; | |||
| } | |||
| return false; // Return true if your handler uses this key event, or false to allow it to be passed-on. | |||
| } | |||
| void PluginSelectorComponent::textEditorTextChanged(juce::TextEditor& textEditor) { | |||
| _state.filterString = searchEdt->getText(); | |||
| pluginTableListBox->onFiltersOrSortUpdate(); | |||
| } | |||
| void PluginSelectorComponent::_setupHeaderRow(const SelectorComponentStyle& style) { | |||
| searchEdt.reset(new juce::TextEditor("Search Text Editor")); | |||
| addAndMakeVisible(searchEdt.get()); | |||
| searchEdt->setMultiLine(false); | |||
| searchEdt->setReturnKeyStartsNewLine(false); | |||
| searchEdt->setReadOnly(false); | |||
| searchEdt->setScrollbarsShown(true); | |||
| searchEdt->setCaretVisible(true); | |||
| searchEdt->setPopupMenuEnabled(true); | |||
| searchEdt->setText(juce::String()); | |||
| searchEdt->addListener(this); | |||
| searchEdt->setEscapeAndReturnKeysConsumed(false); | |||
| searchEdt->setSelectAllWhenFocused(true); | |||
| searchEdt->setWantsKeyboardFocus(true); | |||
| searchEdt->setLookAndFeel(style.searchBarLookAndFeel.get()); | |||
| searchEdt->setColour(juce::TextEditor::outlineColourId, style.highlightColour); | |||
| searchEdt->setColour(juce::TextEditor::backgroundColourId, style.backgroundColour); | |||
| searchEdt->setColour(juce::TextEditor::textColourId, style.highlightColour); | |||
| searchEdt->setColour(juce::TextEditor::highlightColourId, style.highlightColour); | |||
| searchEdt->setColour(juce::TextEditor::highlightedTextColourId, style.neutralColour); | |||
| searchEdt->setColour(juce::CaretComponent::caretColourId, style.highlightColour); | |||
| vstBtn.reset(new juce::TextButton("VST Button")); | |||
| addAndMakeVisible(vstBtn.get()); | |||
| vstBtn->setButtonText(TRANS("VST")); | |||
| vstBtn->addListener(this); | |||
| vstBtn->setLookAndFeel(style.headerButtonLookAndFeel.get()); | |||
| vstBtn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, style.buttonBackgroundColour); | |||
| vstBtn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, style.highlightColour); | |||
| vstBtn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, style.disabledColour); | |||
| vst3Btn.reset(new juce::TextButton ("VST3 Button")); | |||
| addAndMakeVisible(vst3Btn.get()); | |||
| vst3Btn->setButtonText(TRANS("VST3")); | |||
| vst3Btn->addListener(this); | |||
| vst3Btn->setLookAndFeel(style.headerButtonLookAndFeel.get()); | |||
| vst3Btn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, style.buttonBackgroundColour); | |||
| vst3Btn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, style.highlightColour); | |||
| vst3Btn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, style.disabledColour); | |||
| auBtn.reset(new juce::TextButton("AU Button")); | |||
| addAndMakeVisible(auBtn.get()); | |||
| auBtn->setButtonText(TRANS("AU")); | |||
| auBtn->addListener(this); | |||
| auBtn->setLookAndFeel(style.headerButtonLookAndFeel.get()); | |||
| auBtn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, style.buttonBackgroundColour); | |||
| auBtn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, style.highlightColour); | |||
| auBtn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, style.disabledColour); | |||
| } | |||
| void PluginSelectorComponent::restoreScrollPosition() { | |||
| pluginTableListBox->setVerticalPosition(_state.scrollPosition); | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginSelectorList.h" | |||
| #include "PluginScanStatusBar.h" | |||
| #include "SelectorComponentStyle.h" | |||
| class PluginSelectorComponent : public juce::Component, | |||
| public juce::TextEditor::Listener, | |||
| public juce::Button::Listener { | |||
| public: | |||
| PluginSelectorComponent(PluginSelectorListParameters selectorListParameters, | |||
| std::function<void()> onCloseCallback, | |||
| const SelectorComponentStyle& style); | |||
| ~PluginSelectorComponent() override; | |||
| void textEditorTextChanged(juce::TextEditor& textEditor) override; | |||
| void resized() override; | |||
| void paint(juce::Graphics& g) override; | |||
| void buttonClicked(juce::Button* buttonThatWasClicked) override; | |||
| bool keyPressed(const juce::KeyPress& key) override; | |||
| /** | |||
| * Restores the scroll position from the stored state. This must be done only after the | |||
| * component bounds have been restored, otherwise it'll scroll to the wrong place. | |||
| */ | |||
| void restoreScrollPosition(); | |||
| private: | |||
| PluginSelectorState& _state; | |||
| std::function<void()> _onCloseCallback; | |||
| const juce::Colour _backgroundColour; | |||
| std::unique_ptr<juce::TextEditor> searchEdt; | |||
| std::unique_ptr<juce::TextButton> vstBtn; | |||
| std::unique_ptr<juce::TextButton> vst3Btn; | |||
| std::unique_ptr<juce::TextButton> auBtn; | |||
| std::unique_ptr<PluginSelectorTableListBox> pluginTableListBox; | |||
| std::unique_ptr<PluginScanStatusBar> statusBar; | |||
| void _setupHeaderRow(const SelectorComponentStyle& style); | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginSelectorComponent) | |||
| }; | |||
| @@ -0,0 +1,210 @@ | |||
| /* | |||
| ============================================================================== | |||
| PluginSelectorList.cpp | |||
| Created: 23 May 2021 12:39:35am | |||
| Author: Jack Devlin | |||
| ============================================================================== | |||
| */ | |||
| #include "PluginSelectorList.h" | |||
| #include "PluginScanStatusMessage.h" | |||
| namespace { | |||
| enum COLUMN_ID { | |||
| NULL_ID, // Can't use 0 as a column ID | |||
| NAME, | |||
| MANUFACTURER, | |||
| CATEGORY, | |||
| FORMAT | |||
| }; | |||
| } | |||
| PluginListSorter::PluginListSorter(PluginSelectorState& state) : state(state) { | |||
| } | |||
| void PluginListSorter::setPluginList(juce::Array<juce::PluginDescription> pluginList) { | |||
| _fullPluginList = pluginList; | |||
| } | |||
| juce::Array<juce::PluginDescription> PluginListSorter::getFilteredPluginList() const { | |||
| juce::Array<juce::PluginDescription> filteredPluginList; | |||
| // Apply the user's filters and don't display instruments | |||
| for (const juce::PluginDescription& thisPlugin : _fullPluginList) { | |||
| if (_passesFilter(thisPlugin) && !thisPlugin.isInstrument) { | |||
| filteredPluginList.add(thisPlugin); | |||
| } | |||
| } | |||
| // Now sort the list | |||
| filteredPluginList.sort(*this, false); | |||
| return filteredPluginList; | |||
| } | |||
| int PluginListSorter::compareElements(juce::PluginDescription first, juce::PluginDescription second) const { | |||
| int retVal {0}; | |||
| switch (state.sortColumnId) { | |||
| case NAME: | |||
| retVal = first.name.compare(second.name); | |||
| break; | |||
| case MANUFACTURER: | |||
| retVal = first.manufacturerName.compare(second.manufacturerName); | |||
| break; | |||
| case CATEGORY: | |||
| retVal = first.category.compare(second.category); | |||
| break; | |||
| case FORMAT: | |||
| retVal = first.pluginFormatName.compare(second.pluginFormatName); | |||
| break; | |||
| } | |||
| return retVal * (state.sortForwards ? 1 : -1); | |||
| } | |||
| bool PluginListSorter::_passesFilter(const juce::PluginDescription& plugin) const { | |||
| const bool passesTextFilter { | |||
| plugin.name.containsIgnoreCase(state.filterString) || | |||
| plugin.manufacturerName.containsIgnoreCase(state.filterString) || | |||
| state.filterString.isEmpty() | |||
| }; | |||
| const bool passesFormatFilter { | |||
| (plugin.pluginFormatName == "VST" && state.includeVST) || | |||
| (plugin.pluginFormatName == "VST3" && state.includeVST3) || | |||
| (plugin.pluginFormatName == "AudioUnit" && state.includeAU) | |||
| }; | |||
| return passesTextFilter && passesFormatFilter; | |||
| } | |||
| PluginSelectorTableListBoxModel::PluginSelectorTableListBoxModel( | |||
| PluginSelectorListParameters selectorListParameters, | |||
| const SelectorComponentStyle& style) | |||
| : _scanner(selectorListParameters.scanner), | |||
| _pluginListSorter(selectorListParameters.state), | |||
| _pluginCreationCallback(selectorListParameters.pluginCreationCallback), | |||
| _getSampleRateCallback(selectorListParameters.getSampleRate), | |||
| _getBlockSizeCallback(selectorListParameters.getBlockSize), | |||
| _formatManager(selectorListParameters.formatManager), | |||
| _rowBackgroundColour(style.backgroundColour), | |||
| _rowTextColour(style.neutralColour), | |||
| _isReplacingParameter(selectorListParameters.isReplacingPlugin) { | |||
| _pluginListSorter.setPluginList(_scanner.getPluginTypes()); | |||
| _pluginList = _pluginListSorter.getFilteredPluginList(); | |||
| juce::Logger::writeToLog("Created PluginSelectorTableListBoxModel, found " + juce::String(_pluginList.size()) + " plugins"); | |||
| } | |||
| void PluginSelectorTableListBoxModel::onFiltersOrSortUpdate() { | |||
| _pluginList = _pluginListSorter.getFilteredPluginList(); | |||
| } | |||
| int PluginSelectorTableListBoxModel::getNumRows() { | |||
| return _pluginList.size(); | |||
| } | |||
| void PluginSelectorTableListBoxModel::paintRowBackground(juce::Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) { | |||
| g.fillAll(_rowBackgroundColour); | |||
| } | |||
| void PluginSelectorTableListBoxModel::paintCell(juce::Graphics& g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) { | |||
| if (rowNumber < _pluginList.size()) { | |||
| const juce::PluginDescription& thisPlugin = _pluginList[rowNumber]; | |||
| juce::String text; | |||
| switch (columnId) { | |||
| case NAME: | |||
| text = thisPlugin.name; | |||
| break; | |||
| case MANUFACTURER: | |||
| text = thisPlugin.manufacturerName; | |||
| break; | |||
| case CATEGORY: | |||
| text = thisPlugin.category; | |||
| break; | |||
| case FORMAT: | |||
| text = thisPlugin.pluginFormatName; | |||
| break; | |||
| default: | |||
| break; | |||
| } | |||
| g.setColour(_rowTextColour); | |||
| g.drawText(text, 2, 0, width - 4, height, juce::Justification::centredLeft, true); | |||
| } | |||
| } | |||
| void PluginSelectorTableListBoxModel::cellDoubleClicked(int rowNumber, | |||
| int columnId, | |||
| const juce::MouseEvent& event) { | |||
| juce::Logger::writeToLog("PluginSelectorTableListBoxModel: Row " + juce::String(rowNumber) + " clicked, attempting to load plugin: " + _pluginList[rowNumber].name); | |||
| const bool shouldCloseWindow {!juce::ModifierKeys::currentModifiers.isCommandDown() || _isReplacingParameter}; | |||
| _formatManager.createPluginInstanceAsync(_pluginList[rowNumber], | |||
| _getSampleRateCallback(), | |||
| _getBlockSizeCallback(), | |||
| [&, shouldCloseWindow](std::unique_ptr<juce::AudioPluginInstance> plugin, const juce::String& error) { _pluginCreationCallback(std::move(plugin), error, shouldCloseWindow); }); | |||
| }; | |||
| void PluginSelectorTableListBoxModel::sortOrderChanged(int newSortColumnId, bool isForwards) { | |||
| _pluginListSorter.state.sortColumnId = newSortColumnId; | |||
| _pluginListSorter.state.sortForwards = isForwards; | |||
| _pluginList = _pluginListSorter.getFilteredPluginList(); | |||
| } | |||
| void PluginSelectorTableListBoxModel::onPluginScanUpdate() { | |||
| _pluginListSorter.setPluginList(_scanner.getPluginTypes()); | |||
| _pluginList = _pluginListSorter.getFilteredPluginList(); | |||
| juce::Logger::writeToLog("PluginSelectorTableListBoxModel: now listing " + juce::String(_pluginList.size()) + " plugins"); | |||
| } | |||
| PluginSelectorTableListBox::PluginSelectorTableListBox(PluginSelectorListParameters selectorListParameters, | |||
| const SelectorComponentStyle& style) | |||
| : _pluginTableListBoxModel(selectorListParameters, style), _scanner(selectorListParameters.scanner) { | |||
| // Start listening for scan messages and update the plugin list | |||
| _scanner.addListener(this); | |||
| constexpr int flags {juce::TableHeaderComponent::visible | juce::TableHeaderComponent::sortable}; | |||
| getHeader().addColumn("Name", NAME, 265, 265, 265, flags); | |||
| getHeader().addColumn("Manufacturer", MANUFACTURER, 265, 265, 265, flags); | |||
| getHeader().addColumn("Category", CATEGORY, 178, 178, 178, flags); | |||
| getHeader().addColumn("Format", FORMAT, 64, 64, 64, flags); | |||
| setModel(&_pluginTableListBoxModel); | |||
| setColour(juce::ListBox::backgroundColourId, style.backgroundColour); | |||
| // Restore the previous sort state | |||
| getHeader().setSortColumnId(selectorListParameters.state.sortColumnId, | |||
| selectorListParameters.state.sortForwards); | |||
| } | |||
| PluginSelectorTableListBox::~PluginSelectorTableListBox() { | |||
| _scanner.removeListener(this); | |||
| } | |||
| void PluginSelectorTableListBox::onFiltersOrSortUpdate() { | |||
| _pluginTableListBoxModel.onFiltersOrSortUpdate(); | |||
| updateContent(); | |||
| } | |||
| void PluginSelectorTableListBox::handleMessage(const juce::Message& message) { | |||
| const PluginScanStatusMessage* statusMessage { | |||
| dynamic_cast<const PluginScanStatusMessage*>(&message) | |||
| }; | |||
| if (statusMessage != nullptr) { | |||
| _pluginTableListBoxModel.onPluginScanUpdate(); | |||
| updateContent(); | |||
| juce::Logger::writeToLog("PluginSelectorTableListBox: received PluginScanStatusMessage"); | |||
| } | |||
| } | |||
| @@ -0,0 +1,87 @@ | |||
| /* | |||
| ============================================================================== | |||
| PluginSelectorList.h | |||
| Created: 23 May 2021 12:39:35am | |||
| Author: Jack Devlin | |||
| ============================================================================== | |||
| */ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginScanClient.h" | |||
| #include "PluginSelectorListParameters.h" | |||
| #include "PluginSelectorState.h" | |||
| #include "SelectorComponentStyle.h" | |||
| class PluginListSorter { | |||
| public: | |||
| PluginSelectorState& state; | |||
| PluginListSorter(PluginSelectorState& state); | |||
| ~PluginListSorter() = default; | |||
| void setPluginList(juce::Array<juce::PluginDescription> pluginList); | |||
| juce::Array<juce::PluginDescription> getFilteredPluginList() const; | |||
| int compareElements(juce::PluginDescription first, juce::PluginDescription second) const; | |||
| private: | |||
| juce::Array<juce::PluginDescription> _fullPluginList; | |||
| bool _passesFilter(const juce::PluginDescription& plugin) const; | |||
| }; | |||
| class PluginSelectorTableListBoxModel : public juce::TableListBoxModel { | |||
| public: | |||
| PluginSelectorTableListBoxModel(PluginSelectorListParameters selectorListParameters, | |||
| const SelectorComponentStyle& style); | |||
| virtual ~PluginSelectorTableListBoxModel() = default; | |||
| void onFiltersOrSortUpdate(); | |||
| virtual int getNumRows() override; | |||
| virtual void paintRowBackground(juce::Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) override; | |||
| virtual void paintCell(juce::Graphics& g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) override; | |||
| void cellDoubleClicked(int rowNumber, int columnId, const juce::MouseEvent& event) override; | |||
| void sortOrderChanged(int newSortColumnId, bool isForwards) override; | |||
| void onPluginScanUpdate(); | |||
| private: | |||
| PluginScanClient& _scanner; | |||
| PluginListSorter _pluginListSorter; | |||
| juce::Array<juce::PluginDescription> _pluginList; | |||
| std::function<void(std::unique_ptr<juce::AudioPluginInstance>, const juce::String&, bool)> _pluginCreationCallback; | |||
| std::function<double()> _getSampleRateCallback; | |||
| std::function<int()> _getBlockSizeCallback; | |||
| juce::AudioPluginFormatManager& _formatManager; | |||
| juce::Colour _rowBackgroundColour; | |||
| juce::Colour _rowTextColour; | |||
| const bool _isReplacingParameter; | |||
| }; | |||
| class PluginSelectorTableListBox : public juce::TableListBox, | |||
| public juce::MessageListener { | |||
| public: | |||
| PluginSelectorTableListBox(PluginSelectorListParameters selectorListParameters, | |||
| const SelectorComponentStyle& style); | |||
| virtual ~PluginSelectorTableListBox(); | |||
| void onFiltersOrSortUpdate(); | |||
| void handleMessage(const juce::Message& message) override; | |||
| private: | |||
| PluginSelectorTableListBoxModel _pluginTableListBoxModel; | |||
| PluginScanClient& _scanner; | |||
| }; | |||
| @@ -0,0 +1,26 @@ | |||
| /* | |||
| ============================================================================== | |||
| PluginSelectorListParameters.h | |||
| Created: 28 May 2021 11:53:23pm | |||
| Author: Jack Devlin | |||
| ============================================================================== | |||
| */ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginScanClient.h" | |||
| #include "PluginSelectorState.h" | |||
| struct PluginSelectorListParameters { | |||
| PluginScanClient& scanner; | |||
| PluginSelectorState& state; | |||
| juce::AudioPluginFormatManager& formatManager; | |||
| std::function<void(std::unique_ptr<juce::AudioPluginInstance>, const juce::String&, bool)> pluginCreationCallback; | |||
| std::function<double()> getSampleRate; | |||
| std::function<int()> getBlockSize; | |||
| bool isReplacingPlugin; | |||
| }; | |||
| @@ -0,0 +1,78 @@ | |||
| #include "PluginSelectorState.h" | |||
| namespace { | |||
| const char* XML_SORT_COLUMN_ID_STR {"sortColumnId"}; | |||
| const char* XML_SORT_FORWARDS_STR {"sortForwards"}; | |||
| const char* XML_SORT_INCLUDE_VST_STR {"includeVST"}; | |||
| const char* XML_SORT_INCLUDE_VST3_STR {"includeVST3"}; | |||
| const char* XML_SORT_INCLUDE_AU_STR {"includeAU"}; | |||
| const char* XML_SORT_FILTER_STRING_STR {"filterString"}; | |||
| const char* XML_SORT_BOUNDS_STR {"bounds"}; | |||
| const char* XML_SORT_SCROLL_POSITION_STR {"scrollPosition"}; | |||
| } | |||
| void PluginSelectorState::restoreFromXml(juce::XmlElement* element) { | |||
| if (element->hasAttribute(XML_SORT_COLUMN_ID_STR)) { | |||
| sortColumnId = element->getIntAttribute(XML_SORT_COLUMN_ID_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_COLUMN_ID_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SORT_FORWARDS_STR)) { | |||
| sortForwards = element->getBoolAttribute(XML_SORT_FORWARDS_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_FORWARDS_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SORT_INCLUDE_VST_STR)) { | |||
| includeVST = element->getBoolAttribute(XML_SORT_INCLUDE_VST_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_INCLUDE_VST_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SORT_INCLUDE_VST3_STR)) { | |||
| includeVST3 = element->getBoolAttribute(XML_SORT_INCLUDE_VST3_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_INCLUDE_VST3_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SORT_INCLUDE_AU_STR)) { | |||
| includeAU = element->getBoolAttribute(XML_SORT_INCLUDE_AU_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_INCLUDE_AU_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SORT_FILTER_STRING_STR)) { | |||
| filterString = element->getStringAttribute(XML_SORT_FILTER_STRING_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_FILTER_STRING_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SORT_BOUNDS_STR)) { | |||
| juce::String boundsStr = element->getStringAttribute(XML_SORT_BOUNDS_STR); | |||
| bounds = juce::Rectangle<int>::fromString(boundsStr); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_BOUNDS_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SORT_SCROLL_POSITION_STR)) { | |||
| scrollPosition = element->getDoubleAttribute(XML_SORT_SCROLL_POSITION_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SORT_SCROLL_POSITION_STR)); | |||
| } | |||
| } | |||
| void PluginSelectorState::writeToXml(juce::XmlElement* element) const { | |||
| element->setAttribute(XML_SORT_COLUMN_ID_STR, sortColumnId); | |||
| element->setAttribute(XML_SORT_FORWARDS_STR, sortForwards); | |||
| element->setAttribute(XML_SORT_INCLUDE_VST_STR, includeVST); | |||
| element->setAttribute(XML_SORT_INCLUDE_VST3_STR, includeVST3); | |||
| element->setAttribute(XML_SORT_INCLUDE_AU_STR, includeAU); | |||
| element->setAttribute(XML_SORT_FILTER_STRING_STR, filterString); | |||
| if (bounds.has_value()) { | |||
| element->setAttribute(XML_SORT_BOUNDS_STR, bounds->toString()); | |||
| } | |||
| element->setAttribute(XML_SORT_SCROLL_POSITION_STR, scrollPosition); | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include <optional> | |||
| struct PluginSelectorState { | |||
| int sortColumnId; | |||
| bool sortForwards; | |||
| bool includeLV2; | |||
| bool includeVST; | |||
| bool includeVST3; | |||
| bool includeAU; | |||
| // Stores the value of the text box | |||
| juce::String filterString; | |||
| // Stores the last location of the window - empty if the window hasn't been opened yet | |||
| std::optional<juce::Rectangle<int>> bounds; | |||
| // Stores scroll position (value from 0 to 1) | |||
| double scrollPosition; | |||
| PluginSelectorState() : sortColumnId(0), | |||
| sortForwards(true), | |||
| includeLV2(true), | |||
| includeVST(true), | |||
| includeVST3(true), | |||
| includeAU(true), | |||
| filterString(""), | |||
| bounds(), | |||
| scrollPosition(0) { } | |||
| void restoreFromXml(juce::XmlElement* element); | |||
| void writeToXml(juce::XmlElement* element) const; | |||
| }; | |||
| @@ -0,0 +1,59 @@ | |||
| #include "PluginSelectorWindow.h" | |||
| namespace { | |||
| const juce::Colour BACKGROUND_COLOUR(0, 0, 0); | |||
| constexpr int TITLE_BAR_BUTTONS { | |||
| juce::DocumentWindow::TitleBarButtons::minimiseButton | | |||
| juce::DocumentWindow::TitleBarButtons::closeButton | |||
| }; | |||
| } | |||
| PluginSelectorWindow::PluginSelectorWindow(std::function<void()> onCloseCallback, | |||
| PluginSelectorListParameters selectorListParameters, | |||
| std::unique_ptr<SelectorComponentStyle> style, | |||
| juce::String title) : | |||
| juce::DocumentWindow(title, BACKGROUND_COLOUR, TITLE_BAR_BUTTONS), | |||
| _onCloseCallback(onCloseCallback), | |||
| _content(nullptr), | |||
| _style(std::move(style)), | |||
| _state(selectorListParameters.state) { | |||
| if (_state.bounds.has_value()) { | |||
| // Use the previous bounds if we have them | |||
| setBounds(_state.bounds.value()); | |||
| } else { | |||
| // Default to the centre | |||
| constexpr int DEFAULT_WIDTH {900}; | |||
| constexpr int DEFAULT_HEIGHT {550}; | |||
| centreWithSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); | |||
| } | |||
| setVisible(true); | |||
| setResizable(true, true); | |||
| setAlwaysOnTop(true); | |||
| _content = new PluginSelectorComponent(selectorListParameters, onCloseCallback, *(_style.get())); | |||
| setContentOwned(_content, false); | |||
| _content->setBounds(0, getTitleBarHeight(), getWidth(), getHeight() - getTitleBarHeight()); | |||
| _content->restoreScrollPosition(); | |||
| selectorListParameters.scanner.clearMissingPlugins(); | |||
| juce::Logger::writeToLog("Created PluginSelectorWindow"); | |||
| } | |||
| PluginSelectorWindow::~PluginSelectorWindow() { | |||
| juce::Logger::writeToLog("Closing PluginSelectorWindow"); | |||
| _state.bounds = getBounds(); | |||
| clearContentComponent(); | |||
| } | |||
| void PluginSelectorWindow::closeButtonPressed() { | |||
| _onCloseCallback(); | |||
| } | |||
| void PluginSelectorWindow::takeFocus() { | |||
| if (_content != nullptr) { | |||
| _content->grabKeyboardFocus(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginSelectorComponent.h" | |||
| #include "PluginSelectorListParameters.h" | |||
| class PluginSelectorWindow : public juce::DocumentWindow { | |||
| public: | |||
| PluginSelectorWindow(std::function<void()> onCloseCallback, | |||
| PluginSelectorListParameters selectorListParameters, | |||
| std::unique_ptr<SelectorComponentStyle> style, | |||
| juce::String title); | |||
| virtual ~PluginSelectorWindow(); | |||
| virtual void closeButtonPressed() override; | |||
| void takeFocus(); | |||
| private: | |||
| std::function<void()> _onCloseCallback; | |||
| PluginSelectorComponent* _content; | |||
| std::unique_ptr<SelectorComponentStyle> _style; | |||
| // We need to keep a reference to state to update the bounds on resize | |||
| PluginSelectorState& _state; | |||
| }; | |||
| @@ -0,0 +1,107 @@ | |||
| #include "ScanConfiguration.hpp" | |||
| #include "AllUtils.h" | |||
| namespace { | |||
| const char* XML_SCAN_VST_DEFAULT_PATHS_STR {"vstDefaultPaths"}; | |||
| const char* XML_CUSTOM_VST_PATHS_STR {"vstCustomPaths"}; | |||
| const char* XML_SCAN_VST3_DEFAULT_PATHS_STR {"vst3DefaultPaths"}; | |||
| const char* XML_CUSTOM_VST3_PATHS_STR {"vst3CustomPaths"}; | |||
| } | |||
| ScanConfiguration::ScanConfiguration() : VSTDefaultPaths(true), VST3DefaultPaths(true) { | |||
| } | |||
| juce::FileSearchPath ScanConfiguration::getAUPaths() { | |||
| // Audiounit scanning is managed by macOS, so no custom paths | |||
| #ifdef __APPLE__ | |||
| return auFormat.getDefaultLocationsToSearch(); | |||
| #else | |||
| return juce::FileSearchPath(); | |||
| #endif | |||
| } | |||
| juce::FileSearchPath ScanConfiguration::getVSTPaths() { | |||
| juce::FileSearchPath paths = customVSTPaths; | |||
| if (VSTDefaultPaths) { | |||
| paths.addPath(vstFormat.getDefaultLocationsToSearch()); | |||
| } | |||
| return paths; | |||
| } | |||
| juce::FileSearchPath ScanConfiguration::getVST3Paths() { | |||
| juce::FileSearchPath paths = customVST3Paths; | |||
| if (VST3DefaultPaths) { | |||
| paths.addPath(vst3Format.getDefaultLocationsToSearch()); | |||
| } | |||
| return paths; | |||
| } | |||
| void ScanConfiguration::restoreFromXml() { | |||
| juce::File configFile = Utils::DataDirectory.getChildFile(Utils::SCAN_CONFIGURATION_FILE_NAME); | |||
| if (!configFile.existsAsFile()) { | |||
| juce::Logger::writeToLog("Scan configuration file doesn't exist"); | |||
| return; | |||
| } | |||
| std::unique_ptr<juce::XmlElement> element = juce::XmlDocument::parse(configFile); | |||
| if (element == nullptr) { | |||
| juce::Logger::writeToLog("Failed to parse XML from scan configuration file"); | |||
| return; | |||
| } | |||
| if (element->hasAttribute(XML_SCAN_VST_DEFAULT_PATHS_STR)) { | |||
| VSTDefaultPaths = element->getBoolAttribute(XML_SCAN_VST_DEFAULT_PATHS_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SCAN_VST_DEFAULT_PATHS_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_CUSTOM_VST_PATHS_STR)) { | |||
| // Clear existing paths | |||
| while (customVSTPaths.getNumPaths() > 0) { | |||
| customVSTPaths.remove(0); | |||
| } | |||
| auto paths = juce::StringArray::fromTokens(element->getStringAttribute(XML_CUSTOM_VST_PATHS_STR), ";", ""); | |||
| for (juce::String path : paths) { | |||
| customVSTPaths.addIfNotAlreadyThere(juce::File(path)); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_CUSTOM_VST_PATHS_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_SCAN_VST3_DEFAULT_PATHS_STR)) { | |||
| VST3DefaultPaths = element->getBoolAttribute(XML_SCAN_VST3_DEFAULT_PATHS_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SCAN_VST3_DEFAULT_PATHS_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_CUSTOM_VST3_PATHS_STR)) { | |||
| // Clear existing paths | |||
| while (customVST3Paths.getNumPaths() > 0) { | |||
| customVST3Paths.remove(0); | |||
| } | |||
| auto paths = juce::StringArray::fromTokens(element->getStringAttribute(XML_CUSTOM_VST3_PATHS_STR), ";", ""); | |||
| for (juce::String path : paths) { | |||
| customVST3Paths.addIfNotAlreadyThere(juce::File(path)); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_CUSTOM_VST3_PATHS_STR)); | |||
| } | |||
| } | |||
| void ScanConfiguration::writeToXml() const { | |||
| juce::File configFile = Utils::DataDirectory.getChildFile(Utils::SCAN_CONFIGURATION_FILE_NAME); | |||
| configFile.deleteFile(); | |||
| auto element = std::make_unique<juce::XmlElement>("scanConfiguration"); | |||
| element->setAttribute(XML_SCAN_VST_DEFAULT_PATHS_STR, VSTDefaultPaths); | |||
| element->setAttribute(XML_CUSTOM_VST_PATHS_STR, customVSTPaths.toString()); | |||
| element->setAttribute(XML_SCAN_VST3_DEFAULT_PATHS_STR, VST3DefaultPaths); | |||
| element->setAttribute(XML_CUSTOM_VST3_PATHS_STR, customVST3Paths.toString()); | |||
| element->writeTo(configFile); | |||
| } | |||
| @@ -0,0 +1,29 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| class ScanConfiguration { | |||
| public: | |||
| #ifdef __APPLE__ | |||
| juce::AudioUnitPluginFormat auFormat; | |||
| #endif | |||
| juce::VSTPluginFormat vstFormat; | |||
| juce::VST3PluginFormat vst3Format; | |||
| bool VSTDefaultPaths; | |||
| juce::FileSearchPath customVSTPaths; | |||
| bool VST3DefaultPaths; | |||
| juce::FileSearchPath customVST3Paths; | |||
| ScanConfiguration(); | |||
| juce::FileSearchPath getAUPaths(); | |||
| juce::FileSearchPath getVSTPaths(); | |||
| juce::FileSearchPath getVST3Paths(); | |||
| void restoreFromXml(); | |||
| void writeToXml() const; | |||
| }; | |||
| @@ -0,0 +1,34 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| struct SelectorComponentStyle { | |||
| const juce::Colour backgroundColour; | |||
| const juce::Colour buttonBackgroundColour; | |||
| const juce::Colour neutralColour; | |||
| const juce::Colour highlightColour; | |||
| const juce::Colour disabledColour; | |||
| const std::unique_ptr<juce::LookAndFeel> searchBarLookAndFeel; | |||
| const std::unique_ptr<juce::LookAndFeel> headerButtonLookAndFeel; | |||
| const std::unique_ptr<juce::LookAndFeel> scanButtonLookAndFeel; | |||
| const std::unique_ptr<juce::LookAndFeel> tableHeaderLookAndFeel; | |||
| SelectorComponentStyle(juce::Colour newBackgroundColour, | |||
| juce::Colour newButtonBackgroundColour, | |||
| juce::Colour newNeutralColour, | |||
| juce::Colour newHighlightColour, | |||
| juce::Colour newDisabledColour, | |||
| std::unique_ptr<juce::LookAndFeel> newSearchBarLookAndFeel, | |||
| std::unique_ptr<juce::LookAndFeel> newHeaderButtonLookAndFeel, | |||
| std::unique_ptr<juce::LookAndFeel> newScanButtonLookAndFeel, | |||
| std::unique_ptr<juce::LookAndFeel> newTableHeaderLookAndFeel) : | |||
| backgroundColour(newBackgroundColour), | |||
| buttonBackgroundColour(newButtonBackgroundColour), | |||
| neutralColour(newNeutralColour), | |||
| highlightColour(newHighlightColour), | |||
| disabledColour(newDisabledColour), | |||
| searchBarLookAndFeel(std::move(newSearchBarLookAndFeel)), | |||
| headerButtonLookAndFeel(std::move(newHeaderButtonLookAndFeel)), | |||
| scanButtonLookAndFeel(std::move(newScanButtonLookAndFeel)), | |||
| tableHeaderLookAndFeel(std::move(newTableHeaderLookAndFeel)) { } | |||
| }; | |||
| @@ -0,0 +1,59 @@ | |||
| /* | |||
| ============================================================================== | |||
| Utils.h | |||
| Created: 16 Mar 2021 9:09:05pm | |||
| Author: Jack Devlin | |||
| ============================================================================== | |||
| */ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| namespace Utils { | |||
| const juce::String pluginSelectorComponentID("PluginSelectorComponent"); | |||
| inline juce::String busesLayoutToString(juce::AudioProcessor::BusesLayout layout) { | |||
| juce::String retVal; | |||
| retVal += "Inputs: "; | |||
| for (const juce::AudioChannelSet& bus : layout.inputBuses) { | |||
| for (const juce::AudioChannelSet::ChannelType channelType : bus.getChannelTypes()) { | |||
| retVal += juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType) + " "; | |||
| } | |||
| retVal += "| "; | |||
| } | |||
| retVal += "\n"; | |||
| retVal += "Outputs: "; | |||
| for (const juce::AudioChannelSet& bus : layout.outputBuses) { | |||
| for (const juce::AudioChannelSet::ChannelType channelType : bus.getChannelTypes()) { | |||
| retVal += juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType) + " "; | |||
| } | |||
| retVal += "| "; | |||
| } | |||
| return retVal; | |||
| } | |||
| inline void processBalance(float panValue, juce::AudioBuffer<float>& buffer) { | |||
| // Check we have enough channels | |||
| if (buffer.getNumChannels() >= 2) { | |||
| if (panValue > 0) { | |||
| // Balance is to the right - so linearly attenuate the left | |||
| const float leftGain {1 - panValue}; | |||
| juce::FloatVectorOperations::multiply(buffer.getWritePointer(0), | |||
| leftGain, | |||
| buffer.getNumSamples()); | |||
| } else if (panValue < 0) { | |||
| // Balance is to the left - so linearly attenuate the right | |||
| const float rightGain {1 + panValue}; | |||
| juce::FloatVectorOperations::multiply(buffer.getWritePointer(1), | |||
| rightGain, | |||
| buffer.getNumSamples()); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,186 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "WEFilters/AREnvelopeFollowerSquareLaw.h" | |||
| #include "ModulationSourceDefinition.hpp" | |||
| #include "PluginConfigurator.hpp" | |||
| struct ChainSlotBase { | |||
| bool isBypassed; | |||
| ChainSlotBase(bool newIsBypassed) : isBypassed(newIsBypassed) {} | |||
| virtual ~ChainSlotBase() = default; | |||
| virtual ChainSlotBase* clone() const = 0; | |||
| }; | |||
| /** | |||
| * Represents a gain stage in a slot in a processing chain. | |||
| */ | |||
| struct ChainSlotGainStage : ChainSlotBase { | |||
| // Linear 0 to 1 (or a little more) values | |||
| float gain; | |||
| // -1 to 1 values | |||
| float pan; | |||
| int numMainChannels; | |||
| std::array<WECore::AREnv::AREnvelopeFollowerSquareLaw, 2> meterEnvelopes; | |||
| ChainSlotGainStage(float newGain, float newPan, bool newIsBypassed, const juce::AudioProcessor::BusesLayout& busesLayout) | |||
| : ChainSlotBase(newIsBypassed), gain(newGain), pan(newPan), numMainChannels(busesLayout.getMainInputChannels()) { | |||
| _setUpEnvelopes(); | |||
| } | |||
| ~ChainSlotGainStage() = default; | |||
| ChainSlotGainStage* clone() const override { | |||
| return new ChainSlotGainStage(gain, pan, isBypassed, numMainChannels); | |||
| } | |||
| private: | |||
| void _setUpEnvelopes() { | |||
| for (auto& env : meterEnvelopes) { | |||
| env.setAttackTimeMs(1); | |||
| env.setReleaseTimeMs(50); | |||
| env.setFilterEnabled(false); | |||
| } | |||
| } | |||
| ChainSlotGainStage( | |||
| float newGain, | |||
| float newPan, | |||
| bool newIsBypassed, | |||
| int newNumMainChannels) | |||
| : ChainSlotBase(newIsBypassed), gain(newGain), pan(newPan), numMainChannels(newNumMainChannels) { | |||
| _setUpEnvelopes(); | |||
| } | |||
| }; | |||
| struct PluginParameterModulationSource { | |||
| // Definition of the modulation source | |||
| ModulationSourceDefinition definition; | |||
| // Amount of modulation to be applied (-1 : 1) | |||
| float modulationAmount; | |||
| PluginParameterModulationSource() : definition(0, MODULATION_TYPE::MACRO), modulationAmount(0) { } | |||
| PluginParameterModulationSource(ModulationSourceDefinition newDefinition, | |||
| float newModulationAmount) : | |||
| definition(newDefinition), | |||
| modulationAmount(newModulationAmount) { } | |||
| }; | |||
| struct PluginParameterModulationConfig { | |||
| // Name of the parameter being modulated | |||
| juce::String targetParameterName; | |||
| // Parameter value without modulation applied (0 : 1) | |||
| float restValue; | |||
| // All the sources being provided for this parameter | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> sources; | |||
| // Used when retrieving the parameter name from a juce::AudioProcessorParameter | |||
| static constexpr int PLUGIN_PARAMETER_NAME_LENGTH_LIMIT {30}; | |||
| PluginParameterModulationConfig() : restValue(0) {} | |||
| PluginParameterModulationConfig* clone() const { | |||
| auto newConfig = new PluginParameterModulationConfig(); | |||
| newConfig->targetParameterName = targetParameterName; | |||
| newConfig->restValue = restValue; | |||
| for (auto& source : sources) { | |||
| newConfig->sources.push_back(std::make_shared<PluginParameterModulationSource>(source->definition, source->modulationAmount)); | |||
| } | |||
| return newConfig; | |||
| } | |||
| }; | |||
| struct PluginModulationConfig { | |||
| bool isActive; | |||
| std::vector<std::shared_ptr<PluginParameterModulationConfig>> parameterConfigs; | |||
| PluginModulationConfig() : isActive(false) {} | |||
| PluginModulationConfig& operator=(const PluginModulationConfig& other) { | |||
| isActive = other.isActive; | |||
| parameterConfigs = other.parameterConfigs; | |||
| return *this; | |||
| } | |||
| PluginModulationConfig* clone() const { | |||
| auto newConfig = new PluginModulationConfig(); | |||
| newConfig->isActive = isActive; | |||
| for (auto& parameterConfig : parameterConfigs) { | |||
| newConfig->parameterConfigs.push_back(std::shared_ptr<PluginParameterModulationConfig>(parameterConfig->clone())); | |||
| } | |||
| return newConfig; | |||
| } | |||
| }; | |||
| struct PluginEditorBoundsContainer { | |||
| juce::Rectangle<int> editorBounds; | |||
| juce::Rectangle<int> displayArea; | |||
| PluginEditorBoundsContainer( | |||
| juce::Rectangle<int> newEditorBounds, | |||
| juce::Rectangle<int> newDisplayAreaBounds) : editorBounds(newEditorBounds), | |||
| displayArea(newDisplayAreaBounds) { } | |||
| }; | |||
| typedef std::optional<PluginEditorBoundsContainer> PluginEditorBounds; | |||
| /** | |||
| * Represents a plugin in a slot in a processing chain. | |||
| */ | |||
| struct ChainSlotPlugin : ChainSlotBase { | |||
| std::shared_ptr<juce::AudioPluginInstance> plugin; | |||
| std::shared_ptr<PluginModulationConfig> modulationConfig; | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback; | |||
| std::shared_ptr<PluginEditorBounds> editorBounds; | |||
| // AUs always require a sidechain input, but if Syndicate is loaded as a VST3 it might not have | |||
| // one, as VST3s don't require a sidechain input. So we keep this empty buffer to provide as a | |||
| // sidechain for AUs if needed. | |||
| std::unique_ptr<juce::AudioBuffer<float>> spareSCBuffer; | |||
| ChainSlotPlugin(std::shared_ptr<juce::AudioPluginInstance> newPlugin, | |||
| bool newIsBypassed, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| HostConfiguration config) | |||
| : ChainSlotBase(newIsBypassed), | |||
| plugin(newPlugin), | |||
| modulationConfig(std::make_shared<PluginModulationConfig>()), | |||
| getModulationValueCallback(newGetModulationValueCallback), | |||
| editorBounds(new PluginEditorBounds()), | |||
| spareSCBuffer(new juce::AudioBuffer<float>(config.layout.getMainInputChannels() * 2, config.blockSize)) {} | |||
| ~ChainSlotPlugin() = default; | |||
| ChainSlotPlugin* clone() const override { | |||
| auto newSpareSCBuffer = std::make_unique<juce::AudioBuffer<float>>(spareSCBuffer->getNumChannels(), spareSCBuffer->getNumSamples()); | |||
| return new ChainSlotPlugin(plugin, isBypassed, modulationConfig, getModulationValueCallback, editorBounds, std::move(newSpareSCBuffer)); | |||
| } | |||
| private: | |||
| ChainSlotPlugin( | |||
| std::shared_ptr<juce::AudioPluginInstance> newPlugin, | |||
| bool newIsBypassed, | |||
| std::shared_ptr<PluginModulationConfig> newModulationConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::shared_ptr<PluginEditorBounds> newEditorBounds, | |||
| std::unique_ptr<juce::AudioBuffer<float>> newSpareSCBuffer) | |||
| : ChainSlotBase(newIsBypassed), | |||
| plugin(newPlugin), | |||
| modulationConfig(std::shared_ptr<PluginModulationConfig>(newModulationConfig->clone())), | |||
| getModulationValueCallback(newGetModulationValueCallback), | |||
| editorBounds(newEditorBounds), | |||
| spareSCBuffer(std::move(newSpareSCBuffer)) {} | |||
| }; | |||
| @@ -0,0 +1,104 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "ChainSlots.hpp" | |||
| SCENARIO("ChainSlotGainStage: Clone works correctly") { | |||
| GIVEN("A gain stage slot") { | |||
| ChainSlotGainStage gainStage(0.5f, 0.0f, false, juce::AudioProcessor::BusesLayout()); | |||
| WHEN("It is cloned") { | |||
| ChainSlotGainStage* clonedGainStage = gainStage.clone(); | |||
| THEN("The cloned gain stage is equal to the original") { | |||
| CHECK(clonedGainStage->isBypassed == gainStage.isBypassed); | |||
| CHECK(clonedGainStage->gain == gainStage.gain); | |||
| CHECK(clonedGainStage->pan == gainStage.pan); | |||
| CHECK(clonedGainStage->numMainChannels == gainStage.numMainChannels); | |||
| // We don't need to copy the meter envelopes, but they should be configured exactly | |||
| // the same | |||
| CHECK(clonedGainStage->meterEnvelopes.size() == gainStage.meterEnvelopes.size()); | |||
| for (int envIndex {0}; envIndex < clonedGainStage->meterEnvelopes.size(); envIndex++) { | |||
| auto& clonedEnvelope = clonedGainStage->meterEnvelopes[envIndex]; | |||
| auto& originalEnvelope = gainStage.meterEnvelopes[envIndex]; | |||
| CHECK(clonedEnvelope.getAttackTimeMs() == originalEnvelope.getAttackTimeMs()); | |||
| CHECK(clonedEnvelope.getReleaseTimeMs() == originalEnvelope.getReleaseTimeMs()); | |||
| CHECK(clonedEnvelope.getFilterEnabled() == originalEnvelope.getFilterEnabled()); | |||
| } | |||
| } | |||
| delete clonedGainStage; | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainSlotPlugin: Clone works correctly") { | |||
| GIVEN("A plugin slot with some basic modulation") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.2f; | |||
| }; | |||
| auto pluginSlot = std::make_shared<ChainSlotPlugin>(plugin, false, modulationCallback, hostConfig); | |||
| REQUIRE(pluginSlot->spareSCBuffer != nullptr); | |||
| REQUIRE(pluginSlot->spareSCBuffer->getNumChannels() == 4); | |||
| REQUIRE(pluginSlot->spareSCBuffer->getNumSamples() == 10); | |||
| auto modulationConfig = std::make_shared<PluginModulationConfig>(); | |||
| modulationConfig->isActive = true; | |||
| auto parameterModulationConfig = std::make_shared<PluginParameterModulationConfig>(); | |||
| parameterModulationConfig->targetParameterName = "test param"; | |||
| parameterModulationConfig->restValue = 0.75f; | |||
| auto source = std::make_shared<PluginParameterModulationSource>(); | |||
| source->definition = ModulationSourceDefinition(3, MODULATION_TYPE::LFO); | |||
| source->modulationAmount = 0.8f; | |||
| parameterModulationConfig->sources.push_back(source); | |||
| modulationConfig->parameterConfigs.push_back(parameterModulationConfig); | |||
| pluginSlot->modulationConfig = modulationConfig; | |||
| WHEN("It is cloned") { | |||
| ChainSlotPlugin* clonedPluginSlot = pluginSlot->clone(); | |||
| THEN("The cloned plugin slot is equal to the original") { | |||
| CHECK(clonedPluginSlot->isBypassed == pluginSlot->isBypassed); | |||
| CHECK(clonedPluginSlot->plugin == pluginSlot->plugin); // Should be the same shared pointer | |||
| CHECK(clonedPluginSlot->modulationConfig != pluginSlot->modulationConfig); // Should be a different shared pointer | |||
| CHECK(clonedPluginSlot->getModulationValueCallback(0, MODULATION_TYPE::MACRO) == 1.2f); | |||
| CHECK(clonedPluginSlot->editorBounds == pluginSlot->editorBounds); // Should be the same shared pointer | |||
| CHECK(clonedPluginSlot->spareSCBuffer != pluginSlot->spareSCBuffer); // Should be a different shared pointer | |||
| CHECK(clonedPluginSlot->spareSCBuffer->getNumChannels() == pluginSlot->spareSCBuffer->getNumChannels()); | |||
| CHECK(clonedPluginSlot->spareSCBuffer->getNumSamples() == pluginSlot->spareSCBuffer->getNumSamples()); | |||
| // Check all the modulation | |||
| auto clonedModulationConfig = clonedPluginSlot->modulationConfig; | |||
| CHECK(clonedModulationConfig->isActive == modulationConfig->isActive); | |||
| CHECK(clonedModulationConfig->parameterConfigs.size() == modulationConfig->parameterConfigs.size()); | |||
| auto clonedParameterModulationConfig = clonedModulationConfig->parameterConfigs[0]; | |||
| CHECK(clonedParameterModulationConfig != parameterModulationConfig); // Should be a different shared pointer | |||
| CHECK(clonedParameterModulationConfig->targetParameterName == parameterModulationConfig->targetParameterName); | |||
| CHECK(clonedParameterModulationConfig->restValue == parameterModulationConfig->restValue); | |||
| CHECK(clonedParameterModulationConfig->sources.size() == parameterModulationConfig->sources.size()); | |||
| auto clonedSource = clonedParameterModulationConfig->sources[0]; | |||
| CHECK(clonedSource != source); // Should be a different shared pointer | |||
| CHECK(clonedSource->definition == source->definition); | |||
| CHECK(clonedSource->modulationAmount == source->modulationAmount); | |||
| } | |||
| delete clonedPluginSlot; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,132 @@ | |||
| #include "CloneableDelayLine.hpp" | |||
| /* | |||
| ============================================================================== | |||
| This file is part of the JUCE library. | |||
| Copyright (c) 2022 - Raw Material Software Limited | |||
| JUCE is an open source library subject to commercial or open-source | |||
| licensing. | |||
| By using JUCE, you agree to the terms of both the JUCE 7 End-User License | |||
| Agreement and JUCE Privacy Policy. | |||
| End User License Agreement: www.juce.com/juce-7-licence | |||
| Privacy Policy: www.juce.com/juce-privacy-policy | |||
| Or: You may also use this code under the terms of the GPL v3 (see | |||
| www.gnu.org/licenses). | |||
| JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
| EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
| DISCLAIMED. | |||
| ============================================================================== | |||
| */ | |||
| //============================================================================== | |||
| template <typename SampleType, typename InterpolationType> | |||
| CloneableDelayLine<SampleType, InterpolationType>::CloneableDelayLine() | |||
| : CloneableDelayLine (0) | |||
| { | |||
| } | |||
| template <typename SampleType, typename InterpolationType> | |||
| CloneableDelayLine<SampleType, InterpolationType>::CloneableDelayLine (int maximumDelayInSamples) | |||
| { | |||
| jassert (maximumDelayInSamples >= 0); | |||
| sampleRate = 44100.0; | |||
| setMaximumDelayInSamples (maximumDelayInSamples); | |||
| } | |||
| //============================================================================== | |||
| template <typename SampleType, typename InterpolationType> | |||
| void CloneableDelayLine<SampleType, InterpolationType>::setDelay (SampleType newDelayInSamples) | |||
| { | |||
| auto upperLimit = (SampleType) getMaximumDelayInSamples(); | |||
| jassert (juce::isPositiveAndNotGreaterThan (newDelayInSamples, upperLimit)); | |||
| delay = juce::jlimit ((SampleType) 0, upperLimit, newDelayInSamples); | |||
| delayInt = static_cast<int> (std::floor (delay)); | |||
| delayFrac = delay - (SampleType) delayInt; | |||
| updateInternalVariables(); | |||
| } | |||
| template <typename SampleType, typename InterpolationType> | |||
| SampleType CloneableDelayLine<SampleType, InterpolationType>::getDelay() const | |||
| { | |||
| return delay; | |||
| } | |||
| //============================================================================== | |||
| template <typename SampleType, typename InterpolationType> | |||
| void CloneableDelayLine<SampleType, InterpolationType>::prepare (const juce::dsp::ProcessSpec& spec) | |||
| { | |||
| jassert (spec.numChannels > 0); | |||
| bufferData.setSize ((int) spec.numChannels, totalSize, false, false, true); | |||
| writePos.resize (spec.numChannels); | |||
| readPos.resize (spec.numChannels); | |||
| v.resize (spec.numChannels); | |||
| sampleRate = spec.sampleRate; | |||
| reset(); | |||
| } | |||
| template <typename SampleType, typename InterpolationType> | |||
| void CloneableDelayLine<SampleType, InterpolationType>::setMaximumDelayInSamples (int maxDelayInSamples) | |||
| { | |||
| jassert (maxDelayInSamples >= 0); | |||
| totalSize = juce::jmax (4, maxDelayInSamples + 2); | |||
| bufferData.setSize ((int) bufferData.getNumChannels(), totalSize, false, false, true); | |||
| reset(); | |||
| } | |||
| template <typename SampleType, typename InterpolationType> | |||
| void CloneableDelayLine<SampleType, InterpolationType>::reset() | |||
| { | |||
| for (auto vec : { &writePos, &readPos }) | |||
| std::fill (vec->begin(), vec->end(), 0); | |||
| std::fill (v.begin(), v.end(), static_cast<SampleType> (0)); | |||
| bufferData.clear(); | |||
| } | |||
| //============================================================================== | |||
| template <typename SampleType, typename InterpolationType> | |||
| void CloneableDelayLine<SampleType, InterpolationType>::pushSample (int channel, SampleType sample) | |||
| { | |||
| bufferData.setSample (channel, writePos[(size_t) channel], sample); | |||
| writePos[(size_t) channel] = (writePos[(size_t) channel] + totalSize - 1) % totalSize; | |||
| } | |||
| template <typename SampleType, typename InterpolationType> | |||
| SampleType CloneableDelayLine<SampleType, InterpolationType>::popSample (int channel, SampleType delayInSamples, bool updateReadPointer) | |||
| { | |||
| if (delayInSamples >= 0) | |||
| setDelay (delayInSamples); | |||
| auto result = interpolateSample (channel); | |||
| if (updateReadPointer) | |||
| readPos[(size_t) channel] = (readPos[(size_t) channel] + totalSize - 1) % totalSize; | |||
| return result; | |||
| } | |||
| //============================================================================== | |||
| template class CloneableDelayLine<float, juce::dsp::DelayLineInterpolationTypes::None>; | |||
| template class CloneableDelayLine<double, juce::dsp::DelayLineInterpolationTypes::None>; | |||
| template class CloneableDelayLine<float, juce::dsp::DelayLineInterpolationTypes::Linear>; | |||
| template class CloneableDelayLine<double, juce::dsp::DelayLineInterpolationTypes::Linear>; | |||
| template class CloneableDelayLine<float, juce::dsp::DelayLineInterpolationTypes::Lagrange3rd>; | |||
| template class CloneableDelayLine<double, juce::dsp::DelayLineInterpolationTypes::Lagrange3rd>; | |||
| template class CloneableDelayLine<float, juce::dsp::DelayLineInterpolationTypes::Thiran>; | |||
| template class CloneableDelayLine<double, juce::dsp::DelayLineInterpolationTypes::Thiran>; | |||
| @@ -0,0 +1,257 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| // Borrowed from JUCE, but made cloneable | |||
| //============================================================================== | |||
| /** | |||
| A delay line processor featuring several algorithms for the fractional delay | |||
| calculation, block processing, and sample-by-sample processing useful when | |||
| modulating the delay in real time or creating a standard delay effect with | |||
| feedback. | |||
| Note: If you intend to change the delay in real time, you may want to smooth | |||
| changes to the delay systematically using either a ramp or a low-pass filter. | |||
| @see SmoothedValue, FirstOrderTPTFilter | |||
| @tags{DSP} | |||
| */ | |||
| template <typename SampleType, typename InterpolationType = juce::dsp::DelayLineInterpolationTypes::Linear> | |||
| class CloneableDelayLine | |||
| { | |||
| public: | |||
| //============================================================================== | |||
| /** Default constructor. */ | |||
| CloneableDelayLine(); | |||
| /** Constructor. */ | |||
| explicit CloneableDelayLine (int maximumDelayInSamples); | |||
| //============================================================================== | |||
| /** Sets the delay in samples. */ | |||
| void setDelay (SampleType newDelayInSamples); | |||
| /** Returns the current delay in samples. */ | |||
| SampleType getDelay() const; | |||
| //============================================================================== | |||
| /** Initialises the processor. */ | |||
| void prepare (const juce::dsp::ProcessSpec& spec); | |||
| /** Sets a new maximum delay in samples. | |||
| Also clears the delay line. | |||
| This may allocate internally, so you should never call it from the audio thread. | |||
| */ | |||
| void setMaximumDelayInSamples (int maxDelayInSamples); | |||
| /** Gets the maximum possible delay in samples. | |||
| For very short delay times, the result of getMaximumDelayInSamples() may | |||
| differ from the last value passed to setMaximumDelayInSamples(). | |||
| */ | |||
| int getMaximumDelayInSamples() const noexcept { return totalSize - 2; } | |||
| /** Resets the internal state variables of the processor. */ | |||
| void reset(); | |||
| //============================================================================== | |||
| /** Pushes a single sample into one channel of the delay line. | |||
| Use this function and popSample instead of process if you need to modulate | |||
| the delay in real time instead of using a fixed delay value, or if you want | |||
| to code a delay effect with a feedback loop. | |||
| @see setDelay, popSample, process | |||
| */ | |||
| void pushSample (int channel, SampleType sample); | |||
| /** Pops a single sample from one channel of the delay line. | |||
| Use this function to modulate the delay in real time or implement standard | |||
| delay effects with feedback. | |||
| @param channel the target channel for the delay line. | |||
| @param delayInSamples sets the wanted fractional delay in samples, or -1 | |||
| to use the value being used before or set with | |||
| setDelay function. | |||
| @param updateReadPointer should be set to true if you use the function | |||
| once for each sample, or false if you need | |||
| multi-tap delay capabilities. | |||
| @see setDelay, pushSample, process | |||
| */ | |||
| SampleType popSample (int channel, SampleType delayInSamples = -1, bool updateReadPointer = true); | |||
| //============================================================================== | |||
| /** Processes the input and output samples supplied in the processing context. | |||
| Can be used for block processing when the delay is not going to change | |||
| during processing. The delay must first be set by calling setDelay. | |||
| @see setDelay | |||
| */ | |||
| template <typename ProcessContext> | |||
| void process (const ProcessContext& context) noexcept | |||
| { | |||
| const auto& inputBlock = context.getInputBlock(); | |||
| auto& outputBlock = context.getOutputBlock(); | |||
| const auto numChannels = outputBlock.getNumChannels(); | |||
| const auto numSamples = outputBlock.getNumSamples(); | |||
| jassert (inputBlock.getNumChannels() == numChannels); | |||
| jassert (inputBlock.getNumChannels() == writePos.size()); | |||
| jassert (inputBlock.getNumSamples() == numSamples); | |||
| if (context.isBypassed) | |||
| { | |||
| outputBlock.copyFrom (inputBlock); | |||
| return; | |||
| } | |||
| for (size_t channel = 0; channel < numChannels; ++channel) | |||
| { | |||
| auto* inputSamples = inputBlock.getChannelPointer (channel); | |||
| auto* outputSamples = outputBlock.getChannelPointer (channel); | |||
| for (size_t i = 0; i < numSamples; ++i) | |||
| { | |||
| pushSample ((int) channel, inputSamples[i]); | |||
| outputSamples[i] = popSample ((int) channel); | |||
| } | |||
| } | |||
| } | |||
| CloneableDelayLine* clone() const { | |||
| return new CloneableDelayLine(*this); | |||
| } | |||
| // Public for tests | |||
| double sampleRate; | |||
| juce::AudioBuffer<SampleType> bufferData; | |||
| std::vector<SampleType> v; | |||
| std::vector<int> writePos, readPos; | |||
| SampleType delay = 0.0, delayFrac = 0.0; | |||
| int delayInt = 0, totalSize = 4; | |||
| SampleType alpha = 0.0; | |||
| private: | |||
| CloneableDelayLine(const CloneableDelayLine& other) : | |||
| sampleRate(other.sampleRate), | |||
| bufferData(other.bufferData), | |||
| v(other.v), | |||
| writePos(other.writePos), | |||
| readPos(other.readPos), | |||
| delay(other.delay), | |||
| delayFrac(other.delayFrac), | |||
| delayInt(other.delayInt), | |||
| totalSize(other.totalSize), | |||
| alpha(other.alpha) { | |||
| } | |||
| //============================================================================== | |||
| SampleType interpolateSample (int channel) | |||
| { | |||
| if constexpr (std::is_same_v<InterpolationType, juce::dsp::DelayLineInterpolationTypes::None>) | |||
| { | |||
| auto index = (readPos[(size_t) channel] + delayInt) % totalSize; | |||
| return bufferData.getSample (channel, index); | |||
| } | |||
| else if constexpr (std::is_same_v<InterpolationType, juce::dsp::DelayLineInterpolationTypes::Linear>) | |||
| { | |||
| auto index1 = readPos[(size_t) channel] + delayInt; | |||
| auto index2 = index1 + 1; | |||
| if (index2 >= totalSize) | |||
| { | |||
| index1 %= totalSize; | |||
| index2 %= totalSize; | |||
| } | |||
| auto value1 = bufferData.getSample (channel, index1); | |||
| auto value2 = bufferData.getSample (channel, index2); | |||
| return value1 + delayFrac * (value2 - value1); | |||
| } | |||
| else if constexpr (std::is_same_v<InterpolationType, juce::dsp::DelayLineInterpolationTypes::Lagrange3rd>) | |||
| { | |||
| auto index1 = readPos[(size_t) channel] + delayInt; | |||
| auto index2 = index1 + 1; | |||
| auto index3 = index2 + 1; | |||
| auto index4 = index3 + 1; | |||
| if (index4 >= totalSize) | |||
| { | |||
| index1 %= totalSize; | |||
| index2 %= totalSize; | |||
| index3 %= totalSize; | |||
| index4 %= totalSize; | |||
| } | |||
| auto* samples = bufferData.getReadPointer (channel); | |||
| auto value1 = samples[index1]; | |||
| auto value2 = samples[index2]; | |||
| auto value3 = samples[index3]; | |||
| auto value4 = samples[index4]; | |||
| auto d1 = delayFrac - 1.f; | |||
| auto d2 = delayFrac - 2.f; | |||
| auto d3 = delayFrac - 3.f; | |||
| auto c1 = -d1 * d2 * d3 / 6.f; | |||
| auto c2 = d2 * d3 * 0.5f; | |||
| auto c3 = -d1 * d3 * 0.5f; | |||
| auto c4 = d1 * d2 / 6.f; | |||
| return value1 * c1 + delayFrac * (value2 * c2 + value3 * c3 + value4 * c4); | |||
| } | |||
| else if constexpr (std::is_same_v<InterpolationType, juce::dsp::DelayLineInterpolationTypes::Thiran>) | |||
| { | |||
| auto index1 = readPos[(size_t) channel] + delayInt; | |||
| auto index2 = index1 + 1; | |||
| if (index2 >= totalSize) | |||
| { | |||
| index1 %= totalSize; | |||
| index2 %= totalSize; | |||
| } | |||
| auto value1 = bufferData.getSample (channel, index1); | |||
| auto value2 = bufferData.getSample (channel, index2); | |||
| auto output = juce::approximatelyEqual (delayFrac, (SampleType) 0) ? value1 : value2 + alpha * (value1 - v[(size_t) channel]); | |||
| v[(size_t) channel] = output; | |||
| return output; | |||
| } | |||
| } | |||
| //============================================================================== | |||
| void updateInternalVariables() | |||
| { | |||
| if constexpr (std::is_same_v<InterpolationType, juce::dsp::DelayLineInterpolationTypes::Lagrange3rd>) | |||
| { | |||
| if (delayFrac < (SampleType) 2.0 && delayInt >= 1) | |||
| { | |||
| delayFrac++; | |||
| delayInt--; | |||
| } | |||
| } | |||
| else if constexpr (std::is_same_v<InterpolationType, juce::dsp::DelayLineInterpolationTypes::Thiran>) | |||
| { | |||
| if (delayFrac < (SampleType) 0.618 && delayInt >= 1) | |||
| { | |||
| delayFrac++; | |||
| delayInt--; | |||
| } | |||
| alpha = (1 - delayFrac) / (1 + delayFrac); | |||
| } | |||
| } | |||
| }; | |||
| @@ -0,0 +1,47 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "CloneableDelayLine.hpp" | |||
| SCENARIO("CloneableDelayLine: Clone works correctly") { | |||
| GIVEN("A CloneableDelayLine") { | |||
| CloneableDelayLine<float, juce::dsp::DelayLineInterpolationTypes::None> delayLine(0); | |||
| // Set some unique values so we can test for them later | |||
| delayLine.setDelay(1000); | |||
| // Populate the state with some unique values so we can test for them later | |||
| constexpr int numFilterChannels {2}; | |||
| delayLine.prepare({48000, 64, numFilterChannels}); | |||
| juce::AudioBuffer<float> buffer(numFilterChannels, 64); | |||
| for (int channel {0}; channel < numFilterChannels; ++channel) { | |||
| for (int sample {0}; sample < buffer.getNumSamples(); ++sample) { | |||
| buffer.setSample(channel, sample, (sample % 10) / 10.0f); | |||
| } | |||
| } | |||
| juce::dsp::AudioBlock<float> block(juce::dsp::AudioBlock<float>(buffer).getSubsetChannelBlock(0, numFilterChannels)); | |||
| juce::dsp::ProcessContextReplacing context(block); | |||
| delayLine.process(context); | |||
| WHEN("It is cloned") { | |||
| auto clonedDelayLine = delayLine.clone(); | |||
| THEN("The cloned delay line is equal to the original") { | |||
| CHECK(clonedDelayLine->sampleRate == delayLine.sampleRate); | |||
| CHECK(clonedDelayLine->bufferData == delayLine.bufferData); | |||
| CHECK(clonedDelayLine->v == delayLine.v); | |||
| CHECK(clonedDelayLine->writePos == delayLine.writePos); | |||
| CHECK(clonedDelayLine->readPos == delayLine.readPos); | |||
| CHECK(clonedDelayLine->delay == delayLine.delay); | |||
| CHECK(clonedDelayLine->delayFrac == delayLine.delayFrac); | |||
| CHECK(clonedDelayLine->delayInt == delayLine.delayInt); | |||
| CHECK(clonedDelayLine->totalSize == delayLine.totalSize); | |||
| CHECK(clonedDelayLine->alpha == delayLine.alpha); | |||
| } | |||
| delete clonedDelayLine; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,118 @@ | |||
| #include "CloneableLRFilter.hpp" | |||
| //============================================================================== | |||
| template <typename SampleType> | |||
| CloneableLRFilter<SampleType>::CloneableLRFilter() | |||
| { | |||
| update(); | |||
| } | |||
| //============================================================================== | |||
| template <typename SampleType> | |||
| void CloneableLRFilter<SampleType>::setType (Type newType) | |||
| { | |||
| filterType = newType; | |||
| } | |||
| template <typename SampleType> | |||
| void CloneableLRFilter<SampleType>::setCutoffFrequency (SampleType newCutoffFrequencyHz) | |||
| { | |||
| jassert (juce::isPositiveAndBelow (newCutoffFrequencyHz, static_cast<SampleType> (sampleRate * 0.5))); | |||
| cutoffFrequency = newCutoffFrequencyHz; | |||
| update(); | |||
| } | |||
| //============================================================================== | |||
| template <typename SampleType> | |||
| void CloneableLRFilter<SampleType>::prepare (const juce::dsp::ProcessSpec& spec) | |||
| { | |||
| jassert (spec.sampleRate > 0); | |||
| jassert (spec.numChannels > 0); | |||
| sampleRate = spec.sampleRate; | |||
| update(); | |||
| s1.resize (spec.numChannels); | |||
| s2.resize (spec.numChannels); | |||
| s3.resize (spec.numChannels); | |||
| s4.resize (spec.numChannels); | |||
| reset(); | |||
| } | |||
| template <typename SampleType> | |||
| void CloneableLRFilter<SampleType>::reset() | |||
| { | |||
| for (auto s : { &s1, &s2, &s3, &s4 }) | |||
| std::fill (s->begin(), s->end(), static_cast<SampleType> (0)); | |||
| } | |||
| template <typename SampleType> | |||
| void CloneableLRFilter<SampleType>::snapToZero() noexcept | |||
| { | |||
| for (auto s : { &s1, &s2, &s3, &s4 }) | |||
| for (auto& element : *s) | |||
| juce::dsp::util::snapToZero (element); | |||
| } | |||
| //============================================================================== | |||
| template <typename SampleType> | |||
| SampleType CloneableLRFilter<SampleType>::processSample (int channel, SampleType inputValue) | |||
| { | |||
| auto yH = (inputValue - (R2 + g) * s1[(size_t) channel] - s2[(size_t) channel]) * h; | |||
| auto yB = g * yH + s1[(size_t) channel]; | |||
| s1[(size_t) channel] = g * yH + yB; | |||
| auto yL = g * yB + s2[(size_t) channel]; | |||
| s2[(size_t) channel] = g * yB + yL; | |||
| if (filterType == Type::allpass) | |||
| return yL - R2 * yB + yH; | |||
| auto yH2 = ((filterType == Type::lowpass ? yL : yH) - (R2 + g) * s3[(size_t) channel] - s4[(size_t) channel]) * h; | |||
| auto yB2 = g * yH2 + s3[(size_t) channel]; | |||
| s3[(size_t) channel] = g * yH2 + yB2; | |||
| auto yL2 = g * yB2 + s4[(size_t) channel]; | |||
| s4[(size_t) channel] = g * yB2 + yL2; | |||
| return filterType == Type::lowpass ? yL2 : yH2; | |||
| } | |||
| template <typename SampleType> | |||
| void CloneableLRFilter<SampleType>::processSample (int channel, SampleType inputValue, SampleType &outputLow, SampleType &outputHigh) | |||
| { | |||
| auto yH = (inputValue - (R2 + g) * s1[(size_t) channel] - s2[(size_t) channel]) * h; | |||
| auto yB = g * yH + s1[(size_t) channel]; | |||
| s1[(size_t) channel] = g * yH + yB; | |||
| auto yL = g * yB + s2[(size_t) channel]; | |||
| s2[(size_t) channel] = g * yB + yL; | |||
| auto yH2 = (yL - (R2 + g) * s3[(size_t) channel] - s4[(size_t) channel]) * h; | |||
| auto yB2 = g * yH2 + s3[(size_t) channel]; | |||
| s3[(size_t) channel] = g * yH2 + yB2; | |||
| auto yL2 = g * yB2 + s4[(size_t) channel]; | |||
| s4[(size_t) channel] = g * yB2 + yL2; | |||
| outputLow = yL2; | |||
| outputHigh = yL - R2 * yB + yH - yL2; | |||
| } | |||
| template <typename SampleType> | |||
| void CloneableLRFilter<SampleType>::update() | |||
| { | |||
| g = (SampleType) std::tan (juce::MathConstants<double>::pi * cutoffFrequency / sampleRate); | |||
| R2 = (SampleType) std::sqrt (2.0); | |||
| h = (SampleType) (1.0 / (1.0 + R2 * g + g * g)); | |||
| } | |||
| //============================================================================== | |||
| template class CloneableLRFilter<float>; | |||
| template class CloneableLRFilter<double>; | |||
| @@ -0,0 +1,125 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| // Borrowed from JUCE, but made cloneable | |||
| /** | |||
| A filter class designed to perform multi-band separation using the TPT | |||
| (Topology-Preserving Transform) structure. | |||
| Linkwitz-Riley filters are widely used in audio crossovers that have two outputs, | |||
| a low-pass and a high-pass, such that their sum is equivalent to an all-pass filter | |||
| with a flat magnitude frequency response. The Linkwitz-Riley filters available in | |||
| this class are designed to have a -24 dB/octave slope (LR 4th order). | |||
| @tags{DSP} | |||
| */ | |||
| template <typename SampleType> | |||
| class CloneableLRFilter | |||
| { | |||
| public: | |||
| //============================================================================== | |||
| using Type = juce::dsp::LinkwitzRileyFilterType; | |||
| //============================================================================== | |||
| /** Constructor. */ | |||
| CloneableLRFilter(); | |||
| //============================================================================== | |||
| /** Sets the filter type. */ | |||
| void setType (Type newType); | |||
| /** Sets the cutoff frequency of the filter in Hz. */ | |||
| void setCutoffFrequency (SampleType newCutoffFrequencyHz); | |||
| //============================================================================== | |||
| /** Returns the type of the filter. */ | |||
| Type getType() const noexcept { return filterType; } | |||
| /** Returns the cutoff frequency of the filter. */ | |||
| SampleType getCutoffFrequency() const noexcept { return cutoffFrequency; } | |||
| //============================================================================== | |||
| /** Initialises the filter. */ | |||
| void prepare (const juce::dsp::ProcessSpec& spec); | |||
| /** Resets the internal state variables of the filter. */ | |||
| void reset(); | |||
| //============================================================================== | |||
| /** Processes the input and output samples supplied in the processing context. */ | |||
| template <typename ProcessContext> | |||
| void process (const ProcessContext& context) noexcept | |||
| { | |||
| const auto& inputBlock = context.getInputBlock(); | |||
| auto& outputBlock = context.getOutputBlock(); | |||
| const auto numChannels = outputBlock.getNumChannels(); | |||
| const auto numSamples = outputBlock.getNumSamples(); | |||
| jassert (inputBlock.getNumChannels() <= s1.size()); | |||
| jassert (inputBlock.getNumChannels() == numChannels); | |||
| jassert (inputBlock.getNumSamples() == numSamples); | |||
| if (context.isBypassed) | |||
| { | |||
| outputBlock.copyFrom (inputBlock); | |||
| return; | |||
| } | |||
| for (size_t channel = 0; channel < numChannels; ++channel) | |||
| { | |||
| auto* inputSamples = inputBlock.getChannelPointer (channel); | |||
| auto* outputSamples = outputBlock.getChannelPointer (channel); | |||
| for (size_t i = 0; i < numSamples; ++i) | |||
| outputSamples[i] = processSample ((int) channel, inputSamples[i]); | |||
| } | |||
| #if JUCE_DSP_ENABLE_SNAP_TO_ZERO | |||
| snapToZero(); | |||
| #endif | |||
| } | |||
| /** Performs the filter operation on a single sample at a time. */ | |||
| SampleType processSample (int channel, SampleType inputValue); | |||
| /** Performs the filter operation on a single sample at a time, and returns both | |||
| the low-pass and the high-pass outputs of the TPT structure. | |||
| */ | |||
| void processSample (int channel, SampleType inputValue, SampleType &outputLow, SampleType &outputHigh); | |||
| /** Ensure that the state variables are rounded to zero if the state | |||
| variables are denormals. This is only needed if you are doing | |||
| sample by sample processing. | |||
| */ | |||
| void snapToZero() noexcept; | |||
| CloneableLRFilter<SampleType>* clone() const { | |||
| return new CloneableLRFilter<SampleType>(*this); | |||
| } | |||
| // Public for tests | |||
| SampleType g, R2, h; | |||
| std::vector<SampleType> s1, s2, s3, s4; | |||
| double sampleRate = 44100.0; | |||
| SampleType cutoffFrequency = 2000.0; | |||
| Type filterType = Type::lowpass; | |||
| private: | |||
| CloneableLRFilter(const CloneableLRFilter<SampleType>& other) : | |||
| g(other.g), | |||
| R2(other.R2), | |||
| h(other.h), | |||
| s1(other.s1), | |||
| s2(other.s2), | |||
| s3(other.s3), | |||
| s4(other.s4), | |||
| sampleRate(other.sampleRate), | |||
| cutoffFrequency(other.cutoffFrequency), | |||
| filterType(other.filterType) { | |||
| } | |||
| //============================================================================== | |||
| void update(); | |||
| }; | |||
| @@ -0,0 +1,48 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "CloneableLRFilter.hpp" | |||
| SCENARIO("CloneableLRFilter: Clone works correctly") { | |||
| GIVEN("A CloneableLRFilter") { | |||
| CloneableLRFilter<float> filter; | |||
| // Set some unique values so we can test for them later | |||
| filter.setType(juce::dsp::LinkwitzRileyFilterType::lowpass); | |||
| filter.setCutoffFrequency(1000); | |||
| // Populate the state with some unique values so we can test for them later | |||
| constexpr int numFilterChannels {2}; | |||
| filter.prepare({48000, 64, numFilterChannels}); | |||
| juce::AudioBuffer<float> buffer(numFilterChannels, 64); | |||
| for (int channel {0}; channel < numFilterChannels; ++channel) { | |||
| for (int sample {0}; sample < buffer.getNumSamples(); ++sample) { | |||
| buffer.setSample(channel, sample, (sample % 10) / 10.0f); | |||
| } | |||
| } | |||
| juce::dsp::AudioBlock<float> block(juce::dsp::AudioBlock<float>(buffer).getSubsetChannelBlock(0, numFilterChannels)); | |||
| juce::dsp::ProcessContextReplacing context(block); | |||
| filter.process(context); | |||
| WHEN("It is cloned") { | |||
| auto clonedFilter = filter.clone(); | |||
| THEN("The cloned filter is equal to the original") { | |||
| CHECK(clonedFilter->g == filter.g); | |||
| CHECK(clonedFilter->R2 == filter.R2); | |||
| CHECK(clonedFilter->h == filter.h); | |||
| CHECK(clonedFilter->s1 == filter.s1); | |||
| CHECK(clonedFilter->s2 == filter.s2); | |||
| CHECK(clonedFilter->s3 == filter.s3); | |||
| CHECK(clonedFilter->s4 == filter.s4); | |||
| CHECK(clonedFilter->sampleRate == filter.sampleRate); | |||
| CHECK(clonedFilter->cutoffFrequency == filter.cutoffFrequency); | |||
| CHECK(clonedFilter->filterType == filter.filterType); | |||
| } | |||
| delete clonedFilter; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| #pragma once | |||
| #include "RichterLFO/RichterLFO.h" | |||
| #include "WEFilters/AREnvelopeFollowerSquareLaw.h" | |||
| namespace ModelInterface { | |||
| class CloneableLFO : public WECore::Richter::RichterLFO { | |||
| public: | |||
| CloneableLFO() : WECore::Richter::RichterLFO() {} | |||
| CloneableLFO* clone() const { | |||
| return new CloneableLFO(*this); | |||
| } | |||
| void setFreqModulationSources(std::vector<WECore::ModulationSourceWrapper<double>> sources) { | |||
| _freqModulationSources = sources; | |||
| } | |||
| void setDepthModulationSources(std::vector<WECore::ModulationSourceWrapper<double>> sources) { | |||
| _depthModulationSources = sources; | |||
| } | |||
| void setPhaseModulationSources(std::vector<WECore::ModulationSourceWrapper<double>> sources) { | |||
| _phaseModulationSources = sources; | |||
| } | |||
| private: | |||
| CloneableLFO(const CloneableLFO& other) { | |||
| _wave = other._wave; | |||
| _outputMode = other._outputMode; | |||
| _indexOffset = other._indexOffset; | |||
| _bypassSwitch = other._bypassSwitch; | |||
| _tempoSyncSwitch = other._tempoSyncSwitch; | |||
| _phaseSyncSwitch = other._phaseSyncSwitch; | |||
| _invertSwitch = other._invertSwitch; | |||
| _needsSeekOffsetCalc = other._needsSeekOffsetCalc; | |||
| _tempoNumer = other._tempoNumer; | |||
| _tempoDenom = other._tempoDenom; | |||
| _rawFreq = other._rawFreq; | |||
| _rawDepth = other._rawDepth; | |||
| _manualPhase = other._manualPhase; | |||
| _sampleRate = other._sampleRate; | |||
| _bpm = other._bpm; | |||
| _wavetablePosition = other._wavetablePosition; | |||
| _waveArrayPointer = other._waveArrayPointer; | |||
| _cachedOutput = other._cachedOutput; | |||
| _freqModulationSources = other._freqModulationSources; | |||
| _depthModulationSources = other._depthModulationSources; | |||
| _phaseModulationSources = other._phaseModulationSources; | |||
| } | |||
| }; | |||
| class CloneableEnvelopeFollower : public WECore::AREnv::AREnvelopeFollowerSquareLaw { | |||
| public: | |||
| CloneableEnvelopeFollower() : WECore::AREnv::AREnvelopeFollowerSquareLaw() {} | |||
| CloneableEnvelopeFollower* clone() const { | |||
| return new CloneableEnvelopeFollower(*this); | |||
| } | |||
| private: | |||
| CloneableEnvelopeFollower(const CloneableEnvelopeFollower& other) { | |||
| _envVal = other._envVal; | |||
| _attackTimeMs = other._attackTimeMs; | |||
| _releaseTimeMs = other._releaseTimeMs; | |||
| _attackCoef = other._attackCoef; | |||
| _releaseCoef = other._releaseCoef; | |||
| _filterEnabled = other._filterEnabled; | |||
| _lowCutFilter = other._lowCutFilter.clone(); | |||
| _highCutFilter = other._highCutFilter.clone(); | |||
| _sampleRate = other._sampleRate; | |||
| _cachedOutput = other._cachedOutput; | |||
| } | |||
| }; | |||
| } | |||
| @@ -0,0 +1,107 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "CloneableSources.hpp" | |||
| SCENARIO("CloneableLFO: Clone works correctly") { | |||
| GIVEN("A CloneableLFO") { | |||
| ModelInterface::CloneableLFO lfo; | |||
| const auto freqSourceLFO = std::make_shared<ModelInterface::CloneableLFO>(); | |||
| const auto depthSourceLFO = std::make_shared<ModelInterface::CloneableLFO>(); | |||
| const auto phaseSourceLFO = std::make_shared<ModelInterface::CloneableLFO>(); | |||
| // Set some unique values so we can test for them later | |||
| lfo.setBypassSwitch(true); | |||
| lfo.setPhaseSyncSwitch(true); | |||
| lfo.setTempoSyncSwitch(true); | |||
| lfo.setInvertSwitch(true); | |||
| lfo.setWave(WECore::Richter::Parameters::WAVE.SQUARE); | |||
| lfo.setOutputMode(WECore::Richter::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| lfo.setTempoNumer(2); | |||
| lfo.setTempoDenom(3); | |||
| lfo.setFreq(4.5); | |||
| lfo.setDepth(0.7); | |||
| lfo.setManualPhase(250); | |||
| lfo.setSampleRate(48000); | |||
| lfo.addFreqModulationSource(freqSourceLFO); | |||
| lfo.setFreqModulationAmount(0, 0.3); | |||
| lfo.addDepthModulationSource(depthSourceLFO); | |||
| lfo.setDepthModulationAmount(0, 0.4); | |||
| lfo.addPhaseModulationSource(phaseSourceLFO); | |||
| lfo.setPhaseModulationAmount(0, 0.5); | |||
| // Set up some internal state | |||
| lfo.prepareForNextBuffer(110, 5); | |||
| lfo.getNextOutput(0.5); | |||
| lfo.getNextOutput(0.5); | |||
| lfo.getNextOutput(0.5); | |||
| WHEN("It is cloned") { | |||
| ModelInterface::CloneableLFO* clonedLFO = lfo.clone(); | |||
| THEN("The cloned LFO is equal to the original") { | |||
| CHECK(clonedLFO->getBypassSwitch() == lfo.getBypassSwitch()); | |||
| CHECK(clonedLFO->getPhaseSyncSwitch() == lfo.getPhaseSyncSwitch()); | |||
| CHECK(clonedLFO->getTempoSyncSwitch() == lfo.getTempoSyncSwitch()); | |||
| CHECK(clonedLFO->getInvertSwitch() == lfo.getInvertSwitch()); | |||
| CHECK(clonedLFO->getWave() == lfo.getWave()); | |||
| CHECK(clonedLFO->getOutputMode() == lfo.getOutputMode()); | |||
| CHECK(clonedLFO->getTempoNumer() == lfo.getTempoNumer()); | |||
| CHECK(clonedLFO->getTempoDenom() == lfo.getTempoDenom()); | |||
| CHECK(clonedLFO->getFreq() == lfo.getFreq()); | |||
| CHECK(clonedLFO->getDepth() == lfo.getDepth()); | |||
| CHECK(clonedLFO->getManualPhase() == lfo.getManualPhase()); | |||
| // CHECK(clonedLFO->getSampleRate() == lfo.getSampleRate()); | |||
| CHECK(clonedLFO->getLastOutput() == lfo.getLastOutput()); | |||
| CHECK(clonedLFO->getNextOutput(0.5) == lfo.getNextOutput(0.5)); | |||
| REQUIRE(clonedLFO->getFreqModulationSources().size() == 1); | |||
| CHECK(clonedLFO->getFreqModulationSources()[0].source == freqSourceLFO); | |||
| CHECK(clonedLFO->getFreqModulationSources()[0].amount == 0.3); | |||
| REQUIRE(clonedLFO->getFreqModulationSources().size() == 1); | |||
| CHECK(clonedLFO->getDepthModulationSources()[0].source == depthSourceLFO); | |||
| CHECK(clonedLFO->getDepthModulationSources()[0].amount == 0.4); | |||
| REQUIRE(clonedLFO->getPhaseModulationSources().size() == 1); | |||
| CHECK(clonedLFO->getPhaseModulationSources()[0].source == phaseSourceLFO); | |||
| CHECK(clonedLFO->getPhaseModulationSources()[0].amount == 0.5); | |||
| } | |||
| delete clonedLFO; | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("CloneableEnvelopeFollower: Clone works correctly") { | |||
| GIVEN("A CloneableEnvelopeFollower") { | |||
| ModelInterface::CloneableEnvelopeFollower envelope; | |||
| // Set some unique values so we can test for them later | |||
| envelope.setSampleRate(48000); | |||
| envelope.setAttackTimeMs(1.2); | |||
| envelope.setReleaseTimeMs(3.4); | |||
| envelope.setFilterEnabled(true); | |||
| envelope.setLowCutHz(21); | |||
| envelope.setHighCutHz(530); | |||
| // Set up some internal state | |||
| envelope.getNextOutput(0.4); | |||
| envelope.getNextOutput(0.5); | |||
| envelope.getNextOutput(0.8); | |||
| WHEN("It is cloned") { | |||
| ModelInterface::CloneableEnvelopeFollower* clonedEnvelope = envelope.clone(); | |||
| THEN("The cloned envelope is equal to the original") { | |||
| CHECK(clonedEnvelope->getAttackTimeMs() == envelope.getAttackTimeMs()); | |||
| CHECK(clonedEnvelope->getReleaseTimeMs() == envelope.getReleaseTimeMs()); | |||
| CHECK(clonedEnvelope->getFilterEnabled() == envelope.getFilterEnabled()); | |||
| CHECK(clonedEnvelope->getLowCutHz() == envelope.getLowCutHz()); | |||
| CHECK(clonedEnvelope->getHighCutHz() == envelope.getHighCutHz()); | |||
| // CHECK(clonedEnvelope->getSampleRate() == envelope.getSampleRate()); | |||
| CHECK(clonedEnvelope->getLastOutput() == envelope.getLastOutput()); | |||
| CHECK(clonedEnvelope->getNextOutput(0.5) == envelope.getNextOutput(0.5)); | |||
| } | |||
| delete clonedEnvelope; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,93 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginChain.hpp" | |||
| #include "CloneableLRFilter.hpp" | |||
| struct BandState { | |||
| bool isSoloed; | |||
| std::shared_ptr<PluginChain> chain; | |||
| BandState() : isSoloed(false) { | |||
| } | |||
| }; | |||
| class CrossoverState { | |||
| public: | |||
| // Num low/highpass filters = num bands - 1 (= num crossovers) | |||
| std::vector<std::shared_ptr<CloneableLRFilter<float>>> lowpassFilters; | |||
| std::vector<std::shared_ptr<CloneableLRFilter<float>>> highpassFilters; | |||
| // Num allpass filters = num bands - 2 | |||
| std::vector<std::shared_ptr<CloneableLRFilter<float>>> allpassFilters; | |||
| // Num buffers = num bands - 1 (= num crossovers) | |||
| std::vector<juce::AudioBuffer<float>> buffers; | |||
| std::vector<BandState> bands; | |||
| HostConfiguration config; | |||
| // We only need to implement solo at this level - chains handle bypass and mute themselves | |||
| int numBandsSoloed; | |||
| CrossoverState() : numBandsSoloed(0) {} | |||
| CrossoverState* clone() const { | |||
| auto newState = new CrossoverState(); | |||
| const int numFilterChannels {canDoStereoSplitTypes(config.layout) ? 2 : 1}; | |||
| for (auto& filter : lowpassFilters) { | |||
| newState->lowpassFilters.emplace_back(filter->clone()); | |||
| } | |||
| for (auto& filter : highpassFilters) { | |||
| newState->highpassFilters.emplace_back(filter->clone()); | |||
| } | |||
| for (auto& filter : allpassFilters) { | |||
| newState->allpassFilters.emplace_back(filter->clone()); | |||
| } | |||
| for (auto& buffer : buffers) { | |||
| newState->buffers.emplace_back(buffer); | |||
| } | |||
| for (auto& band : bands) { | |||
| newState->bands.emplace_back(); | |||
| newState->bands.back().isSoloed = band.isSoloed; | |||
| newState->bands.back().chain = band.chain; | |||
| } | |||
| newState->config = config; | |||
| newState->numBandsSoloed = numBandsSoloed; | |||
| return newState; | |||
| } | |||
| }; | |||
| inline std::shared_ptr<CrossoverState> createDefaultCrossoverState(HostConfiguration newConfig) { | |||
| auto state = std::make_shared<CrossoverState>(); | |||
| // Initialise configuration for two bands | |||
| constexpr int DEFAULT_FREQ {1000}; | |||
| state->lowpassFilters.emplace_back(new CloneableLRFilter<float>()); | |||
| state->lowpassFilters[0]->setType(juce::dsp::LinkwitzRileyFilterType::lowpass); | |||
| state->lowpassFilters[0]->setCutoffFrequency(DEFAULT_FREQ); | |||
| state->highpassFilters.emplace_back(new CloneableLRFilter<float>()); | |||
| state->highpassFilters[0]->setType(juce::dsp::LinkwitzRileyFilterType::highpass); | |||
| state->highpassFilters[0]->setCutoffFrequency(DEFAULT_FREQ); | |||
| state->buffers.emplace_back(); | |||
| state->bands.emplace_back(); | |||
| state->bands.emplace_back(); | |||
| state->config = newConfig; | |||
| return state; | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "CrossoverState.hpp" | |||
| #include "CrossoverMutators.hpp" | |||
| #include "CrossoverProcessors.hpp" | |||
| #include "ChainMutators.hpp" | |||
| SCENARIO("CrossoverState: Clone works correctly") { | |||
| GIVEN("A CrossoverState") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto crossover = createDefaultCrossoverState(hostConfig); | |||
| // Set some unique values so we can test for them later | |||
| CrossoverMutators::addBand(crossover); | |||
| CrossoverMutators::setCrossoverFrequency(crossover, 0, 100); | |||
| CrossoverMutators::setCrossoverFrequency(crossover, 1, 200); | |||
| CrossoverMutators::setIsSoloed(crossover, 1, true); | |||
| auto chain = std::make_shared<PluginChain>([](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.2f; | |||
| }); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| ChainMutators::insertPlugin(chain, plugin, 0, hostConfig); | |||
| CrossoverMutators::setPluginChain(crossover, 1, chain); | |||
| // Set the host configuration (and filter states) | |||
| CrossoverProcessors::prepareToPlay(*crossover.get(), hostConfig.sampleRate, hostConfig.blockSize, hostConfig.layout); | |||
| // Populate the buffers | |||
| for (size_t bufferIndex {0}; bufferIndex < crossover->buffers.size(); bufferIndex++) { | |||
| for (size_t channelIndex {0}; channelIndex < crossover->buffers[bufferIndex].getNumChannels(); channelIndex++) { | |||
| for (size_t sampleIndex {0}; sampleIndex < crossover->buffers[bufferIndex].getNumSamples(); sampleIndex++) { | |||
| crossover->buffers[bufferIndex].getWritePointer(channelIndex)[sampleIndex] = 0.5f; | |||
| } | |||
| } | |||
| } | |||
| REQUIRE(crossover->lowpassFilters.size() == 2); | |||
| REQUIRE(crossover->highpassFilters.size() == 2); | |||
| REQUIRE(crossover->allpassFilters.size() == 1); | |||
| REQUIRE(crossover->buffers.size() == 2); | |||
| REQUIRE(crossover->buffers[0].getNumChannels() == 2); | |||
| REQUIRE(crossover->buffers[0].getNumSamples() == 10); | |||
| REQUIRE(crossover->buffers[1].getNumChannels() == 2); | |||
| REQUIRE(crossover->buffers[1].getNumSamples() == 10); | |||
| REQUIRE(crossover->bands.size() == 3); | |||
| WHEN("It is cloned") { | |||
| CrossoverState* clonedCrossover = crossover->clone(); | |||
| THEN("The cloned crossover is equal to the original") { | |||
| CHECK(clonedCrossover->lowpassFilters.size() == crossover->lowpassFilters.size()); | |||
| CHECK(clonedCrossover->highpassFilters.size() == crossover->highpassFilters.size()); | |||
| CHECK(clonedCrossover->allpassFilters.size() == crossover->allpassFilters.size()); | |||
| CHECK(clonedCrossover->buffers.size() == crossover->buffers.size()); | |||
| CHECK(clonedCrossover->bands.size() == crossover->bands.size()); | |||
| CHECK(clonedCrossover->config.layout == crossover->config.layout); | |||
| CHECK(clonedCrossover->config.sampleRate == crossover->config.sampleRate); | |||
| CHECK(clonedCrossover->config.blockSize == crossover->config.blockSize); | |||
| CHECK(clonedCrossover->numBandsSoloed == crossover->numBandsSoloed); | |||
| // Check the filters | |||
| for (size_t filterIndex {0}; filterIndex < crossover->lowpassFilters.size(); filterIndex++) { | |||
| CHECK(clonedCrossover->lowpassFilters[filterIndex]->getType() == crossover->lowpassFilters[filterIndex]->getType()); | |||
| CHECK(clonedCrossover->lowpassFilters[filterIndex]->getCutoffFrequency() == crossover->lowpassFilters[filterIndex]->getCutoffFrequency()); | |||
| CHECK(clonedCrossover->highpassFilters[filterIndex]->getType() == crossover->highpassFilters[filterIndex]->getType()); | |||
| CHECK(clonedCrossover->highpassFilters[filterIndex]->getCutoffFrequency() == crossover->highpassFilters[filterIndex]->getCutoffFrequency()); | |||
| } | |||
| for (size_t filterIndex {0}; filterIndex < crossover->allpassFilters.size(); filterIndex++) { | |||
| CHECK(clonedCrossover->allpassFilters[filterIndex]->getType() == crossover->allpassFilters[filterIndex]->getType()); | |||
| CHECK(clonedCrossover->allpassFilters[filterIndex]->getCutoffFrequency() == crossover->allpassFilters[filterIndex]->getCutoffFrequency()); | |||
| } | |||
| // Check the buffers | |||
| for (size_t bufferIndex {0}; bufferIndex < crossover->buffers.size(); bufferIndex++) { | |||
| CHECK(clonedCrossover->buffers[bufferIndex].getNumChannels() == crossover->buffers[bufferIndex].getNumChannels()); | |||
| CHECK(clonedCrossover->buffers[bufferIndex].getNumSamples() == crossover->buffers[bufferIndex].getNumSamples()); | |||
| for (size_t channelIndex {0}; channelIndex < crossover->buffers[bufferIndex].getNumChannels(); channelIndex++) { | |||
| for (size_t sampleIndex {0}; sampleIndex < crossover->buffers[bufferIndex].getNumSamples(); sampleIndex++) { | |||
| CHECK(clonedCrossover->buffers[bufferIndex].getReadPointer(channelIndex)[sampleIndex] == 0.5f); | |||
| } | |||
| } | |||
| } | |||
| // Check that modifying a cloned buffer doesn't modify the original | |||
| clonedCrossover->buffers[0].getWritePointer(0)[0] = 0.6f; | |||
| CHECK(crossover->buffers[0].getReadPointer(0)[0] == 0.5f); | |||
| // Check the bands | |||
| for (size_t bandIndex {0}; bandIndex < crossover->bands.size(); bandIndex++) { | |||
| CHECK(clonedCrossover->bands[bandIndex].isSoloed == crossover->bands[bandIndex].isSoloed); | |||
| CHECK(clonedCrossover->bands[bandIndex].chain == crossover->bands[bandIndex].chain); | |||
| } | |||
| } | |||
| delete clonedCrossover; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,144 @@ | |||
| #pragma once | |||
| #include <memory> | |||
| #include <deque> | |||
| #include "PluginSplitter.hpp" | |||
| #include "General/AudioSpinMutex.h" | |||
| #include "SplitterProcessors.hpp" | |||
| #include "CloneableSources.hpp" | |||
| #include "WEFilters/PerlinSource.hpp" | |||
| namespace ModelInterface { | |||
| struct SplitterState { | |||
| // Internal representation of the data model | |||
| std::shared_ptr<PluginSplitter> splitter; | |||
| // We store the crossover frequencies so they can be restored if the user switches from a | |||
| // multiband split to another type and back again | |||
| std::optional<std::vector<float>> cachedcrossoverFrequencies; | |||
| SplitterState(HostConfiguration config, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) { | |||
| splitter.reset( | |||
| new PluginSplitterSeries(config, getModulationValueCallback, latencyChangeCallback) | |||
| ); | |||
| SplitterProcessors::prepareToPlay(*splitter.get(), config.sampleRate, config.blockSize, config.layout); | |||
| } | |||
| SplitterState* clone() const { | |||
| return new SplitterState(*this); | |||
| } | |||
| private: | |||
| SplitterState(const SplitterState& other) : splitter(other.splitter->clone()) { | |||
| if (other.cachedcrossoverFrequencies.has_value()) { | |||
| cachedcrossoverFrequencies = other.cachedcrossoverFrequencies; | |||
| } | |||
| } | |||
| }; | |||
| struct EnvelopeWrapper { | |||
| std::shared_ptr<CloneableEnvelopeFollower> envelope; | |||
| float amount; | |||
| bool useSidechainInput; | |||
| EnvelopeWrapper() : envelope(new CloneableEnvelopeFollower()), amount(0), useSidechainInput(false) { } | |||
| EnvelopeWrapper* clone() const { | |||
| return new EnvelopeWrapper(*this); | |||
| } | |||
| private: | |||
| EnvelopeWrapper(const EnvelopeWrapper& other) : envelope(other.envelope->clone()), amount(other.amount), useSidechainInput(other.useSidechainInput) { | |||
| } | |||
| }; | |||
| struct ModulationSourcesState { | |||
| std::vector<std::shared_ptr<CloneableLFO>> lfos; | |||
| std::vector<std::shared_ptr<EnvelopeWrapper>> envelopes; | |||
| std::vector<std::shared_ptr<WECore::Perlin::PerlinSource>> randomSources; | |||
| // Needed for the envelope followers to figure out which buffers to read from | |||
| HostConfiguration hostConfig; | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback; | |||
| ModulationSourcesState(std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback) : | |||
| getModulationValueCallback(newGetModulationValueCallback) { } | |||
| ModulationSourcesState* clone() const { | |||
| return new ModulationSourcesState(*this); | |||
| } | |||
| private: | |||
| ModulationSourcesState(const ModulationSourcesState& other) : hostConfig(other.hostConfig), getModulationValueCallback(other.getModulationValueCallback) { | |||
| for (std::shared_ptr<CloneableLFO> lfo : other.lfos) { | |||
| lfos.emplace_back(lfo->clone()); | |||
| } | |||
| for (std::shared_ptr<EnvelopeWrapper> env : other.envelopes) { | |||
| envelopes.emplace_back(env->clone()); | |||
| } | |||
| for (std::shared_ptr<WECore::Perlin::PerlinSource> source : other.randomSources) { | |||
| randomSources.emplace_back(source->clone()); | |||
| } | |||
| } | |||
| }; | |||
| struct StateWrapper { | |||
| std::shared_ptr<SplitterState> splitterState; | |||
| std::shared_ptr<ModulationSourcesState> modulationSourcesState; | |||
| // String representation of the operation that was performed to get to this state | |||
| juce::String operation; | |||
| StateWrapper(HostConfiguration config, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) : | |||
| splitterState(new SplitterState(config, getModulationValueCallback, latencyChangeCallback)), | |||
| modulationSourcesState(new ModulationSourcesState(getModulationValueCallback)), | |||
| operation("") { } | |||
| StateWrapper(std::shared_ptr<SplitterState> newSplitterState, | |||
| std::shared_ptr<ModulationSourcesState> newModulationSourcesState, | |||
| juce::String newOperation) : splitterState(newSplitterState), | |||
| modulationSourcesState(newModulationSourcesState), | |||
| operation(newOperation) { } | |||
| }; | |||
| struct StateManager { | |||
| std::deque<std::shared_ptr<StateWrapper>> undoHistory; | |||
| std::deque<std::shared_ptr<StateWrapper>> redoHistory; | |||
| // This mutex must be locked by all mutators before attempting to read or write to or from | |||
| // data model. Its purpose is to stop a mutator being called on one thread from changing the | |||
| // data model in a way that would crash a mutator being called on another thread. | |||
| // | |||
| // Recursive because the application code may call something via the mutator forEach methods | |||
| // that tries to take the lock on the same thread as the forEach method itself | |||
| std::recursive_mutex mutatorsMutex; | |||
| // This mutex must be locked by mutators which change the structure of the data model, and | |||
| // also by the processors. Its purpose is to stop a mutator being called on one thread from | |||
| // changing the data model in a way that would crash a processor being called on another | |||
| // thread. | |||
| // | |||
| // Mutators reading from the data model or writing only primitive values don't need to lock | |||
| // this | |||
| WECore::AudioSpinMutex sharedMutex; | |||
| StateManager(HostConfiguration config, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) { | |||
| undoHistory.push_back(std::make_shared<StateWrapper>(config, getModulationValueCallback, latencyChangeCallback)); | |||
| } | |||
| // TODO remove this once all functions are migrated | |||
| SplitterState& getSplitterStateUnsafe() { return *(undoHistory.back()->splitterState); } | |||
| std::shared_ptr<ModulationSourcesState> getSourcesStateUnsafe() { return undoHistory.back()->modulationSourcesState; } | |||
| }; | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| #include "FFTProvider.hpp" | |||
| FFTProvider::FFTProvider() : _inputBuffer(new float[FFT_SIZE]), | |||
| _fftBuffer(new float[FFT_SIZE]), | |||
| _outputs(new float[NUM_OUTPUTS]), | |||
| _fft(FFT_ORDER), | |||
| _isStereo(false), | |||
| _binWidth(0) { | |||
| juce::FloatVectorOperations::fill(_inputBuffer, 0, FFT_SIZE); | |||
| juce::FloatVectorOperations::fill(_fftBuffer, 0, FFT_SIZE); | |||
| juce::FloatVectorOperations::fill(_outputs, 0, NUM_OUTPUTS); | |||
| for (auto& env : _envs) { | |||
| env.setAttackTimeMs(0.1); | |||
| env.setReleaseTimeMs(0.5); | |||
| env.setFilterEnabled(false); | |||
| } | |||
| } | |||
| FFTProvider::~FFTProvider() { | |||
| WECore::AudioSpinLock lock(_fftMutex); | |||
| delete[] _inputBuffer; | |||
| delete[] _fftBuffer; | |||
| delete[] _outputs; | |||
| } | |||
| void FFTProvider::setSampleRate(double sampleRate) { | |||
| _binWidth = (sampleRate / 2) / NUM_OUTPUTS; | |||
| for (auto& env : _envs) { | |||
| env.setSampleRate(sampleRate); | |||
| } | |||
| } | |||
| void FFTProvider::reset() { | |||
| for (auto& env : _envs) { | |||
| env.reset(); | |||
| } | |||
| } | |||
| void FFTProvider::processBlock(juce::AudioBuffer<float>& buffer) { | |||
| WECore::AudioSpinTryLock lock(_fftMutex); | |||
| if (lock.isLocked()) { | |||
| const size_t numBuffersRequired {static_cast<size_t>( | |||
| std::ceil(static_cast<double>(buffer.getNumSamples()) / FFT_SIZE) | |||
| )}; | |||
| for (size_t bufferNumber {0}; bufferNumber < numBuffersRequired; bufferNumber++) { | |||
| // Calculate how many samples need to be processed in this chunk | |||
| const size_t numSamplesRemaining {buffer.getNumSamples() - (bufferNumber * FFT_SIZE)}; | |||
| const size_t numSamplesToCopy { | |||
| std::min(numSamplesRemaining, static_cast<size_t>(FFT_SIZE)) | |||
| }; | |||
| // The input buffer size might be smaller than the FFT buffer size, so then we need to | |||
| // append the the existing FFT buffer by shifting it back and adding the new samples to | |||
| // the end | |||
| juce::FloatVectorOperations::copy(_inputBuffer, _inputBuffer + numSamplesToCopy, FFT_SIZE - numSamplesToCopy); | |||
| float* const fillStart {_inputBuffer + FFT_SIZE - numSamplesToCopy}; | |||
| if (_isStereo) { | |||
| // Add the left and right buffers | |||
| const float* const leftBufferInputStart {buffer.getReadPointer(0) + bufferNumber * FFT_SIZE}; | |||
| const float* const rightBufferInputStart {buffer.getReadPointer(1) + bufferNumber * FFT_SIZE}; | |||
| juce::FloatVectorOperations::add(fillStart, leftBufferInputStart, rightBufferInputStart, numSamplesToCopy); | |||
| juce::FloatVectorOperations::multiply(fillStart, 0.5, numSamplesToCopy); | |||
| } else { | |||
| const float* const bufferInputStart {buffer.getReadPointer(0) + bufferNumber * FFT_SIZE}; | |||
| juce::FloatVectorOperations::copy(fillStart, bufferInputStart, numSamplesToCopy); | |||
| } | |||
| // Perform the FFT | |||
| juce::FloatVectorOperations::copy(_fftBuffer, _inputBuffer, FFT_SIZE); | |||
| _fft.performFrequencyOnlyForwardTransform(_fftBuffer); | |||
| // Run each FFT output bin through an envelope follower so that it is smoothed when | |||
| // displayed on the UI | |||
| for (int index {0}; index < NUM_OUTPUTS; index++) { | |||
| _outputs[index] = _envs[index].getNextOutput(_fftBuffer[index]); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,42 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "WEFilters/EffectsProcessor.h" | |||
| #include "WEFilters/AREnvelopeFollowerSquareLaw.h" | |||
| #include "General/AudioSpinMutex.h" | |||
| /** | |||
| * Performs an FFT on the signal which can be provided to the UI to drive the visualiser. | |||
| */ | |||
| class FFTProvider { | |||
| public: | |||
| static constexpr int FFT_ORDER {9}; | |||
| static constexpr int FFT_SIZE {(1 << FFT_ORDER) * 2}; | |||
| static constexpr int NUM_OUTPUTS { FFT_SIZE / 4 }; | |||
| FFTProvider(); | |||
| ~FFTProvider(); | |||
| void setSampleRate(double sampleRate); | |||
| void reset(); | |||
| void processBlock(juce::AudioBuffer<float>& buffer); | |||
| const float* getOutputs() { return _outputs; } | |||
| void setIsStereo(bool val) { _isStereo = val; } | |||
| float getBinWidth() const { return _binWidth; } | |||
| private: | |||
| float* _inputBuffer; | |||
| float* _fftBuffer; | |||
| float* _outputs; | |||
| juce::dsp::FFT _fft; | |||
| std::array<WECore::AREnv::AREnvelopeFollowerSquareLaw, NUM_OUTPUTS> _envs; | |||
| WECore::AudioSpinMutex _fftMutex; | |||
| bool _isStereo; | |||
| float _binWidth; | |||
| }; | |||
| @@ -0,0 +1,56 @@ | |||
| #include "LatencyListener.hpp" | |||
| #include "PluginChain.hpp" | |||
| #include "PluginSplitter.hpp" | |||
| PluginChainLatencyListener::PluginChainLatencyListener(PluginChain* chain) : | |||
| calculatedTotalPluginLatency(0), | |||
| _chain(chain), | |||
| _splitter(nullptr) { | |||
| } | |||
| void PluginChainLatencyListener::onPluginChainUpdate() { | |||
| handleAsyncUpdate(); | |||
| } | |||
| void PluginChainLatencyListener::handleAsyncUpdate() { | |||
| calculatedTotalPluginLatency = _calculateTotalPluginLatency(); | |||
| // Notify the splitter | |||
| if (_splitter != nullptr) { | |||
| _splitter->onLatencyChange(); | |||
| } | |||
| } | |||
| void PluginChainLatencyListener::audioProcessorParameterChanged(juce::AudioProcessor* /*processor*/, | |||
| int /*parameterIndex*/, | |||
| float /*newValue*/) { | |||
| // Do nothing | |||
| } | |||
| void PluginChainLatencyListener::audioProcessorChanged(juce::AudioProcessor* /*processor*/, | |||
| const ChangeDetails& details) { | |||
| if (details.latencyChanged) { | |||
| // Trigger the update on another thread | |||
| triggerAsyncUpdate(); | |||
| } | |||
| } | |||
| int PluginChainLatencyListener::_calculateTotalPluginLatency() const { | |||
| // Iterate through each plugin and total the reported latency | |||
| int totalLatency {0}; | |||
| // If the chain is bypassed the reported latency should be 0 | |||
| if (!_chain->isChainBypassed) { | |||
| for (int index {0}; index < _chain->chain.size(); index++) { | |||
| const auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(_chain->chain[index]); | |||
| // If this slot is a plugin and it's not bypassed, add it to the total | |||
| if (pluginSlot != nullptr && !pluginSlot->isBypassed) { | |||
| totalLatency += pluginSlot->plugin->getLatencySamples(); | |||
| } | |||
| } | |||
| } | |||
| return totalLatency; | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| class PluginChain; | |||
| class PluginSplitter; | |||
| /** | |||
| * Listens to updates to reported latency for plugins in the chain and calculates the new latency. | |||
| */ | |||
| class PluginChainLatencyListener : public juce::AsyncUpdater, | |||
| public juce::AudioProcessorListener { | |||
| public: | |||
| int calculatedTotalPluginLatency; | |||
| PluginChainLatencyListener(PluginChain* chain); | |||
| /** | |||
| * Call when there is a change to the plugin chain's layout (ie. a plugin is added or removed). | |||
| */ | |||
| void onPluginChainUpdate(); | |||
| /** | |||
| * Called on the message thread after a latency change. | |||
| */ | |||
| void handleAsyncUpdate() override; | |||
| void audioProcessorParameterChanged(juce::AudioProcessor* processor, | |||
| int parameterIndex, | |||
| float newValue) override; | |||
| /** | |||
| * Called on the audio thread by a processor after a latency change. | |||
| */ | |||
| void audioProcessorChanged(juce::AudioProcessor* processor, | |||
| const ChangeDetails& details) override; | |||
| void setSplitter(PluginSplitter* splitter) { _splitter = splitter; } | |||
| void removeSplitter() { _splitter = nullptr; } | |||
| private: | |||
| PluginChain* _chain; | |||
| PluginSplitter* _splitter; | |||
| int _calculateTotalPluginLatency() const; | |||
| }; | |||
| @@ -0,0 +1,128 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include <optional> | |||
| #include "XmlConsts.hpp" | |||
| #include "WEFilters/ModulationSource.h" | |||
| enum class MODULATION_TYPE { | |||
| MACRO, | |||
| LFO, | |||
| ENVELOPE, | |||
| RANDOM | |||
| }; | |||
| class ModulationSourceDefinition { | |||
| public: | |||
| int id; | |||
| MODULATION_TYPE type; | |||
| ModulationSourceDefinition(int newID, MODULATION_TYPE newType) : id(newID), type(newType) {} | |||
| bool operator==(const ModulationSourceDefinition& other) const { | |||
| return id == other.id && type == other.type; | |||
| } | |||
| /** | |||
| * Returns an optional containing a ModulationSourceDefinition if one can be constructed from | |||
| * the provided juce::var, otherwise empty. | |||
| */ | |||
| static std::optional<ModulationSourceDefinition> fromVariant(juce::var variant) { | |||
| std::optional<ModulationSourceDefinition> retVal; | |||
| if (variant.isArray() && variant.size() == 2 && variant[0].isInt() && variant[1].isString()) { | |||
| const int newId = variant[0]; | |||
| const juce::String typeString = variant[1].toString(); | |||
| const MODULATION_TYPE newType = _stringToModulationType(typeString); | |||
| retVal.emplace(newId, newType); | |||
| } | |||
| return retVal; | |||
| } | |||
| juce::var toVariant() const { | |||
| // Encode the members as items in the variant's array | |||
| juce::var retVal; | |||
| retVal.append(id); | |||
| retVal.append(_modulationTypeToString(type)); | |||
| return retVal; | |||
| } | |||
| void restoreFromXml(juce::XmlElement* element) { | |||
| if (element->hasAttribute(XML_MODULATION_SOURCE_ID)) { | |||
| id = element->getIntAttribute(XML_MODULATION_SOURCE_ID); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_ID)); | |||
| } | |||
| if (element->hasAttribute(XML_MODULATION_SOURCE_TYPE)) { | |||
| type = _stringToModulationType(element->getStringAttribute(XML_MODULATION_SOURCE_TYPE)); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_TYPE)); | |||
| } | |||
| } | |||
| void writeToXml(juce::XmlElement* element) const { | |||
| element->setAttribute(XML_MODULATION_SOURCE_ID, id); | |||
| element->setAttribute(XML_MODULATION_SOURCE_TYPE, _modulationTypeToString(type)); | |||
| } | |||
| private: | |||
| static juce::String _modulationTypeToString(MODULATION_TYPE type) { | |||
| switch (type) { | |||
| case MODULATION_TYPE::MACRO: | |||
| return "macro"; | |||
| case MODULATION_TYPE::LFO: | |||
| return "lfo"; | |||
| case MODULATION_TYPE::ENVELOPE: | |||
| return "envelope"; | |||
| case MODULATION_TYPE::RANDOM: | |||
| return "random"; | |||
| default: | |||
| return ""; | |||
| } | |||
| } | |||
| static MODULATION_TYPE _stringToModulationType(juce::String typeString) { | |||
| if (typeString == "macro") { | |||
| return MODULATION_TYPE::MACRO; | |||
| } else if (typeString == "lfo") { | |||
| return MODULATION_TYPE::LFO; | |||
| } else if (typeString == "envelope") { | |||
| return MODULATION_TYPE::ENVELOPE; | |||
| } else /*(typeString == "random")*/ { | |||
| return MODULATION_TYPE::RANDOM; | |||
| } | |||
| } | |||
| }; | |||
| /** | |||
| * Provides a way for sources (LFOs, envelopes) to lookup the modulation values for sources assigned | |||
| * to their parameters. | |||
| */ | |||
| class ModulationSourceProvider : public WECore::ModulationSource<double> { | |||
| public: | |||
| ModulationSourceDefinition definition; | |||
| ModulationSourceProvider( | |||
| ModulationSourceDefinition newDefinition, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback): | |||
| definition(newDefinition), _getModulationValueCallback(getModulationValueCallback) { } | |||
| double getLastOutput() const override { | |||
| return _getModulationValueCallback(definition.id, definition.type); | |||
| } | |||
| private: | |||
| std::function<float(int, MODULATION_TYPE)> _getModulationValueCallback; | |||
| virtual double _getNextOutputImpl(double /*inSample*/) override { return 0.0; } | |||
| virtual void _resetImpl() override {} | |||
| }; | |||
| @@ -0,0 +1,81 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "ChainSlots.hpp" | |||
| #include "LatencyListener.hpp" | |||
| #include "General/AudioSpinMutex.h" | |||
| #include "CloneableDelayLine.hpp" | |||
| typedef CloneableDelayLine<float, juce::dsp::DelayLineInterpolationTypes::None> CloneableDelayLineType; | |||
| class PluginChain { | |||
| public: | |||
| std::vector<std::shared_ptr<ChainSlotBase>> chain; | |||
| bool isChainBypassed; | |||
| bool isChainMuted; | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback; | |||
| std::unique_ptr<CloneableDelayLineType> latencyCompLine; | |||
| WECore::AudioSpinMutex latencyCompLineMutex; | |||
| PluginChainLatencyListener latencyListener; | |||
| juce::String customName; | |||
| PluginChain(std::function<float(int, MODULATION_TYPE)> getModulationValueCallback) : | |||
| isChainBypassed(false), | |||
| isChainMuted(false), | |||
| getModulationValueCallback(getModulationValueCallback), | |||
| latencyListener(this) { | |||
| latencyCompLine.reset(new CloneableDelayLineType(0)); | |||
| latencyCompLine->setDelay(0); | |||
| } | |||
| virtual ~PluginChain() { | |||
| // The plugins might outlive this chain if they're also referenced by another copy in the | |||
| // history, so remove the listener | |||
| for (auto& slot : chain) { | |||
| if (std::shared_ptr<ChainSlotPlugin> plugin = std::dynamic_pointer_cast<ChainSlotPlugin>(slot)) { | |||
| plugin->plugin->removeListener(&latencyListener); | |||
| } | |||
| } | |||
| } | |||
| PluginChain* clone() const { | |||
| return new PluginChain( | |||
| chain, | |||
| isChainBypassed, | |||
| isChainMuted, | |||
| getModulationValueCallback, | |||
| std::unique_ptr<CloneableDelayLineType>(latencyCompLine->clone()), | |||
| customName | |||
| ); | |||
| } | |||
| private: | |||
| PluginChain( | |||
| std::vector<std::shared_ptr<ChainSlotBase>> newChain, | |||
| bool newIsChainBypassed, | |||
| bool newIsChainMuted, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::unique_ptr<CloneableDelayLineType> newLatencyCompLine, | |||
| const juce::String& newCustomName) : | |||
| isChainBypassed(newIsChainBypassed), | |||
| isChainMuted(newIsChainMuted), | |||
| getModulationValueCallback(newGetModulationValueCallback), | |||
| latencyCompLine(std::move(newLatencyCompLine)), | |||
| latencyListener(this), | |||
| customName(newCustomName) { | |||
| for (auto& slot : newChain) { | |||
| chain.push_back(std::shared_ptr<ChainSlotBase>(slot->clone())); | |||
| if (auto plugin = std::dynamic_pointer_cast<ChainSlotPlugin>(slot)) { | |||
| plugin->plugin->addListener(&latencyListener); | |||
| } | |||
| } | |||
| latencyListener.onPluginChainUpdate(); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,86 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "PluginChain.hpp" | |||
| SCENARIO("PluginChain: Clone works correctly") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("A chain with one plugin slot and one gain stage") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.2f; | |||
| }; | |||
| auto pluginSlot = std::make_shared<ChainSlotPlugin>(plugin, false, modulationCallback, hostConfig); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| plugin->setLatencySamples(15); | |||
| chain->chain.push_back(pluginSlot); | |||
| plugin->addListener(&chain->latencyListener); | |||
| chain->latencyListener.onPluginChainUpdate(); | |||
| REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 15); | |||
| auto gainStage = std::make_shared<ChainSlotGainStage>(0.5f, 0.0f, false, juce::AudioProcessor::BusesLayout()); | |||
| chain->chain.push_back(gainStage); | |||
| // Change the values so we can test for them | |||
| // chain->isBypassed = true; // Can't test for this one as it sets the latency to 0 so would stop us testing the latency listener | |||
| chain->isChainMuted = true; | |||
| chain->latencyCompLine.reset(new CloneableDelayLineType(100)); | |||
| chain->latencyCompLine->setDelay(50); | |||
| chain->latencyCompLine->prepare({44100, 10, 1}); | |||
| chain->latencyCompLine->pushSample(0, 0.75f); | |||
| WHEN("It is cloned") { | |||
| PluginChain* clonedChain = chain->clone(); | |||
| THEN("The cloned chain is equal to the original") { | |||
| CHECK(clonedChain->isChainBypassed == chain->isChainBypassed); | |||
| CHECK(clonedChain->isChainMuted == chain->isChainMuted); | |||
| CHECK(clonedChain->getModulationValueCallback(0, MODULATION_TYPE::MACRO) == 1.2f); | |||
| // Check the plugin slot | |||
| auto clonedPluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(clonedChain->chain[0]); | |||
| CHECK(clonedPluginSlot != nullptr); | |||
| CHECK(clonedPluginSlot->isBypassed == pluginSlot->isBypassed); | |||
| CHECK(clonedPluginSlot->plugin == pluginSlot->plugin); // Should be the same shared pointer | |||
| CHECK(clonedPluginSlot->modulationConfig != pluginSlot->modulationConfig); // Should be a different shared pointer | |||
| // Check the gain stage | |||
| auto clonedGainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(clonedChain->chain[1]); | |||
| CHECK(clonedGainStage != nullptr); | |||
| CHECK(clonedGainStage->isBypassed == gainStage->isBypassed); | |||
| CHECK(clonedGainStage->gain == gainStage->gain); | |||
| CHECK(clonedGainStage->pan == gainStage->pan); | |||
| CHECK(clonedGainStage->numMainChannels == gainStage->numMainChannels); | |||
| // Check the delay line | |||
| CHECK(clonedChain->latencyCompLine != chain->latencyCompLine); | |||
| CHECK(chain->latencyCompLine != nullptr); | |||
| CHECK(clonedChain->latencyCompLine->getMaximumDelayInSamples() == 100); | |||
| CHECK(clonedChain->latencyCompLine->getDelay() == 50); | |||
| CHECK(clonedChain->latencyCompLine->popSample(0, 0) == 0.75f); | |||
| // Check the latency listener | |||
| CHECK(clonedChain->latencyListener.calculatedTotalPluginLatency == 15); | |||
| // Check the listeners for both chains are still connected | |||
| plugin->setLatencySamples(30); | |||
| messageManager->runDispatchLoopUntil(10); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 30); | |||
| CHECK(clonedChain->latencyListener.calculatedTotalPluginLatency == 30); | |||
| } | |||
| delete clonedChain; | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| @@ -0,0 +1,66 @@ | |||
| #include "PluginConfigurator.hpp" | |||
| PluginConfigurator::PluginConfigurator() { | |||
| monoInMonoOut.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoInMonoOut.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoInMonoOutSC.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoInMonoOutSC.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoInMonoOutSC.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| stereoInStereoOut.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoInStereoOut.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoInStereoOutSC.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoInStereoOutSC.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoInStereoOutSC.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| } | |||
| bool PluginConfigurator::configure(std::shared_ptr<juce::AudioPluginInstance> plugin, | |||
| HostConfiguration configuration) const { | |||
| // Note: for this layout stuff to work correctly it *must* be done before prepareToPlay() is | |||
| // called on the plugin we just loaded. If we try it afterwards JUCE can't actually query the | |||
| // layouts that the plugin supports so might do something weird. | |||
| std::vector<const juce::AudioProcessor::BusesLayout*> rankedLayouts; | |||
| const bool isSyndicateStereo { | |||
| configuration.layout.getMainInputChannels() == 2 && configuration.layout.getMainOutputChannels() == 2 | |||
| }; | |||
| const bool isSyndicateSidechain {layoutHasSidechain(configuration.layout)}; | |||
| if (isSyndicateStereo) { | |||
| if (isSyndicateSidechain) { | |||
| rankedLayouts.push_back(&stereoInStereoOutSC); | |||
| rankedLayouts.push_back(&stereoInStereoOut); | |||
| } else { | |||
| rankedLayouts.push_back(&stereoInStereoOut); | |||
| rankedLayouts.push_back(&stereoInStereoOutSC); | |||
| } | |||
| } else { | |||
| if (isSyndicateSidechain) { | |||
| rankedLayouts.push_back(&monoInMonoOutSC); | |||
| rankedLayouts.push_back(&monoInMonoOut); | |||
| } else { | |||
| rankedLayouts.push_back(&monoInMonoOut); | |||
| rankedLayouts.push_back(&monoInMonoOutSC); | |||
| } | |||
| } | |||
| // Try each layout in order | |||
| bool setLayoutOk {false}; | |||
| for (const juce::AudioProcessor::BusesLayout* layout : rankedLayouts) { | |||
| if (plugin->setBusesLayout(*layout)) { | |||
| setLayoutOk = true; | |||
| break; | |||
| } | |||
| } | |||
| if (setLayoutOk) { | |||
| plugin->enableAllBuses(); | |||
| plugin->setRateAndBufferSizeDetails(configuration.sampleRate, configuration.blockSize); | |||
| plugin->prepareToPlay(configuration.sampleRate, configuration.blockSize); | |||
| } | |||
| return setLayoutOk; | |||
| } | |||
| @@ -0,0 +1,56 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| inline bool canDoStereoSplitTypes(const juce::AudioProcessor::BusesLayout& layout) { | |||
| return layout.getMainInputChannels() == layout.getMainOutputChannels() && | |||
| layout.getMainOutputChannels() == 2; | |||
| } | |||
| inline int getTotalNumInputChannels(const juce::AudioProcessor::BusesLayout& layout) { | |||
| int retVal {0}; | |||
| for (int index {0}; index < layout.inputBuses.size(); index++) { | |||
| retVal += layout.getNumChannels(true, index); | |||
| } | |||
| return retVal; | |||
| } | |||
| inline bool layoutHasSidechain(const juce::AudioProcessor::BusesLayout& layout) { | |||
| return layout.getBuses(true).size() > 1 && layout.getBuses(true)[1].size() > 0; | |||
| } | |||
| struct HostConfiguration { | |||
| juce::AudioProcessor::BusesLayout layout; | |||
| double sampleRate; | |||
| int blockSize; | |||
| }; | |||
| class PluginConfigurator { | |||
| public: | |||
| PluginConfigurator(); | |||
| /** | |||
| * To simpify plugin routing we assume that all plugins support both mono and stereo layouts, | |||
| * and just set them to use the same layout as Syndicate. Syndicate also supports sidechain so | |||
| * we'll try to configure sidechain layouts for the plugins, and drop back to a non-sidechain | |||
| * layout if it doesn't work. In processBlock we'll still pass the full buffer with sidechain to | |||
| * the plugin, which should just be ignored if it doesn't need it. | |||
| * | |||
| * We assume the buses layout will never change while the plugin is running, so only need to be | |||
| * configured when a plugin is added by the user or has been restored from XML. | |||
| * | |||
| * We could do things like try to load plugins in mono for split types that only ever need mono | |||
| * (ie. left/right and mid/side) but that gets really complicated and most users won't see any | |||
| * benefit from it. | |||
| */ | |||
| bool configure(std::shared_ptr<juce::AudioPluginInstance> plugin, | |||
| HostConfiguration configuration) const; | |||
| private: | |||
| juce::AudioProcessor::BusesLayout monoInMonoOut; | |||
| juce::AudioProcessor::BusesLayout monoInMonoOutSC; | |||
| juce::AudioProcessor::BusesLayout stereoInStereoOut; | |||
| juce::AudioProcessor::BusesLayout stereoInStereoOutSC; | |||
| }; | |||
| @@ -0,0 +1,153 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| namespace { | |||
| class ConfigTestPluginInstance : public TestUtils::TestPluginInstance { | |||
| public: | |||
| bool isPrepared; | |||
| std::function<bool(const juce::AudioProcessor::BusesLayout&)> onIsBusesLayoutSupported; | |||
| ConfigTestPluginInstance(std::function<bool(const juce::AudioProcessor::BusesLayout&)> newOnIsBusesLayoutSupported) : isPrepared(false), onIsBusesLayoutSupported(newOnIsBusesLayoutSupported) {} | |||
| void prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock) override { | |||
| TestUtils::TestPluginInstance::prepareToPlay(sampleRate, maximumExpectedSamplesPerBlock); | |||
| isPrepared = true; | |||
| } | |||
| protected: | |||
| bool isBusesLayoutSupported(const BusesLayout& arr) const override { | |||
| return onIsBusesLayoutSupported(arr); | |||
| } | |||
| }; | |||
| HostConfiguration getHostConfig(std::string name) { | |||
| HostConfiguration config; | |||
| if (name == "mono") { | |||
| config.layout.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| config.layout.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| config.sampleRate = 80000; | |||
| config.blockSize = 100; | |||
| } else if (name == "monoSC") { | |||
| config.layout.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| config.layout.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| config.layout.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| config.sampleRate = 85000; | |||
| config.blockSize = 150; | |||
| } else if (name == "stereo") { | |||
| config.layout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| config.layout.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| config.sampleRate = 90000; | |||
| config.blockSize = 200; | |||
| } else if (name == "stereoSC") { | |||
| config.layout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| config.layout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| config.layout.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| config.sampleRate = 95000; | |||
| config.blockSize = 250; | |||
| } | |||
| return config; | |||
| } | |||
| std::vector<juce::AudioProcessor::BusesLayout> getExpectedLayouts(std::string name) { | |||
| std::vector<juce::AudioProcessor::BusesLayout> layouts; | |||
| if (name == "mono") { | |||
| juce::AudioProcessor::BusesLayout mono; | |||
| mono.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| mono.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| layouts.push_back(mono); | |||
| juce::AudioProcessor::BusesLayout monoSC; | |||
| monoSC.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoSC.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoSC.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| layouts.push_back(monoSC); | |||
| } else if (name == "monoSC") { | |||
| juce::AudioProcessor::BusesLayout monoSC; | |||
| monoSC.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoSC.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| monoSC.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| layouts.push_back(monoSC); | |||
| juce::AudioProcessor::BusesLayout mono; | |||
| mono.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| mono.outputBuses.add(juce::AudioChannelSet::mono()); | |||
| layouts.push_back(mono); | |||
| } else if (name == "stereo") { | |||
| juce::AudioProcessor::BusesLayout stereo; | |||
| stereo.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereo.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| layouts.push_back(stereo); | |||
| juce::AudioProcessor::BusesLayout stereoSC; | |||
| stereoSC.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoSC.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoSC.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| layouts.push_back(stereoSC); | |||
| } else if (name == "stereoSC") { | |||
| juce::AudioProcessor::BusesLayout stereoSC; | |||
| stereoSC.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoSC.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereoSC.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| layouts.push_back(stereoSC); | |||
| juce::AudioProcessor::BusesLayout stereo; | |||
| stereo.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| stereo.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| layouts.push_back(stereo); | |||
| } | |||
| return layouts; | |||
| } | |||
| } | |||
| SCENARIO("PluginConfigurator: Can configure the plugin correctly") { | |||
| PluginConfigurator configurator; | |||
| GIVEN("A host configuration and a plugin") { | |||
| typedef std::tuple<std::string, bool> TestData; | |||
| auto [hostConfigString, shouldSucceed] = GENERATE( | |||
| TestData("mono", true), | |||
| TestData("monoSC", true), | |||
| TestData("stereo", true), | |||
| TestData("stereoSC", true), | |||
| TestData("mono", false), | |||
| TestData("monoSC", false), | |||
| TestData("stereo", false), | |||
| TestData("stereoSC", false) | |||
| ); | |||
| const HostConfiguration hostConfig = getHostConfig(hostConfigString); | |||
| std::vector<juce::AudioProcessor::BusesLayout> testedLayouts; | |||
| auto plugin = std::make_shared<ConfigTestPluginInstance>([hostConfig, shouldSucceed = shouldSucceed, &testedLayouts](const juce::AudioProcessor::BusesLayout& layout) { | |||
| testedLayouts.push_back(layout); | |||
| return shouldSucceed; | |||
| }); | |||
| WHEN("Asked to configure it") { | |||
| const bool success {configurator.configure(plugin, hostConfig)}; | |||
| THEN("The plugin is configured") { | |||
| CHECK(success == shouldSucceed); | |||
| CHECK(plugin->isPrepared == shouldSucceed); | |||
| const auto expectedLayouts = getExpectedLayouts(hostConfigString); | |||
| if (shouldSucceed) { | |||
| CHECK(plugin->getSampleRate() == hostConfig.sampleRate); | |||
| CHECK(plugin->getBlockSize() == hostConfig.blockSize); | |||
| // Just check the first layout as the configurator will stop after the first success | |||
| CHECK(testedLayouts[0] == expectedLayouts[0]); | |||
| } else { | |||
| // We can check each layout as the configurator will try all of them | |||
| CHECK(testedLayouts == expectedLayouts); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,379 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "ChainMutators.hpp" | |||
| #include "PluginChain.hpp" | |||
| #include "FFTProvider.hpp" | |||
| #include "CrossoverState.hpp" | |||
| #include "CrossoverMutators.hpp" | |||
| #include "CrossoverProcessors.hpp" | |||
| /** | |||
| * Stores a plugin chain and any associated data. | |||
| */ | |||
| struct PluginChainWrapper { | |||
| PluginChainWrapper(std::shared_ptr<PluginChain> newChain, bool newIsSoloed) | |||
| : chain(newChain), isSoloed(newIsSoloed) {} | |||
| std::shared_ptr<PluginChain> chain; | |||
| bool isSoloed; | |||
| }; | |||
| /** | |||
| * Base class which provides the audio splitting functionality. | |||
| * | |||
| * Each derived class contains one or more plugin chains (one for each split). | |||
| * | |||
| * A splitter may contain more chains than it can actually use if they have been carried over from | |||
| * a previous splitter that could handle more. In this case its processBlock will just ignore the | |||
| * extra chains. | |||
| */ | |||
| class PluginSplitter { | |||
| public: | |||
| std::vector<PluginChainWrapper> chains; | |||
| size_t numChainsSoloed; | |||
| HostConfiguration config; | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback; | |||
| std::function<void(int)> notifyProcessorOnLatencyChange; | |||
| bool shouldNotifyProcessorOnLatencyChange; | |||
| PluginSplitter(int defaultNumChains, | |||
| HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) | |||
| : numChainsSoloed(0), | |||
| config(newConfig), | |||
| getModulationValueCallback(newGetModulationValueCallback), | |||
| notifyProcessorOnLatencyChange(latencyChangeCallback), | |||
| shouldNotifyProcessorOnLatencyChange(true) { | |||
| // Set up the default number of chains | |||
| for (int idx {0}; idx < defaultNumChains; idx++) { | |||
| chains.emplace_back(std::make_shared<PluginChain>(getModulationValueCallback), false); | |||
| chains[chains.size() - 1].chain->latencyListener.setSplitter(this); | |||
| } | |||
| onLatencyChange(); | |||
| } | |||
| PluginSplitter(std::shared_ptr<PluginSplitter> otherSplitter, int defaultNumChains) | |||
| : chains(otherSplitter->chains), | |||
| numChainsSoloed(otherSplitter->numChainsSoloed), | |||
| config(otherSplitter->config), | |||
| getModulationValueCallback(otherSplitter->getModulationValueCallback), | |||
| notifyProcessorOnLatencyChange(otherSplitter->notifyProcessorOnLatencyChange), | |||
| shouldNotifyProcessorOnLatencyChange(true) { | |||
| // Move the latency listeners for the existing chains to point to this splitter | |||
| for (auto& chain : chains) { | |||
| chain.chain->latencyListener.setSplitter(this); | |||
| } | |||
| // Add chains if we still need to reach the default | |||
| while (defaultNumChains > chains.size()) { | |||
| chains.emplace_back(std::make_shared<PluginChain>(getModulationValueCallback), false); | |||
| chains[chains.size() - 1].chain->latencyListener.setSplitter(this); | |||
| } | |||
| onLatencyChange(); | |||
| } | |||
| virtual ~PluginSplitter() = default; | |||
| void onLatencyChange() { | |||
| // The latency of the splitter is the latency of the slowest chain, so iterate through each | |||
| // chain and report the highest latency | |||
| int highestLatency {0}; | |||
| for (const PluginChainWrapper& chain : chains) { | |||
| const int thisLatency {chain.chain->latencyListener.calculatedTotalPluginLatency}; | |||
| if (highestLatency < thisLatency) { | |||
| highestLatency = thisLatency; | |||
| } | |||
| } | |||
| // Tell each chain the latency of the slowest chain, so they can all add compensation to match | |||
| // it | |||
| for (PluginChainWrapper& chain : chains) { | |||
| ChainMutators::setRequiredLatency(chain.chain, highestLatency, config); | |||
| } | |||
| if (shouldNotifyProcessorOnLatencyChange) { | |||
| notifyProcessorOnLatencyChange(highestLatency); | |||
| } | |||
| } | |||
| virtual PluginSplitter* clone() const = 0; | |||
| protected: | |||
| PluginSplitter(std::vector<PluginChainWrapper> newChains, | |||
| HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::function<void(int)> newNotifyProcessorOnLatencyChange) : | |||
| numChainsSoloed(0), | |||
| config(newConfig), | |||
| getModulationValueCallback(newGetModulationValueCallback), | |||
| notifyProcessorOnLatencyChange(newNotifyProcessorOnLatencyChange), | |||
| shouldNotifyProcessorOnLatencyChange(true) { | |||
| for (auto& chain : newChains) { | |||
| std::shared_ptr<PluginChain> newChain(chain.chain->clone()); | |||
| newChain->latencyListener.setSplitter(this); | |||
| chains.emplace_back(newChain, chain.isSoloed); | |||
| if (chain.isSoloed) { | |||
| numChainsSoloed++; | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| /** | |||
| * Contains a single plugin graph for plugins arranged in series. | |||
| */ | |||
| class PluginSplitterSeries : public PluginSplitter { | |||
| public: | |||
| static constexpr int DEFAULT_NUM_CHAINS {1}; | |||
| PluginSplitterSeries(HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) | |||
| : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { | |||
| juce::Logger::writeToLog("Constructed PluginSplitterSeries"); | |||
| } | |||
| PluginSplitterSeries(std::shared_ptr<PluginSplitter> otherSplitter) | |||
| : PluginSplitter(otherSplitter, DEFAULT_NUM_CHAINS) { | |||
| juce::Logger::writeToLog("Converted to PluginSplitterSeries"); | |||
| // We only have one active chain in the series splitter, so it can't be muted or soloed | |||
| ChainMutators::setChainMute(chains[0].chain, false); | |||
| } | |||
| PluginSplitterSeries* clone() const override { | |||
| return new PluginSplitterSeries(chains, config, getModulationValueCallback, notifyProcessorOnLatencyChange); | |||
| } | |||
| private: | |||
| PluginSplitterSeries(std::vector<PluginChainWrapper> newChains, | |||
| HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::function<void(int)> newNotifyProcessorOnLatencyChange) : | |||
| PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange) { | |||
| } | |||
| }; | |||
| /** | |||
| * Contains a single plugin graph for plugins arranged in parallel. | |||
| */ | |||
| class PluginSplitterParallel : public PluginSplitter { | |||
| public: | |||
| static constexpr int DEFAULT_NUM_CHAINS {1}; | |||
| std::unique_ptr<juce::AudioBuffer<float>> inputBuffer; | |||
| std::unique_ptr<juce::AudioBuffer<float>> outputBuffer; | |||
| PluginSplitterParallel(HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) | |||
| : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { | |||
| juce::Logger::writeToLog("Constructed PluginSplitterParallel"); | |||
| } | |||
| PluginSplitterParallel(std::shared_ptr<PluginSplitter> otherSplitter) | |||
| : PluginSplitter(otherSplitter, DEFAULT_NUM_CHAINS) { | |||
| juce::Logger::writeToLog("Converted to PluginSplitterParallel"); | |||
| } | |||
| PluginSplitterParallel* clone() const override { | |||
| return new PluginSplitterParallel( | |||
| chains, | |||
| config, | |||
| getModulationValueCallback, | |||
| notifyProcessorOnLatencyChange, | |||
| *inputBuffer, | |||
| *outputBuffer); | |||
| } | |||
| private: | |||
| PluginSplitterParallel(std::vector<PluginChainWrapper> newChains, | |||
| HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::function<void(int)> newNotifyProcessorOnLatencyChange, | |||
| const juce::AudioBuffer<float>& newInputBuffer, | |||
| const juce::AudioBuffer<float>& newOutputBuffer) : | |||
| PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange) { | |||
| // We need to copy the buffers as well | |||
| inputBuffer.reset(new juce::AudioBuffer<float>(newInputBuffer)); | |||
| outputBuffer.reset(new juce::AudioBuffer<float>(newOutputBuffer)); | |||
| } | |||
| }; | |||
| /** | |||
| * Contains a single plugin graph for plugins arranged in a multiband split. | |||
| */ | |||
| class PluginSplitterMultiband : public PluginSplitter { | |||
| public: | |||
| static constexpr int DEFAULT_NUM_CHAINS {2}; | |||
| std::shared_ptr<CrossoverState> crossover; | |||
| FFTProvider fftProvider; | |||
| PluginSplitterMultiband(HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) | |||
| : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback), | |||
| crossover(createDefaultCrossoverState(config)) { | |||
| juce::Logger::writeToLog("Constructed PluginSplitterMultiband"); | |||
| CrossoverProcessors::prepareToPlay(*crossover.get(), config.sampleRate, config.blockSize, config.layout); | |||
| } | |||
| PluginSplitterMultiband(std::shared_ptr<PluginSplitter> otherSplitter, std::optional<std::vector<float>> crossoverFrequencies) | |||
| : PluginSplitter(otherSplitter, DEFAULT_NUM_CHAINS), | |||
| crossover(createDefaultCrossoverState(config)) { | |||
| juce::Logger::writeToLog("Converted to PluginSplitterMultiband"); | |||
| // Set the crossover to have the correct number of bands (this will also default the frequencies) | |||
| while (chains.size() > CrossoverMutators::getNumBands(crossover)) { | |||
| CrossoverMutators::addBand(crossover); | |||
| } | |||
| // Restore the crossover frequencies if there are previous ones | |||
| if (crossoverFrequencies.has_value()) { | |||
| const size_t numCrossovers {std::min(crossoverFrequencies.value().size(), CrossoverMutators::getNumBands(crossover))}; | |||
| for (int index {0}; index < numCrossovers; index++) { | |||
| CrossoverMutators::setCrossoverFrequency(crossover, index, crossoverFrequencies.value()[index]); | |||
| } | |||
| } | |||
| // Set the processors | |||
| for (size_t bandIndex {0}; bandIndex < CrossoverMutators::getNumBands(crossover); bandIndex++) { | |||
| std::shared_ptr<PluginChain> newChain {chains[bandIndex].chain}; | |||
| CrossoverMutators::setPluginChain(crossover, bandIndex, newChain); | |||
| CrossoverMutators::setIsSoloed(crossover, bandIndex, chains[bandIndex].isSoloed); | |||
| } | |||
| CrossoverProcessors::prepareToPlay(*crossover.get(), config.sampleRate, config.blockSize, config.layout); | |||
| } | |||
| PluginSplitterMultiband* clone() const override { | |||
| auto clonedSplitter = new PluginSplitterMultiband( | |||
| chains, | |||
| config, | |||
| getModulationValueCallback, | |||
| notifyProcessorOnLatencyChange, | |||
| std::shared_ptr<CrossoverState>(crossover->clone())); | |||
| // The crossover we cloned needs to be updated to use the new chains that were just cloned | |||
| for (int chainIndex {0}; chainIndex < clonedSplitter->chains.size(); chainIndex++) { | |||
| CrossoverMutators::setPluginChain(clonedSplitter->crossover, chainIndex, clonedSplitter->chains[chainIndex].chain); | |||
| } | |||
| // Set up the fft provider | |||
| clonedSplitter->fftProvider.setSampleRate(config.sampleRate); | |||
| clonedSplitter->fftProvider.setIsStereo(canDoStereoSplitTypes(config.layout)); | |||
| return clonedSplitter; | |||
| } | |||
| private: | |||
| PluginSplitterMultiband(std::vector<PluginChainWrapper> newChains, | |||
| HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::function<void(int)> newNotifyProcessorOnLatencyChange, | |||
| std::shared_ptr<CrossoverState> newCrossover) : | |||
| PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange), | |||
| crossover(newCrossover) { | |||
| } | |||
| }; | |||
| /** | |||
| * Contains a single plugin graph for plugins arranged in a left right split. | |||
| */ | |||
| class PluginSplitterLeftRight : public PluginSplitter { | |||
| public: | |||
| static constexpr int DEFAULT_NUM_CHAINS {2}; | |||
| std::unique_ptr<juce::AudioBuffer<float>> leftBuffer; | |||
| std::unique_ptr<juce::AudioBuffer<float>> rightBuffer; | |||
| PluginSplitterLeftRight(HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) | |||
| : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { | |||
| juce::Logger::writeToLog("Constructed PluginSplitterLeftRight"); | |||
| } | |||
| PluginSplitterLeftRight(std::shared_ptr<PluginSplitter> otherSplitter) | |||
| : PluginSplitter(otherSplitter, DEFAULT_NUM_CHAINS) { | |||
| juce::Logger::writeToLog("Converted to PluginSplitterLeftRight"); | |||
| } | |||
| PluginSplitterLeftRight* clone() const override { | |||
| return new PluginSplitterLeftRight( | |||
| chains, | |||
| config, | |||
| getModulationValueCallback, | |||
| notifyProcessorOnLatencyChange, | |||
| *leftBuffer, | |||
| *rightBuffer); | |||
| } | |||
| private: | |||
| PluginSplitterLeftRight(std::vector<PluginChainWrapper> newChains, | |||
| HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::function<void(int)> newNotifyProcessorOnLatencyChange, | |||
| const juce::AudioBuffer<float>& newLeftBuffer, | |||
| const juce::AudioBuffer<float>& newRightBuffer) : | |||
| PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange) { | |||
| // We need to copy the buffers as well | |||
| leftBuffer.reset(new juce::AudioBuffer<float>(newLeftBuffer)); | |||
| rightBuffer.reset(new juce::AudioBuffer<float>(newRightBuffer)); | |||
| } | |||
| }; | |||
| /** | |||
| * Contains a single plugin graph for plugins arranged in a mid side split. | |||
| */ | |||
| class PluginSplitterMidSide : public PluginSplitter { | |||
| public: | |||
| static constexpr int DEFAULT_NUM_CHAINS {2}; | |||
| std::unique_ptr<juce::AudioBuffer<float>> midBuffer; | |||
| std::unique_ptr<juce::AudioBuffer<float>> sideBuffer; | |||
| PluginSplitterMidSide(HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback) | |||
| : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { | |||
| juce::Logger::writeToLog("Constructed PluginSplitterMidSide"); | |||
| } | |||
| PluginSplitterMidSide(std::shared_ptr<PluginSplitter> otherSplitter) | |||
| : PluginSplitter(otherSplitter, DEFAULT_NUM_CHAINS) { | |||
| juce::Logger::writeToLog("Converted to PluginSplitterMidSide"); | |||
| } | |||
| PluginSplitterMidSide* clone() const override { | |||
| return new PluginSplitterMidSide( | |||
| chains, | |||
| config, | |||
| getModulationValueCallback, | |||
| notifyProcessorOnLatencyChange, | |||
| *midBuffer, | |||
| *sideBuffer); | |||
| } | |||
| private: | |||
| PluginSplitterMidSide(std::vector<PluginChainWrapper> newChains, | |||
| HostConfiguration newConfig, | |||
| std::function<float(int, MODULATION_TYPE)> newGetModulationValueCallback, | |||
| std::function<void(int)> newNotifyProcessorOnLatencyChange, | |||
| const juce::AudioBuffer<float>& newMidBuffer, | |||
| const juce::AudioBuffer<float>& newSideBuffer) : | |||
| PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange) { | |||
| // We need to copy the buffers as well | |||
| midBuffer.reset(new juce::AudioBuffer<float>(newMidBuffer)); | |||
| sideBuffer.reset(new juce::AudioBuffer<float>(newSideBuffer)); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,417 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "PluginSplitter.hpp" | |||
| #include "SplitterMutators.hpp" | |||
| #include "SplitterProcessors.hpp" | |||
| namespace { | |||
| void checkSplitterCommon(PluginSplitter* original, PluginSplitter* clone) { | |||
| // Check the chains | |||
| REQUIRE(original->chains.size() == clone->chains.size()); | |||
| for (int chainIndex {0}; chainIndex < original->chains.size(); chainIndex++) { | |||
| PluginChainWrapper originalChain = original->chains[chainIndex]; | |||
| PluginChainWrapper clonedChain = clone->chains[chainIndex]; | |||
| REQUIRE(originalChain.chain->chain.size() == clonedChain.chain->chain.size()); | |||
| CHECK(originalChain.isSoloed == clonedChain.isSoloed); | |||
| for (int slotIndex {0}; slotIndex < originalChain.chain->chain.size(); slotIndex++) { | |||
| std::shared_ptr<ChainSlotBase> originalSlot = originalChain.chain->chain[slotIndex]; | |||
| std::shared_ptr<ChainSlotBase> clonedSlot = clonedChain.chain->chain[slotIndex]; | |||
| if (std::shared_ptr<ChainSlotPlugin> originalPluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(originalSlot)) { | |||
| // It's a plugin slot, check the plugin | |||
| auto clonedPluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(clonedSlot); | |||
| REQUIRE(clonedPluginSlot != nullptr); | |||
| CHECK(clonedPluginSlot->plugin == originalPluginSlot->plugin); | |||
| CHECK(clonedPluginSlot->modulationConfig != originalPluginSlot->modulationConfig); | |||
| } else { | |||
| // It's a gain stage, check the values | |||
| auto originalGainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(originalSlot); | |||
| auto clonedGainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(clonedSlot); | |||
| REQUIRE(originalGainStage != nullptr); | |||
| REQUIRE(clonedGainStage != nullptr); | |||
| CHECK(clonedGainStage->gain == originalGainStage->gain); | |||
| CHECK(clonedGainStage->pan == originalGainStage->pan); | |||
| } | |||
| } | |||
| } | |||
| CHECK(original->numChainsSoloed == clone->numChainsSoloed); | |||
| CHECK(original->config.layout == clone->config.layout); | |||
| CHECK(original->config.sampleRate == clone->config.sampleRate); | |||
| CHECK(original->config.blockSize == clone->config.blockSize); | |||
| CHECK(original->getModulationValueCallback(0, MODULATION_TYPE::MACRO) == 1.2f); | |||
| CHECK(clone->getModulationValueCallback(0, MODULATION_TYPE::MACRO) == 1.2f); | |||
| CHECK(clone->shouldNotifyProcessorOnLatencyChange == true); | |||
| } | |||
| void checkBuffers(juce::AudioBuffer<float>& buffer1, juce::AudioBuffer<float>& buffer2) { | |||
| // Check the buffers are the same size | |||
| REQUIRE(buffer1.getNumChannels() == buffer2.getNumChannels()); | |||
| REQUIRE(buffer1.getNumSamples() == buffer2.getNumSamples()); | |||
| // Check the buffers have the same values | |||
| for (int channelIndex {0}; channelIndex < buffer1.getNumChannels(); channelIndex++) { | |||
| for (int sampleIndex {0}; sampleIndex < buffer1.getNumSamples(); sampleIndex++) { | |||
| CHECK(buffer1.getReadPointer(channelIndex)[sampleIndex] == buffer2.getReadPointer(channelIndex)[sampleIndex]); | |||
| } | |||
| } | |||
| // Mofify one of the buffers, check they're different | |||
| buffer1.getWritePointer(0)[0] = 0.5f; | |||
| CHECK(buffer1.getReadPointer(0)[0] != buffer2.getReadPointer(0)[0]); | |||
| } | |||
| } | |||
| SCENARIO("PluginSplitterSeries: Clone works correctly") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("A splitter with one plugin slot and one gain stage") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for it later | |||
| return 1.2f; | |||
| }; | |||
| int latencyCalled {0}; | |||
| int receivedLatency {0}; | |||
| auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { | |||
| latencyCalled++; | |||
| receivedLatency = latency; | |||
| }; | |||
| auto splitter = std::make_shared<PluginSplitterSeries>(hostConfig, modulationCallback, latencyCallback); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| SplitterMutators::insertPlugin(splitter, plugin, 0, 0); | |||
| SplitterMutators::insertGainStage(splitter, 0, 1); | |||
| // Set some unique values so we can test for them later | |||
| SplitterMutators::setChainSolo(splitter, 0, true); | |||
| // Reset before starting the tests | |||
| latencyCalled = 0; | |||
| receivedLatency = 0; | |||
| WHEN("It is cloned") { | |||
| PluginSplitterSeries* clonedSplitter = splitter->clone(); | |||
| splitter->shouldNotifyProcessorOnLatencyChange = false; | |||
| THEN("The cloned splitter is equal to the original") { | |||
| checkSplitterCommon(splitter.get(), clonedSplitter); | |||
| // Check that when a plugin updates its latency, the latency callback is called from | |||
| // the new splitter only | |||
| plugin->setLatencySamples(15); | |||
| messageManager->runDispatchLoopUntil(10); | |||
| CHECK(latencyCalled == 1); | |||
| CHECK(receivedLatency == 15); | |||
| } | |||
| delete clonedSplitter; | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| SCENARIO("PluginSplitterParallel: Clone works correctly") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("A splitter with one plugin slot and one gain stage") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for it later | |||
| return 1.2f; | |||
| }; | |||
| int latencyCalled {0}; | |||
| int receivedLatency {0}; | |||
| auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { | |||
| latencyCalled++; | |||
| receivedLatency = latency; | |||
| }; | |||
| auto splitter = std::make_shared<PluginSplitterParallel>(hostConfig, modulationCallback, latencyCallback); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| SplitterMutators::insertPlugin(splitter, plugin, 0, 0); | |||
| SplitterMutators::addChain(splitter); | |||
| SplitterMutators::insertGainStage(splitter, 1, 1); | |||
| // Set some unique values so we can test for them later | |||
| SplitterMutators::setChainSolo(splitter, 0, true); | |||
| // Initialise the buffers | |||
| SplitterProcessors::prepareToPlay(*splitter.get(), hostConfig.sampleRate, hostConfig.blockSize, hostConfig.layout); | |||
| // Reset before starting the tests | |||
| latencyCalled = 0; | |||
| receivedLatency = 0; | |||
| WHEN("It is cloned") { | |||
| PluginSplitterParallel* clonedSplitter = splitter->clone(); | |||
| splitter->shouldNotifyProcessorOnLatencyChange = false; | |||
| THEN("The cloned splitter is equal to the original") { | |||
| checkSplitterCommon(splitter.get(), clonedSplitter); | |||
| // Check that when a plugin updates its latency, the latency callback is called from | |||
| // the new splitter only | |||
| plugin->setLatencySamples(15); | |||
| messageManager->runDispatchLoopUntil(10); | |||
| CHECK(latencyCalled == 1); | |||
| CHECK(receivedLatency == 15); | |||
| // Check that the buffers are equal | |||
| checkBuffers(*splitter->inputBuffer, *clonedSplitter->inputBuffer); | |||
| checkBuffers(*splitter->outputBuffer, *clonedSplitter->outputBuffer); | |||
| } | |||
| delete clonedSplitter; | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| SCENARIO("PluginSplitterMultiband: Clone works correctly") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("A splitter with one plugin slot and one gain stage") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for it later | |||
| return 1.2f; | |||
| }; | |||
| int latencyCalled {0}; | |||
| int receivedLatency {0}; | |||
| auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { | |||
| latencyCalled++; | |||
| receivedLatency = latency; | |||
| }; | |||
| auto splitter = std::make_shared<PluginSplitterMultiband>(hostConfig, modulationCallback, latencyCallback); | |||
| // Workaround for having constructed the multiband splitter directly | |||
| CrossoverMutators::setPluginChain(splitter->crossover, 0, splitter->chains[0].chain); | |||
| CrossoverMutators::setPluginChain(splitter->crossover, 1, splitter->chains[1].chain); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| SplitterMutators::insertPlugin(splitter, plugin, 0, 0); | |||
| SplitterMutators::addBand(splitter); | |||
| SplitterMutators::insertGainStage(splitter, 1, 1); | |||
| // Set some unique values so we can test for them later | |||
| SplitterMutators::setChainSolo(splitter, 0, true); | |||
| SplitterMutators::setCrossoverFrequency(splitter, 0, 150); | |||
| SplitterMutators::setCrossoverFrequency(splitter, 1, 250); | |||
| // Initialise the buffers | |||
| SplitterProcessors::prepareToPlay(*splitter.get(), hostConfig.sampleRate, hostConfig.blockSize, hostConfig.layout); | |||
| // Reset before starting the tests | |||
| latencyCalled = 0; | |||
| receivedLatency = 0; | |||
| // Get a reference to the original crossover state so we can check which splitter it's in | |||
| // after cloning | |||
| std::shared_ptr<PluginChain> originalChain = splitter->chains[0].chain; | |||
| REQUIRE(splitter->chains.size() == 3); | |||
| REQUIRE(splitter->chains[0].chain->chain.size() == 1); | |||
| REQUIRE(splitter->chains[1].chain->chain.size() == 1); | |||
| WHEN("It is cloned") { | |||
| PluginSplitterMultiband* clonedSplitter = splitter->clone(); | |||
| splitter->shouldNotifyProcessorOnLatencyChange = false; | |||
| THEN("The cloned splitter is equal to the original") { | |||
| checkSplitterCommon(splitter.get(), clonedSplitter); | |||
| // Check that when a plugin updates its latency, the latency callback is called from | |||
| // the new splitter only | |||
| plugin->setLatencySamples(15); | |||
| messageManager->runDispatchLoopUntil(10); | |||
| CHECK(latencyCalled == 1); | |||
| CHECK(receivedLatency == 15); | |||
| // Check the crossover is equal but not the same | |||
| CHECK(splitter->crossover != clonedSplitter->crossover); | |||
| CHECK(splitter->crossover->bands[0].chain == originalChain); | |||
| CHECK(clonedSplitter->crossover->bands[0].chain != originalChain); | |||
| CHECK(splitter->crossover->lowpassFilters.size() == clonedSplitter->crossover->lowpassFilters.size()); | |||
| CHECK(splitter->crossover->highpassFilters.size() == clonedSplitter->crossover->highpassFilters.size()); | |||
| CHECK(splitter->crossover->allpassFilters.size() == clonedSplitter->crossover->allpassFilters.size()); | |||
| CHECK(splitter->crossover->buffers.size() == clonedSplitter->crossover->buffers.size()); | |||
| CHECK(splitter->crossover->bands.size() == clonedSplitter->crossover->bands.size()); | |||
| CHECK(splitter->crossover->config.layout == clonedSplitter->crossover->config.layout); | |||
| CHECK(splitter->crossover->config.sampleRate == clonedSplitter->crossover->config.sampleRate); | |||
| CHECK(splitter->crossover->config.blockSize == clonedSplitter->crossover->config.blockSize); | |||
| CHECK(splitter->crossover->numBandsSoloed == clonedSplitter->crossover->numBandsSoloed); | |||
| for (int filterIndex {0}; filterIndex < splitter->crossover->lowpassFilters.size(); filterIndex++) { | |||
| CHECK(splitter->crossover->lowpassFilters[filterIndex]->getType() == clonedSplitter->crossover->lowpassFilters[filterIndex]->getType()); | |||
| CHECK(splitter->crossover->lowpassFilters[filterIndex]->getCutoffFrequency() == clonedSplitter->crossover->lowpassFilters[filterIndex]->getCutoffFrequency()); | |||
| CHECK(splitter->crossover->highpassFilters[filterIndex]->getType() == clonedSplitter->crossover->highpassFilters[filterIndex]->getType()); | |||
| CHECK(splitter->crossover->highpassFilters[filterIndex]->getCutoffFrequency() == clonedSplitter->crossover->highpassFilters[filterIndex]->getCutoffFrequency()); | |||
| } | |||
| for (int filterIndex {0}; filterIndex < splitter->crossover->allpassFilters.size(); filterIndex++) { | |||
| CHECK(splitter->crossover->allpassFilters[filterIndex]->getType() == clonedSplitter->crossover->allpassFilters[filterIndex]->getType()); | |||
| CHECK(splitter->crossover->allpassFilters[filterIndex]->getCutoffFrequency() == clonedSplitter->crossover->allpassFilters[filterIndex]->getCutoffFrequency()); | |||
| } | |||
| } | |||
| delete clonedSplitter; | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| SCENARIO("PluginSplitterLeftRight: Clone works correctly") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("A splitter with one plugin slot and one gain stage") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for it later | |||
| return 1.2f; | |||
| }; | |||
| int latencyCalled {0}; | |||
| int receivedLatency {0}; | |||
| auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { | |||
| latencyCalled++; | |||
| receivedLatency = latency; | |||
| }; | |||
| auto splitter = std::make_shared<PluginSplitterLeftRight>(hostConfig, modulationCallback, latencyCallback); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| SplitterMutators::insertPlugin(splitter, plugin, 0, 0); | |||
| SplitterMutators::insertGainStage(splitter, 1, 1); | |||
| // Set some unique values so we can test for them later | |||
| SplitterMutators::setChainSolo(splitter, 0, true); | |||
| // Initialise the buffers | |||
| SplitterProcessors::prepareToPlay(*splitter.get(), hostConfig.sampleRate, hostConfig.blockSize, hostConfig.layout); | |||
| // Reset before starting the tests | |||
| latencyCalled = 0; | |||
| receivedLatency = 0; | |||
| WHEN("It is cloned") { | |||
| PluginSplitterLeftRight* clonedSplitter = splitter->clone(); | |||
| splitter->shouldNotifyProcessorOnLatencyChange = false; | |||
| THEN("The cloned splitter is equal to the original") { | |||
| checkSplitterCommon(splitter.get(), clonedSplitter); | |||
| // Check that when a plugin updates its latency, the latency callback is called from | |||
| // the new splitter only | |||
| plugin->setLatencySamples(15); | |||
| messageManager->runDispatchLoopUntil(10); | |||
| CHECK(latencyCalled == 1); | |||
| CHECK(receivedLatency == 15); | |||
| // Check that the buffers are equal | |||
| checkBuffers(*splitter->leftBuffer, *clonedSplitter->leftBuffer); | |||
| checkBuffers(*splitter->rightBuffer, *clonedSplitter->rightBuffer); | |||
| } | |||
| delete clonedSplitter; | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| SCENARIO("PluginSplitterMidSide: Clone works correctly") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("A splitter with one plugin slot and one gain stage") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for it later | |||
| return 1.2f; | |||
| }; | |||
| int latencyCalled {0}; | |||
| int receivedLatency {0}; | |||
| auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { | |||
| latencyCalled++; | |||
| receivedLatency = latency; | |||
| }; | |||
| auto splitter = std::make_shared<PluginSplitterMidSide>(hostConfig, modulationCallback, latencyCallback); | |||
| auto plugin = std::make_shared<TestUtils::TestPluginInstance>(); | |||
| SplitterMutators::insertPlugin(splitter, plugin, 0, 0); | |||
| SplitterMutators::insertGainStage(splitter, 1, 1); | |||
| // Set some unique values so we can test for them later | |||
| SplitterMutators::setChainSolo(splitter, 0, true); | |||
| // Initialise the buffers | |||
| SplitterProcessors::prepareToPlay(*splitter.get(), hostConfig.sampleRate, hostConfig.blockSize, hostConfig.layout); | |||
| // Reset before starting the tests | |||
| latencyCalled = 0; | |||
| receivedLatency = 0; | |||
| WHEN("It is cloned") { | |||
| PluginSplitterMidSide* clonedSplitter = splitter->clone(); | |||
| THEN("The cloned splitter is equal to the original") { | |||
| checkSplitterCommon(splitter.get(), clonedSplitter); | |||
| splitter->shouldNotifyProcessorOnLatencyChange = false; | |||
| // Check that when a plugin updates its latency, the latency callback is called from | |||
| // the new splitter only | |||
| plugin->setLatencySamples(15); | |||
| messageManager->runDispatchLoopUntil(10); | |||
| CHECK(latencyCalled == 1); | |||
| CHECK(receivedLatency == 15); | |||
| // Check that the buffers are equal | |||
| checkBuffers(*splitter->midBuffer, *clonedSplitter->midBuffer); | |||
| checkBuffers(*splitter->sideBuffer, *clonedSplitter->sideBuffer); | |||
| } | |||
| delete clonedSplitter; | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| #pragma once | |||
| enum class SPLIT_TYPE { | |||
| SERIES, | |||
| PARALLEL, | |||
| MULTIBAND, | |||
| LEFTRIGHT, | |||
| MIDSIDE | |||
| }; | |||
| @@ -0,0 +1,5 @@ | |||
| #pragma once | |||
| #include "DataModelInterface.hpp" | |||
| #include "MutatorsInterface.hpp" | |||
| #include "ProcessingInterface.hpp" | |||
| @@ -0,0 +1,216 @@ | |||
| #include "ChainMutators.hpp" | |||
| #include "ChainSlotProcessors.hpp" | |||
| namespace ChainMutators { | |||
| void insertPlugin(std::shared_ptr<PluginChain> chain, std::shared_ptr<juce::AudioPluginInstance> plugin, int position, HostConfiguration config) { | |||
| if (chain->chain.size() > position) { | |||
| chain->chain.insert(chain->chain.begin() + position, std::make_shared<ChainSlotPlugin>(plugin, false, chain->getModulationValueCallback, config)); | |||
| } else { | |||
| // If the position is bigger than the chain just add it to the end | |||
| chain->chain.push_back(std::make_shared<ChainSlotPlugin>(plugin, false, chain->getModulationValueCallback, config)); | |||
| } | |||
| plugin->addListener(&chain->latencyListener); | |||
| chain->latencyListener.onPluginChainUpdate(); | |||
| } | |||
| void replacePlugin(std::shared_ptr<PluginChain> chain, std::shared_ptr<juce::AudioPluginInstance> plugin, int position, HostConfiguration config) { | |||
| if (chain->chain.size() > position) { | |||
| // If it's a plugin remove the listener so we don't continue getting updates if it's kept | |||
| // alive somewhere else | |||
| if (const auto oldPluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(chain->chain[position])) { | |||
| oldPluginSlot->plugin->removeListener(&chain->latencyListener); | |||
| } | |||
| chain->chain[position] = std::make_unique<ChainSlotPlugin>(plugin, false, chain->getModulationValueCallback, config); | |||
| } else { | |||
| // If the position is bigger than the chain just add it to the end | |||
| chain->chain.push_back(std::make_unique<ChainSlotPlugin>(plugin, false, chain->getModulationValueCallback, config)); | |||
| } | |||
| plugin->addListener(&chain->latencyListener); | |||
| chain->latencyListener.onPluginChainUpdate(); | |||
| } | |||
| bool removeSlot(std::shared_ptr<PluginChain> chain, int position) { | |||
| if (chain->chain.size() > position) { | |||
| // If it's a plugin remove the listener so we don't continue getting updates if it's kept | |||
| // alive somewhere else | |||
| if (const auto oldPluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(chain->chain[position])) { | |||
| oldPluginSlot->plugin->removeListener(&chain->latencyListener); | |||
| } | |||
| chain->chain.erase(chain->chain.begin() + position); | |||
| chain->latencyListener.onPluginChainUpdate(); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| void insertGainStage(std::shared_ptr<PluginChain> chain, int position, HostConfiguration config) { | |||
| auto gainStage = std::make_shared<ChainSlotGainStage>(1, 0, false, config.layout); | |||
| ChainProcessors::prepareToPlay(*gainStage.get(), config); | |||
| if (chain->chain.size() > position) { | |||
| chain->chain.insert(chain->chain.begin() + position, std::move(gainStage)); | |||
| } else { | |||
| // If the position is bigger than the chain just add it to the end | |||
| chain->chain.push_back(std::move(gainStage)); | |||
| } | |||
| } | |||
| std::shared_ptr<juce::AudioPluginInstance> getPlugin(std::shared_ptr<PluginChain> chain, int position) { | |||
| if (chain->chain.size() > position) { | |||
| if (const auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(chain->chain[position])) { | |||
| return pluginSlot->plugin; | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| bool setPluginModulationConfig(std::shared_ptr<PluginChain> chain, | |||
| PluginModulationConfig config, | |||
| int position) { | |||
| if (chain->chain.size() > position) { | |||
| if (const auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(chain->chain[position])) { | |||
| pluginSlot->modulationConfig = std::make_shared<PluginModulationConfig>(config); | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| PluginModulationConfig getPluginModulationConfig(std::shared_ptr<PluginChain> chain, int position) { | |||
| PluginModulationConfig retVal; | |||
| if (chain->chain.size() > position) { | |||
| if (const auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(chain->chain[position])) { | |||
| retVal = *pluginSlot->modulationConfig.get(); | |||
| } | |||
| } | |||
| return retVal; | |||
| } | |||
| bool setSlotBypass(std::shared_ptr<PluginChain> chain, int position, bool isBypassed) { | |||
| if (chain->chain.size() > position) { | |||
| if (chain->chain[position]->isBypassed != isBypassed) { | |||
| chain->chain[position]->isBypassed = isBypassed; | |||
| // Trigger an update to the latency compensation | |||
| chain->latencyListener.onPluginChainUpdate(); | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool getSlotBypass(std::shared_ptr<PluginChain> chain, int position) { | |||
| if (chain->chain.size() > position) { | |||
| return chain->chain[position]->isBypassed; | |||
| } | |||
| return false; | |||
| } | |||
| void setChainBypass(std::shared_ptr<PluginChain> chain, bool val) { | |||
| chain->isChainBypassed = val; | |||
| // Trigger an update to the latency compensation | |||
| chain->latencyListener.onPluginChainUpdate(); | |||
| } | |||
| void setChainMute(std::shared_ptr<PluginChain> chain, bool val) { | |||
| chain->isChainMuted = val; | |||
| } | |||
| bool setGainLinear(std::shared_ptr<PluginChain> chain, int position, float gain) { | |||
| if (chain->chain.size() > position) { | |||
| if (const auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(chain->chain[position])) { | |||
| // TODO bounds check | |||
| gainStage->gain = gain; | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| float getGainLinear(std::shared_ptr<PluginChain> chain, int position) { | |||
| if (chain->chain.size() > position) { | |||
| if (const auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(chain->chain[position])) { | |||
| return gainStage->gain; | |||
| } | |||
| } | |||
| return 0.0f; | |||
| } | |||
| float getGainStageOutputAmplitude(std::shared_ptr<PluginChain> chain, int position, int channelNumber) { | |||
| if (chain->chain.size() > position) { | |||
| if (const auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(chain->chain[position])) { | |||
| if (channelNumber < gainStage->meterEnvelopes.size()) { | |||
| return gainStage->meterEnvelopes[channelNumber].getLastOutput(); | |||
| } | |||
| } | |||
| } | |||
| return 0.0f; | |||
| } | |||
| bool setPan(std::shared_ptr<PluginChain> chain, int position, float pan) { | |||
| if (chain->chain.size() > position) { | |||
| if (const auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(chain->chain[position])) { | |||
| // TODO bounds check | |||
| gainStage->pan = pan; | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| float getPan(std::shared_ptr<PluginChain> chain, int position) { | |||
| if (chain->chain.size() > position) { | |||
| if (const auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(chain->chain[position])) { | |||
| return gainStage->pan; | |||
| } | |||
| } | |||
| return 0.0f; | |||
| } | |||
| void setRequiredLatency(std::shared_ptr<PluginChain> chain, int numSamples, HostConfiguration config) { | |||
| // The compensation is the amount of latency we need to add artificially to the latency of the | |||
| // plugins in this chain in order to meet the required amount | |||
| // If this is the slowest chain owned by the splitter this should be 0 | |||
| const int compensation {std::max(numSamples - chain->latencyListener.calculatedTotalPluginLatency, 0)}; | |||
| WECore::AudioSpinLock lock(chain->latencyCompLineMutex); | |||
| chain->latencyCompLine.reset(new CloneableDelayLineType(compensation)); | |||
| chain->latencyCompLine->prepare({ | |||
| config.sampleRate, | |||
| static_cast<juce::uint32>(config.blockSize), | |||
| static_cast<juce::uint32>(getTotalNumInputChannels(config.layout)) | |||
| }); | |||
| chain->latencyCompLine->setDelay(compensation); | |||
| } | |||
| std::shared_ptr<PluginEditorBounds> getPluginEditorBounds(std::shared_ptr<PluginChain> chain, int position) { | |||
| std::shared_ptr<PluginEditorBounds> retVal(new PluginEditorBounds()); | |||
| if (chain->chain.size() > position) { | |||
| if (const auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(chain->chain[position])) { | |||
| retVal = pluginSlot->editorBounds; | |||
| } | |||
| } | |||
| return retVal; | |||
| } | |||
| } | |||
| @@ -0,0 +1,113 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginChain.hpp" | |||
| namespace ChainMutators { | |||
| /** | |||
| * Inserts a plugin at the given position, or at the end if that position doesn't exist. | |||
| */ | |||
| void insertPlugin(std::shared_ptr<PluginChain> chain, | |||
| std::shared_ptr<juce::AudioPluginInstance> plugin, | |||
| int position, | |||
| HostConfiguration config); | |||
| /** | |||
| * Replaces an existing plugin in the chain. | |||
| */ | |||
| void replacePlugin(std::shared_ptr<PluginChain> chain, | |||
| std::shared_ptr<juce::AudioPluginInstance> plugin, | |||
| int position, | |||
| HostConfiguration config); | |||
| /** | |||
| * Removes the plugin or gain stage at the given position in the chain. | |||
| */ | |||
| bool removeSlot(std::shared_ptr<PluginChain> chain, int position); | |||
| /** | |||
| * Inserts a gain stage at the given position, or at the end if that position doesn't exist. | |||
| */ | |||
| void insertGainStage(std::shared_ptr<PluginChain> chain, int position, HostConfiguration config); | |||
| /** | |||
| * Returns a pointer to the plugin at the given position. | |||
| */ | |||
| std::shared_ptr<juce::AudioPluginInstance> getPlugin(std::shared_ptr<PluginChain> chain, | |||
| int position); | |||
| /** | |||
| * Set the modulation config for the given plugin to the one provided. | |||
| */ | |||
| bool setPluginModulationConfig(std::shared_ptr<PluginChain> chain, | |||
| PluginModulationConfig config, | |||
| int position); | |||
| /** | |||
| * Returns the modulation config for the given plugin. | |||
| */ | |||
| PluginModulationConfig getPluginModulationConfig(std::shared_ptr<PluginChain> chain, | |||
| int position); | |||
| /** | |||
| * Returns the number of plugins and gain stages in this chain. | |||
| */ | |||
| inline size_t getNumSlots(std::shared_ptr<PluginChain> chain) { return chain->chain.size(); } | |||
| /** | |||
| * Bypasses or enables the slot at the given position. | |||
| */ | |||
| bool setSlotBypass(std::shared_ptr<PluginChain> chain, int position, bool isBypassed); | |||
| /** | |||
| * Returns true if the slot is bypassed. | |||
| */ | |||
| bool getSlotBypass(std::shared_ptr<PluginChain> chain, int position); | |||
| /** | |||
| * Bypasses the entire chain if set to true. | |||
| */ | |||
| void setChainBypass(std::shared_ptr<PluginChain> chain, bool val); | |||
| /** | |||
| * Mutes the entire chain if set to true. | |||
| */ | |||
| void setChainMute(std::shared_ptr<PluginChain> chain, bool val); | |||
| /** | |||
| * Sets the gain for the gain stage at the given position. | |||
| */ | |||
| bool setGainLinear(std::shared_ptr<PluginChain> chain, int position, float gain); | |||
| /** | |||
| * Returns the gain for the gain stage at the given position. | |||
| */ | |||
| float getGainLinear(std::shared_ptr<PluginChain> chain, int position); | |||
| float getGainStageOutputAmplitude(std::shared_ptr<PluginChain> chain, int position, int channelNumber); | |||
| /** | |||
| * Sets the pan/balance for the gain stage at the given position. | |||
| */ | |||
| bool setPan(std::shared_ptr<PluginChain> chain, int position, float pan); | |||
| /** | |||
| * Returns the pan/balance for the gain stage at the given position. | |||
| */ | |||
| float getPan(std::shared_ptr<PluginChain> chain, int position); | |||
| /** | |||
| * Sets the total amount of latency this chain should aim for to keep it inline with other | |||
| * chains. | |||
| * | |||
| * The chain can't reduce its latency below the total of the plugins it hosts, but it can | |||
| * increase its latency to match slower chains. | |||
| */ | |||
| void setRequiredLatency(std::shared_ptr<PluginChain> chain, int numSamples, HostConfiguration config); | |||
| /** | |||
| * Returns a pointer to the bounds for this plugin's editor. Will pointer to an empty optional | |||
| * if there isn't a plugin at the given position. | |||
| */ | |||
| std::shared_ptr<PluginEditorBounds> getPluginEditorBounds(std::shared_ptr<PluginChain> chain, int position); | |||
| } | |||
| @@ -0,0 +1,529 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "ChainMutators.hpp" | |||
| #include "ChainProcessors.hpp" | |||
| namespace { | |||
| constexpr int SAMPLE_RATE {44100}; | |||
| constexpr int NUM_SAMPLES {64}; | |||
| class MutatorTestPluginInstance : public TestUtils::TestPluginInstance { | |||
| public: | |||
| class PluginParameter : public Parameter { | |||
| public: | |||
| float value; | |||
| juce::String name; | |||
| PluginParameter(juce::String newName) : value(0), name(newName) { } | |||
| float getValue() const override { return value; } | |||
| void setValue(float newValue) override { value = newValue; } | |||
| float getDefaultValue() const override { return 0; } | |||
| juce::String getName(int maximumStringLength) const override { return name; } | |||
| juce::String getLabel() const override { return name; } | |||
| juce::String getParameterID() const override { return name; } | |||
| }; | |||
| juce::AudioProcessorListener* addedListener; | |||
| juce::AudioProcessorListener* removedListener; | |||
| MutatorTestPluginInstance() : addedListener(nullptr), removedListener(nullptr) { | |||
| addHostedParameter(std::make_unique<PluginParameter>("param1")); | |||
| addHostedParameter(std::make_unique<PluginParameter>("param2")); | |||
| addHostedParameter(std::make_unique<PluginParameter>("param3")); | |||
| } | |||
| void addListener(juce::AudioProcessorListener* newListener) { | |||
| addedListener = newListener; | |||
| TestUtils::TestPluginInstance::addListener(newListener); | |||
| } | |||
| void removeListener(juce::AudioProcessorListener* listenerToRemove) override { | |||
| removedListener = listenerToRemove; | |||
| TestUtils::TestPluginInstance::removeListener(listenerToRemove); | |||
| } | |||
| }; | |||
| } | |||
| SCENARIO("ChainMutators: Slots can be added, replaced, and removed") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("An empty chain") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 0.0f; | |||
| }; | |||
| const auto layout = TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 0); | |||
| // Catch2 resets the state if we use multiple WHEN clauses. We could use AND_WHEN, but they | |||
| // need to be nested which makes then hard to read, so instead we use a single WHEN. | |||
| WHEN("The chain is modified") { | |||
| // WHEN("A plugin is added") | |||
| { | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(10); | |||
| ChainMutators::insertPlugin(chain, plugin, 0, hostConfig); | |||
| // THEN("The chain contains a single plugin") | |||
| CHECK(chain->chain.size() == 1); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 1); | |||
| CHECK(ChainMutators::getPlugin(chain, 0) == plugin); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 10); | |||
| CHECK(plugin->addedListener == &chain->latencyListener); | |||
| } | |||
| // WHEN("A plugin is added at position > chains.size()") | |||
| { | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(15); | |||
| ChainMutators::insertPlugin(chain, plugin, 5, hostConfig); | |||
| // THEN("The new plugin is added at the end") | |||
| CHECK(chain->chain.size() == 2); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 2); | |||
| CHECK(ChainMutators::getPlugin(chain, 1) == plugin); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 25); | |||
| CHECK(plugin->addedListener == &chain->latencyListener); | |||
| } | |||
| // WHEN("A plugin is added in the middle") | |||
| { | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(20); | |||
| ChainMutators::insertPlugin(chain, plugin, 1, hostConfig); | |||
| // THEN("The new plugin is added in the correct place") | |||
| CHECK(chain->chain.size() == 3); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 3); | |||
| CHECK(ChainMutators::getPlugin(chain, 1) == plugin); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 45); | |||
| CHECK(plugin->addedListener == &chain->latencyListener); | |||
| } | |||
| // WHEN("A gain stage is added in the middle") | |||
| { | |||
| ChainMutators::insertGainStage(chain, 2, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| // THEN("The new gain stage is added in the correct place") | |||
| CHECK(chain->chain.size() == 4); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 4); | |||
| CHECK(ChainMutators::getPlugin(chain, 2) == nullptr); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 45); | |||
| } | |||
| // WHEN("A gain stage is added at position > chains.size()") | |||
| { | |||
| ChainMutators::insertGainStage(chain, 10, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| // THEN("The new gain stage is added at the end") | |||
| CHECK(chain->chain.size() == 5); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 5); | |||
| CHECK(ChainMutators::getPlugin(chain, 4) == nullptr); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 45); | |||
| } | |||
| // WHEN("A plugin is replaced") | |||
| { | |||
| REQUIRE(ChainMutators::getPlugin(chain, 1) != nullptr); | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(25); | |||
| auto oldPlugin = std::dynamic_pointer_cast<MutatorTestPluginInstance>(ChainMutators::getPlugin(chain, 1)); | |||
| ChainMutators::replacePlugin(chain, plugin, 1, hostConfig); | |||
| // THEN("The new plugin is in the correct place") | |||
| CHECK(chain->chain.size() == 5); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 5); | |||
| CHECK(ChainMutators::getPlugin(chain, 1) == plugin); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 50); | |||
| CHECK(oldPlugin->removedListener == &chain->latencyListener); | |||
| CHECK(plugin->addedListener == &chain->latencyListener); | |||
| } | |||
| // WHEN("A gain stage is replaced with a plugin") | |||
| { | |||
| REQUIRE(ChainMutators::getPlugin(chain, 2) == nullptr); | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(30); | |||
| ChainMutators::replacePlugin(chain, plugin, 2, hostConfig); | |||
| // THEN("The new plugin is in the correct place") | |||
| CHECK(chain->chain.size() == 5); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 5); | |||
| CHECK(ChainMutators::getPlugin(chain, 2) == plugin); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 80); | |||
| CHECK(plugin->addedListener == &chain->latencyListener); | |||
| } | |||
| // WHEN("A plugin is replaced at position > chains.size()") | |||
| { | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(35); | |||
| ChainMutators::replacePlugin(chain, plugin, 10, hostConfig); | |||
| // THEN("The new plugin is added at the end") | |||
| CHECK(chain->chain.size() == 6); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 6); | |||
| CHECK(ChainMutators::getPlugin(chain, 5) == plugin); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 115); | |||
| CHECK(plugin->addedListener == &chain->latencyListener); | |||
| } | |||
| // WHEN("A plugin is removed") | |||
| { | |||
| REQUIRE(ChainMutators::getPlugin(chain, 2) != nullptr); | |||
| // Figure out what we expect the chain to look like before making changes | |||
| auto expectedChain = chain->chain; | |||
| expectedChain.erase(expectedChain.begin() + 2); | |||
| auto oldPlugin = std::dynamic_pointer_cast<MutatorTestPluginInstance>(ChainMutators::getPlugin(chain, 2)); | |||
| const bool success {ChainMutators::removeSlot(chain, 2)}; | |||
| // THEN("The plugin is removed from the correct place") | |||
| CHECK(success); | |||
| CHECK(chain->chain.size() == 5); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 5); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 85); | |||
| CHECK(oldPlugin->removedListener == &chain->latencyListener); | |||
| for (int slotIndex {0}; slotIndex < expectedChain.size(); slotIndex++) { | |||
| CHECK(expectedChain[slotIndex] == chain->chain[slotIndex]); | |||
| } | |||
| } | |||
| // WHEN("A gain stage is removed") | |||
| { | |||
| REQUIRE(ChainMutators::getPlugin(chain, 3) == nullptr); | |||
| // Figure out what we expect the chain to look like before making changes | |||
| auto expectedChain = chain->chain; | |||
| expectedChain.erase(expectedChain.begin() + 3); | |||
| const bool success {ChainMutators::removeSlot(chain, 3)}; | |||
| // THEN("The plugin is removed from the correct place") | |||
| CHECK(success); | |||
| CHECK(chain->chain.size() == 4); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 4); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 85); | |||
| for (int slotIndex {0}; slotIndex < expectedChain.size(); slotIndex++) { | |||
| CHECK(expectedChain[slotIndex] == chain->chain[slotIndex]); | |||
| } | |||
| } | |||
| // WHEN("A plugin is removed at position > chains.size()") | |||
| { | |||
| REQUIRE(ChainMutators::getNumSlots(chain) == 4); | |||
| const bool success {ChainMutators::removeSlot(chain, 4)}; | |||
| // THEN("No plugins are removed") | |||
| CHECK(!success); | |||
| CHECK(chain->chain.size() == 4); | |||
| CHECK(ChainMutators::getNumSlots(chain) == 4); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 85); | |||
| } | |||
| // WHEN("A plugin changes latency") | |||
| { | |||
| auto plugin = ChainMutators::getPlugin(chain, 0); | |||
| REQUIRE(plugin != nullptr); | |||
| plugin->setLatencySamples(40); | |||
| // Allow the latency message to be sent | |||
| messageManager->runDispatchLoopUntil(10); | |||
| // THEN("The new latency is calculated") | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 115); | |||
| } | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| SCENARIO("ChainMutators: Modulation config can be set and retrieved") { | |||
| GIVEN("A chain with three plugins and a gain stage") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 0.0f; | |||
| }; | |||
| const auto layout = TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| ChainMutators::insertPlugin(chain, std::make_shared<MutatorTestPluginInstance>(), 0, hostConfig); | |||
| ChainMutators::insertGainStage(chain, 1, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainMutators::insertPlugin(chain, std::make_shared<MutatorTestPluginInstance>(), 2, hostConfig); | |||
| ChainMutators::insertPlugin(chain, std::make_shared<MutatorTestPluginInstance>(), 3, hostConfig); | |||
| WHEN("The config is set for a plugin") { | |||
| PluginModulationConfig config; | |||
| config.isActive = true; | |||
| auto paramConfig = std::make_shared<PluginParameterModulationConfig>(); | |||
| paramConfig->targetParameterName = "testParam"; | |||
| config.parameterConfigs.push_back(paramConfig); | |||
| const bool success {ChainMutators::setPluginModulationConfig(chain, config, 2)}; | |||
| THEN("The new config can be retrieved") { | |||
| CHECK(success); | |||
| const PluginModulationConfig retrievedConfig {ChainMutators::getPluginModulationConfig(chain, 2)}; | |||
| CHECK(retrievedConfig.isActive); | |||
| CHECK(retrievedConfig.parameterConfigs[0]->targetParameterName == config.parameterConfigs[0]->targetParameterName); | |||
| } | |||
| } | |||
| WHEN("The config is set for a gain stage") { | |||
| PluginModulationConfig config; | |||
| config.isActive = true; | |||
| auto paramConfig = std::make_shared<PluginParameterModulationConfig>(); | |||
| paramConfig->targetParameterName = "testParam"; | |||
| config.parameterConfigs.push_back(paramConfig); | |||
| const bool success {ChainMutators::setPluginModulationConfig(chain, config, 1)}; | |||
| THEN("The configs haven't changed") { | |||
| CHECK(!success); | |||
| for (int slotIndex {0}; slotIndex < ChainMutators::getNumSlots(chain); slotIndex++) { | |||
| const PluginModulationConfig retrievedConfig {ChainMutators::getPluginModulationConfig(chain, slotIndex)}; | |||
| CHECK(!retrievedConfig.isActive); | |||
| CHECK(retrievedConfig.parameterConfigs.size() == 0); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainMutators: Slot parameters can be modified and retrieved") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("A chain with two plugins and two gain stages") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 0.0f; | |||
| }; | |||
| const auto layout = TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 0); | |||
| { | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(10); | |||
| ChainMutators::insertPlugin(chain, plugin, 0, hostConfig); | |||
| } | |||
| ChainMutators::insertGainStage(chain, 1, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| { | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(15); | |||
| ChainMutators::insertPlugin(chain, plugin, 2, hostConfig); | |||
| } | |||
| ChainMutators::insertGainStage(chain, 3, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 25); | |||
| WHEN("A plugin is bypassed") { | |||
| const bool success {ChainMutators::setSlotBypass(chain, 2, true)}; | |||
| // Allow the latency message to be sent | |||
| messageManager->runDispatchLoopUntil(10); | |||
| THEN("The plugin is bypassed correctly") { | |||
| CHECK(success); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 0)); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 1)); | |||
| CHECK(ChainMutators::getSlotBypass(chain, 2)); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 3)); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 10); | |||
| } | |||
| } | |||
| WHEN("A gain stage is bypassed") { | |||
| const bool success {ChainMutators::setSlotBypass(chain, 1, true)}; | |||
| // Allow the latency message to be sent (though we don't expect one for a gain stage change) | |||
| messageManager->runDispatchLoopUntil(10); | |||
| THEN("The gain stage is bypassed correctly") { | |||
| CHECK(success); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 0)); | |||
| CHECK(ChainMutators::getSlotBypass(chain, 1)); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 2)); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 3)); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 25); | |||
| } | |||
| } | |||
| WHEN("An out of bounds slot is bypassed") { | |||
| const bool success {ChainMutators::setSlotBypass(chain, 10, true)}; | |||
| // Allow the latency message to be sent (though we don't expect one here) | |||
| messageManager->runDispatchLoopUntil(10); | |||
| THEN("Nothing is bypassed") { | |||
| CHECK(!success); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 0)); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 1)); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 2)); | |||
| CHECK(!ChainMutators::getSlotBypass(chain, 3)); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 25); | |||
| } | |||
| } | |||
| WHEN("Gain and pan is set for a plugin slot") { | |||
| const bool gainSuccess {ChainMutators::setGainLinear(chain, 2, 0.5)}; | |||
| const bool panSuccess {ChainMutators::setPan(chain, 2, -0.5)}; | |||
| THEN("Nothing is changed") { | |||
| CHECK(!gainSuccess); | |||
| CHECK(!panSuccess); | |||
| CHECK(ChainMutators::getGainLinear(chain, 2) == 0.0f); | |||
| CHECK(ChainMutators::getPan(chain, 2) == 0.0f); | |||
| } | |||
| } | |||
| WHEN("Gain and pan is set for a gain stage slot") { | |||
| const bool gainSuccess {ChainMutators::setGainLinear(chain, 1, 0.5)}; | |||
| const bool panSuccess {ChainMutators::setPan(chain, 1, -0.5)}; | |||
| THEN("The gain stage is bypassed correctly") { | |||
| CHECK(gainSuccess); | |||
| CHECK(panSuccess); | |||
| CHECK(ChainMutators::getGainLinear(chain, 1) == 0.5f); | |||
| CHECK(ChainMutators::getPan(chain, 1) == -0.5f); | |||
| } | |||
| } | |||
| WHEN("Gain and pan is set for an out of bounds slot") { | |||
| const bool gainSuccess {ChainMutators::setGainLinear(chain, 10, 0.5)}; | |||
| const bool panSuccess {ChainMutators::setPan(chain, 10, -0.5)}; | |||
| THEN("Nothing is bypassed") { | |||
| CHECK(!gainSuccess); | |||
| CHECK(!panSuccess); | |||
| } | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| SCENARIO("ChainMutators: The chain can be bypassed and muted") { | |||
| auto messageManager = juce::MessageManager::getInstance(); | |||
| GIVEN("An empty chain") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| const auto layout = TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 0); | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| plugin->setLatencySamples(10); | |||
| ChainMutators::insertPlugin(chain, plugin, 0, hostConfig); | |||
| REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 10); | |||
| WHEN("The chain is bypassed") { | |||
| REQUIRE(!chain->isChainBypassed); | |||
| ChainMutators::setChainBypass(chain, true); | |||
| // Allow the latency message to be sent | |||
| messageManager->runDispatchLoopUntil(10); | |||
| THEN("The bypass is set correctly") { | |||
| CHECK(chain->isChainBypassed); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 0); | |||
| } | |||
| } | |||
| WHEN("The chain is muted") { | |||
| REQUIRE(!chain->isChainBypassed); | |||
| ChainMutators::setChainMute(chain, true); | |||
| // Allow the latency message to be sent (though we don't expect one for mute) | |||
| messageManager->runDispatchLoopUntil(10); | |||
| THEN("The mute is set correctly") { | |||
| CHECK(chain->isChainMuted); | |||
| CHECK(chain->latencyListener.calculatedTotalPluginLatency == 10); | |||
| } | |||
| } | |||
| } | |||
| juce::MessageManager::deleteInstance(); | |||
| } | |||
| SCENARIO("ChainMutators: Chains are removed from plugins as latency listeners on destruction") { | |||
| GIVEN("A plugin chain with a single plugin") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| auto plugin = std::make_shared<MutatorTestPluginInstance>(); | |||
| ChainMutators::insertPlugin(chain, plugin, 0, hostConfig); | |||
| WHEN("The chain is deleted") { | |||
| // Get a pointer to the chain as a AudioProcessorListener before resetting | |||
| auto listener = &chain->latencyListener; | |||
| chain.reset(); | |||
| THEN("The chain is removed from the plugin as a listener") { | |||
| CHECK(plugin->removedListener == listener); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,180 @@ | |||
| #include "CrossoverMutators.hpp" | |||
| #include "CrossoverProcessors.hpp" | |||
| namespace { | |||
| constexpr int MAX_FREQ {20000}; | |||
| } | |||
| namespace CrossoverMutators { | |||
| void setIsSoloed(std::shared_ptr<CrossoverState> state, size_t bandNumber, bool isSoloed) { | |||
| if (state->bands.size() > bandNumber) { | |||
| if (state->bands[bandNumber].isSoloed != isSoloed) { | |||
| state->bands[bandNumber].isSoloed = isSoloed; | |||
| if (isSoloed) { | |||
| state->numBandsSoloed++; | |||
| } else { | |||
| state->numBandsSoloed--; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| bool setCrossoverFrequency(std::shared_ptr<CrossoverState> state, size_t crossoverNumber, double val) { | |||
| if (val > MAX_FREQ) { | |||
| return false; | |||
| } | |||
| if (state->lowpassFilters.size() <= crossoverNumber) { | |||
| return false; | |||
| } | |||
| state->lowpassFilters[crossoverNumber]->setCutoffFrequency(val); | |||
| state->highpassFilters[crossoverNumber]->setCutoffFrequency(val); | |||
| // We might also have an allpass filter to set | |||
| if (state->allpassFilters.size() > crossoverNumber - 1) { | |||
| state->allpassFilters[crossoverNumber - 1]->setCutoffFrequency(val); | |||
| } | |||
| // Make sure the crossover frequencies are still in the correct order | |||
| for (size_t otherCrossoverIndex {0}; otherCrossoverIndex < state->lowpassFilters.size(); otherCrossoverIndex++) { | |||
| const double otherCrossoverFrequency {getCrossoverFrequency(state, otherCrossoverIndex)}; | |||
| const bool needsCrossoverUpdate { | |||
| // We've moved the crossover frequency of index below another one that should be | |||
| // below it - move the other one to the new value | |||
| (val < otherCrossoverFrequency && otherCrossoverIndex < crossoverNumber) || | |||
| // We've moved the crossover frequency of index above another one that should be | |||
| // above it - move the other one to the new value | |||
| (otherCrossoverFrequency > val && crossoverNumber > otherCrossoverIndex) | |||
| }; | |||
| if (needsCrossoverUpdate) { | |||
| // We've moved the crossover frequency of index below another one that should be | |||
| // below it - move the other one to the new value | |||
| setCrossoverFrequency(state, otherCrossoverIndex, val); | |||
| } | |||
| } | |||
| return true; | |||
| } | |||
| void setPluginChain(std::shared_ptr<CrossoverState> state, size_t bandNumber, std::shared_ptr<PluginChain> chain) { | |||
| if (state->bands.size() > bandNumber) { | |||
| state->bands[bandNumber].chain = chain; | |||
| } | |||
| } | |||
| bool getIsSoloed(std::shared_ptr<CrossoverState> state, size_t bandNumber) { | |||
| if (state->bands.size() > bandNumber) { | |||
| return state->bands[bandNumber].isSoloed; | |||
| } | |||
| return false; | |||
| } | |||
| double getCrossoverFrequency(std::shared_ptr<CrossoverState> state, size_t crossoverNumber) { | |||
| if (state->lowpassFilters.size() > crossoverNumber) { | |||
| return state->lowpassFilters[crossoverNumber]->getCutoffFrequency(); | |||
| } | |||
| return 0; | |||
| } | |||
| size_t getNumBands(std::shared_ptr<CrossoverState> state) { | |||
| return state->bands.size(); | |||
| } | |||
| void addBand(std::shared_ptr<CrossoverState> state) { | |||
| const double oldHighestCrossover {CrossoverMutators::getCrossoverFrequency(state, state->lowpassFilters.size() - 1)}; | |||
| // Add all the new state | |||
| state->lowpassFilters.emplace_back(new CloneableLRFilter<float>()); | |||
| state->lowpassFilters[state->lowpassFilters.size() - 1]->setType(juce::dsp::LinkwitzRileyFilterType::lowpass); | |||
| state->highpassFilters.emplace_back(new CloneableLRFilter<float>()); | |||
| state->highpassFilters[state->highpassFilters.size() - 1]->setType(juce::dsp::LinkwitzRileyFilterType::highpass); | |||
| state->allpassFilters.emplace_back(new CloneableLRFilter<float>()); | |||
| state->allpassFilters[state->allpassFilters.size() - 1]->setType(juce::dsp::LinkwitzRileyFilterType::allpass); | |||
| state->buffers.emplace_back(); | |||
| state->bands.emplace_back(); | |||
| // Set the crossover frequency of the new band and make room for it if needed | |||
| if (oldHighestCrossover < MAX_FREQ) { | |||
| // The old highest crossover frequency is below the maximum, insert the new one halfway | |||
| // between it and the maximum | |||
| const double topBandWidth {MAX_FREQ - oldHighestCrossover}; | |||
| const double newCrossoverFreq {oldHighestCrossover + (topBandWidth / 2)}; | |||
| setCrossoverFrequency(state, state->lowpassFilters.size() - 1, newCrossoverFreq); | |||
| } else { | |||
| // The old highest crossover is at the maximum, move it halfway down to the one below | |||
| // and place the new one at the maximum | |||
| const double thirdHighestCrossover {CrossoverMutators::getCrossoverFrequency(state, state->lowpassFilters.size() - 3)}; | |||
| const double bandWidth {MAX_FREQ - thirdHighestCrossover}; | |||
| const double adjustedCrossoverFreq {thirdHighestCrossover + (bandWidth / 2)}; | |||
| setCrossoverFrequency(state, state->lowpassFilters.size() - 2, adjustedCrossoverFreq); | |||
| setCrossoverFrequency(state, state->lowpassFilters.size() - 1, MAX_FREQ); | |||
| } | |||
| CrossoverProcessors::prepareToPlay(*state.get(), state->config.sampleRate, state->config.blockSize, state->config.layout); | |||
| CrossoverProcessors::reset(*state.get()); | |||
| } | |||
| bool removeBand(std::shared_ptr<CrossoverState> state, size_t bandNumber) { | |||
| const size_t numBands {state->bands.size()}; | |||
| if (numBands < 3) { | |||
| return false; | |||
| } | |||
| if (bandNumber >= numBands) { | |||
| return false; | |||
| } | |||
| // Remove the associated band | |||
| if (state->bands[bandNumber].isSoloed) { | |||
| state->numBandsSoloed--; | |||
| } | |||
| state->bands.erase(state->bands.begin() + bandNumber); | |||
| // For the filters, it's easier to remove an arbitrary one and then reset them all | |||
| // First get all the crossover frequencies that we want to keep | |||
| std::vector<float> newCrossoverFrequencies; | |||
| for (size_t crossoverNumber {0}; crossoverNumber < numBands - 1; crossoverNumber++) { | |||
| if (bandNumber == 0 && crossoverNumber == 0) { | |||
| // We're removing the first band, so we need to remove the first crossover | |||
| } else if (crossoverNumber == bandNumber - 1) { | |||
| // We're removing the band that this crossover is below, so we need to remove this | |||
| // crossover | |||
| } else { | |||
| // Keep this crossover | |||
| newCrossoverFrequencies.push_back(getCrossoverFrequency(state, crossoverNumber)); | |||
| } | |||
| } | |||
| // Now remove an arbitrary filter | |||
| state->buffers.erase(state->buffers.end() - 1); | |||
| state->lowpassFilters.erase(state->lowpassFilters.end() - 1); | |||
| state->highpassFilters.erase(state->highpassFilters.end() - 1); | |||
| // We might also have an allpass filter to delete | |||
| if (state->allpassFilters.size() > 0) { | |||
| state->allpassFilters.erase(state->allpassFilters.end() - 1); | |||
| } | |||
| // Now we can reset the filters | |||
| for (size_t crossoverNumber {0}; crossoverNumber < newCrossoverFrequencies.size(); crossoverNumber++) { | |||
| setCrossoverFrequency(state, crossoverNumber, newCrossoverFrequencies[crossoverNumber]); | |||
| } | |||
| CrossoverProcessors::reset(*state.get()); | |||
| return true; | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| #pragma once | |||
| #include "CrossoverState.hpp" | |||
| namespace CrossoverMutators { | |||
| void setIsSoloed(std::shared_ptr<CrossoverState> state, size_t bandNumber, bool isSoloed); | |||
| bool setCrossoverFrequency(std::shared_ptr<CrossoverState> state, size_t crossoverNumber, double val); | |||
| void setPluginChain(std::shared_ptr<CrossoverState> state, size_t bandNumber, std::shared_ptr<PluginChain> chain); | |||
| bool getIsSoloed(std::shared_ptr<CrossoverState> state, size_t bandNumber); | |||
| double getCrossoverFrequency(std::shared_ptr<CrossoverState> state, size_t crossoverNumber); | |||
| size_t getNumBands(std::shared_ptr<CrossoverState> state); | |||
| void addBand(std::shared_ptr<CrossoverState> state); | |||
| bool removeBand(std::shared_ptr<CrossoverState> state, size_t bandNumber); | |||
| } | |||
| @@ -0,0 +1,733 @@ | |||
| #include "ModulationMutators.hpp" | |||
| namespace { | |||
| std::vector<WECore::ModulationSourceWrapper<double>> deleteSourceFromTargetSources( | |||
| std::vector<WECore::ModulationSourceWrapper<double>> sources, | |||
| ModulationSourceDefinition definition, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback) { | |||
| bool needsToDelete {false}; | |||
| int indexToDelete {0}; | |||
| // Iterate through each configured source | |||
| for (int sourceIndex {0}; sourceIndex < sources.size(); sourceIndex++) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(sources[sourceIndex].source); | |||
| if (thisSource == nullptr) { | |||
| continue; | |||
| } | |||
| if (thisSource->definition == definition) { | |||
| // We need to come back and delete this one | |||
| needsToDelete = true; | |||
| indexToDelete = sourceIndex; | |||
| } else if (thisSource->definition.type == definition.type && | |||
| thisSource->definition.id > definition.id) { | |||
| // We need to renumber this one since a source below it is being deleted | |||
| // We create a new source rather than modifying the existing one, since that would break undo/redo | |||
| ModulationSourceDefinition newDefinition(thisSource->definition.id - 1, thisSource->definition.type); | |||
| WECore::ModulationSourceWrapper<double> newSource; | |||
| newSource.source = std::make_shared<ModulationSourceProvider>( | |||
| newDefinition, getModulationValueCallback); | |||
| newSource.amount = sources[sourceIndex].amount; | |||
| sources[sourceIndex] = newSource; | |||
| } | |||
| } | |||
| if (needsToDelete) { | |||
| sources.erase(sources.begin() + indexToDelete); | |||
| } | |||
| return sources; | |||
| } | |||
| } | |||
| namespace ModulationMutators { | |||
| // | |||
| // LFOs | |||
| // | |||
| void addLfo(std::shared_ptr<ModelInterface::ModulationSourcesState> sources) { | |||
| std::shared_ptr<ModelInterface::CloneableLFO> newLfo {new ModelInterface::CloneableLFO()}; | |||
| newLfo->setBypassSwitch(true); | |||
| newLfo->setSampleRate(sources->hostConfig.sampleRate); | |||
| sources->lfos.push_back(newLfo); | |||
| } | |||
| bool setLfoTempoSyncSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, bool val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setTempoSyncSwitch(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoInvertSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, bool val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setInvertSwitch(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setOutputMode(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoWave(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setWave(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoTempoNumer(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setTempoNumer(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoTempoDenom(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setTempoDenom(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, double val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setFreq(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, double val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setDepth(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setLfoManualPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, double val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| sources->lfos[lfoIndex]->setManualPhase(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool addSourceToLFOFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source) { | |||
| if (sources->lfos.size() <= lfoIndex) { | |||
| return false; | |||
| } | |||
| auto sourceProvider = std::make_shared<ModulationSourceProvider>(source, sources->getModulationValueCallback); | |||
| return sources->lfos[lfoIndex]->addFreqModulationSource(std::dynamic_pointer_cast<WECore::ModulationSource<double>>(sourceProvider)); | |||
| } | |||
| bool removeSourceFromLFOFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source) { | |||
| if (sources->lfos.size() <= lfoIndex) { | |||
| return false; | |||
| } | |||
| std::vector<WECore::ModulationSourceWrapper<double>> existingSources = sources->lfos[lfoIndex]->getFreqModulationSources(); | |||
| for (const auto& existingSource : existingSources) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(existingSource.source); | |||
| if (thisSource != nullptr && thisSource->definition == source) { | |||
| return sources->lfos[lfoIndex]->removeFreqModulationSource(existingSource.source); | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool setLFOFreqModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int sourceIndex, double val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->setFreqModulationAmount(sourceIndex, val); | |||
| } | |||
| return false; | |||
| } | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFOFreqModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> retVal; | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| for (const auto& source : sources->lfos[lfoIndex]->getFreqModulationSources()) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(source.source); | |||
| retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); | |||
| } | |||
| } | |||
| return retVal; | |||
| } | |||
| bool addSourceToLFODepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source) { | |||
| if (sources->lfos.size() <= lfoIndex) { | |||
| return false; | |||
| } | |||
| auto sourceProvider = std::make_shared<ModulationSourceProvider>(source, sources->getModulationValueCallback); | |||
| return sources->lfos[lfoIndex]->addDepthModulationSource(std::dynamic_pointer_cast<WECore::ModulationSource<double>>(sourceProvider)); | |||
| } | |||
| bool removeSourceFromLFODepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source) { | |||
| if (sources->lfos.size() <= lfoIndex) { | |||
| return false; | |||
| } | |||
| std::vector<WECore::ModulationSourceWrapper<double>> existingSources = sources->lfos[lfoIndex]->getDepthModulationSources(); | |||
| for (const auto& existingSource : existingSources) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(existingSource.source); | |||
| if (thisSource != nullptr && thisSource->definition == source) { | |||
| return sources->lfos[lfoIndex]->removeDepthModulationSource(existingSource.source); | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool setLFODepthModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int sourceIndex, double val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->setDepthModulationAmount(sourceIndex, val); | |||
| } | |||
| return false; | |||
| } | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFODepthModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> retVal; | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| for (const auto& source : sources->lfos[lfoIndex]->getDepthModulationSources()) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(source.source); | |||
| retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); | |||
| } | |||
| } | |||
| return retVal; | |||
| } | |||
| bool addSourceToLFOPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source) { | |||
| if (sources->lfos.size() <= lfoIndex) { | |||
| return false; | |||
| } | |||
| auto sourceProvider = std::make_shared<ModulationSourceProvider>(source, sources->getModulationValueCallback); | |||
| return sources->lfos[lfoIndex]->addPhaseModulationSource(std::dynamic_pointer_cast<WECore::ModulationSource<double>>(sourceProvider)); | |||
| } | |||
| bool removeSourceFromLFOPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source) { | |||
| if (sources->lfos.size() <= lfoIndex) { | |||
| return false; | |||
| } | |||
| std::vector<WECore::ModulationSourceWrapper<double>> existingSources = sources->lfos[lfoIndex]->getPhaseModulationSources(); | |||
| for (const auto& existingSource : existingSources) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(existingSource.source); | |||
| if (thisSource != nullptr && thisSource->definition == source) { | |||
| return sources->lfos[lfoIndex]->removePhaseModulationSource(existingSource.source); | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool setLFOPhaseModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int sourceIndex, double val) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->setPhaseModulationAmount(sourceIndex, val); | |||
| } | |||
| return false; | |||
| } | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFOPhaseModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> retVal; | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| for (const auto& source : sources->lfos[lfoIndex]->getPhaseModulationSources()) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(source.source); | |||
| retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); | |||
| } | |||
| } | |||
| return retVal; | |||
| } | |||
| bool getLfoTempoSyncSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getTempoSyncSwitch(); | |||
| } | |||
| return false; | |||
| } | |||
| bool getLfoInvertSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getInvertSwitch(); | |||
| } | |||
| return false; | |||
| } | |||
| int getLfoOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getOutputMode(); | |||
| } | |||
| return 0; | |||
| } | |||
| int getLfoWave(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getWave(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLfoTempoNumer(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getTempoNumer(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLfoTempoDenom(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getTempoDenom(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLfoFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getFreq(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLFOModulatedFreqValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getModulatedFreqValue(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLfoDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getDepth(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLFOModulatedDepthValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getModulatedDepthValue(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLfoManualPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getManualPhase(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getLFOModulatedPhaseValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex) { | |||
| if (sources->lfos.size() > lfoIndex) { | |||
| return sources->lfos[lfoIndex]->getModulatedPhaseValue(); | |||
| } | |||
| return 0; | |||
| } | |||
| // | |||
| // Envelopes | |||
| // | |||
| void addEnvelope(std::shared_ptr<ModelInterface::ModulationSourcesState> sources) { | |||
| std::shared_ptr<ModelInterface::EnvelopeWrapper> newEnv(new ModelInterface::EnvelopeWrapper()); | |||
| newEnv->envelope->setSampleRate(sources->hostConfig.sampleRate); | |||
| sources->envelopes.push_back(newEnv); | |||
| } | |||
| bool setEnvAttackTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, double val) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| sources->envelopes[envIndex]->envelope->setAttackTimeMs(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setEnvReleaseTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, double val) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| sources->envelopes[envIndex]->envelope->setReleaseTimeMs(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setEnvFilterEnabled(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, bool val) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| sources->envelopes[envIndex]->envelope->setFilterEnabled(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setEnvFilterHz(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, double lowCut, double highCut) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| sources->envelopes[envIndex]->envelope->setLowCutHz(lowCut); | |||
| sources->envelopes[envIndex]->envelope->setHighCutHz(highCut); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setEnvAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, float val) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| sources->envelopes[envIndex]->amount = val; | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setEnvUseSidechainInput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, bool val) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| sources->envelopes[envIndex]->useSidechainInput = val; | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| double getEnvAttackTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->envelope->getAttackTimeMs(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getEnvReleaseTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->envelope->getReleaseTimeMs(); | |||
| } | |||
| return 0; | |||
| } | |||
| bool getEnvFilterEnabled(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->envelope->getFilterEnabled(); | |||
| } | |||
| return false; | |||
| } | |||
| double getEnvLowCutHz(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->envelope->getLowCutHz(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getEnvHighCutHz(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->envelope->getHighCutHz(); | |||
| } | |||
| return 0; | |||
| } | |||
| float getEnvAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->amount; | |||
| } | |||
| return 0; | |||
| } | |||
| bool getEnvUseSidechainInput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->useSidechainInput; | |||
| } | |||
| return false; | |||
| } | |||
| double getEnvLastOutput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex) { | |||
| if (sources->envelopes.size() > envIndex) { | |||
| return sources->envelopes[envIndex]->envelope->getLastOutput(); | |||
| } | |||
| return 0; | |||
| } | |||
| // | |||
| // Random | |||
| // | |||
| void addRandom(std::shared_ptr<ModelInterface::ModulationSourcesState> sources) { | |||
| std::shared_ptr<WECore::Perlin::PerlinSource> newRandom(new WECore::Perlin::PerlinSource()); | |||
| newRandom->setSampleRate(sources->hostConfig.sampleRate); | |||
| sources->randomSources.push_back(newRandom); | |||
| } | |||
| bool setRandomOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, int val) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| sources->randomSources[randomIndex]->setOutputMode(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, double val) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| sources->randomSources[randomIndex]->setFreq(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool setRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, double val) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| sources->randomSources[randomIndex]->setDepth(val); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool addSourceToRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source) { | |||
| if (sources->randomSources.size() <= randomIndex) { | |||
| return false; | |||
| } | |||
| auto sourceProvider = std::make_shared<ModulationSourceProvider>(source, sources->getModulationValueCallback); | |||
| return sources->randomSources[randomIndex]->addFreqModulationSource(std::dynamic_pointer_cast<WECore::ModulationSource<double>>(sourceProvider)); | |||
| } | |||
| bool removeSourceFromRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source) { | |||
| if (sources->randomSources.size() <= randomIndex) { | |||
| return false; | |||
| } | |||
| std::vector<WECore::ModulationSourceWrapper<double>> existingSources = sources->randomSources[randomIndex]->getFreqModulationSources(); | |||
| for (const auto& existingSource : existingSources) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(existingSource.source); | |||
| if (thisSource != nullptr && thisSource->definition == source) { | |||
| return sources->randomSources[randomIndex]->removeFreqModulationSource(existingSource.source); | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool setRandomFreqModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, int sourceIndex, double val) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->setFreqModulationAmount(sourceIndex, val); | |||
| } | |||
| return false; | |||
| } | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getRandomFreqModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> retVal; | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| for (const auto& source : sources->randomSources[randomIndex]->getFreqModulationSources()) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(source.source); | |||
| retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); | |||
| } | |||
| } | |||
| return retVal; | |||
| } | |||
| bool addSourceToRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source) { | |||
| if (sources->randomSources.size() <= randomIndex) { | |||
| return false; | |||
| } | |||
| auto sourceProvider = std::make_shared<ModulationSourceProvider>(source, sources->getModulationValueCallback); | |||
| return sources->randomSources[randomIndex]->addDepthModulationSource(std::dynamic_pointer_cast<WECore::ModulationSource<double>>(sourceProvider)); | |||
| } | |||
| bool removeSourceFromRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source) { | |||
| if (sources->randomSources.size() <= randomIndex) { | |||
| return false; | |||
| } | |||
| std::vector<WECore::ModulationSourceWrapper<double>> existingSources = sources->randomSources[randomIndex]->getDepthModulationSources(); | |||
| for (const auto& existingSource : existingSources) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(existingSource.source); | |||
| if (thisSource != nullptr && thisSource->definition == source) { | |||
| return sources->randomSources[randomIndex]->removeDepthModulationSource(existingSource.source); | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool setRandomDepthModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, int sourceIndex, double val) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->setDepthModulationAmount(sourceIndex, val); | |||
| } | |||
| return false; | |||
| } | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getRandomDepthModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> retVal; | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| for (const auto& source : sources->randomSources[randomIndex]->getDepthModulationSources()) { | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(source.source); | |||
| retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); | |||
| } | |||
| } | |||
| return retVal; | |||
| } | |||
| int getRandomOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->getOutputMode(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->getFreq(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getRandomModulatedFreqValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->getModulatedFreqValue(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->getDepth(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getRandomModulatedDepthValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->getModulatedDepthValue(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getRandomLastOutput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex) { | |||
| if (sources->randomSources.size() > randomIndex) { | |||
| return sources->randomSources[randomIndex]->getLastOutput(); | |||
| } | |||
| return 0; | |||
| } | |||
| bool removeModulationSource(ModelInterface::ModulationSourcesState& state, ModulationSourceDefinition definition) { | |||
| // First remove/renumber any modulation sources that reference this one | |||
| for (std::shared_ptr<ModelInterface::CloneableLFO> lfo : state.lfos) { | |||
| // Freq | |||
| std::vector<WECore::ModulationSourceWrapper<double>> lfoFreqSources = lfo->getFreqModulationSources(); | |||
| lfoFreqSources = deleteSourceFromTargetSources(lfoFreqSources, definition, state.getModulationValueCallback); | |||
| lfo->setFreqModulationSources(lfoFreqSources); | |||
| // Depth | |||
| std::vector<WECore::ModulationSourceWrapper<double>> lfoDepthSources = lfo->getDepthModulationSources(); | |||
| lfoDepthSources = deleteSourceFromTargetSources(lfoDepthSources, definition, state.getModulationValueCallback); | |||
| lfo->setDepthModulationSources(lfoDepthSources); | |||
| // Phase | |||
| std::vector<WECore::ModulationSourceWrapper<double>> lfoPhaseSources = lfo->getPhaseModulationSources(); | |||
| lfoPhaseSources = deleteSourceFromTargetSources(lfoPhaseSources, definition, state.getModulationValueCallback); | |||
| lfo->setPhaseModulationSources(lfoPhaseSources); | |||
| } | |||
| for (std::shared_ptr<WECore::Perlin::PerlinSource> random : state.randomSources) { | |||
| // Freq | |||
| std::vector<WECore::ModulationSourceWrapper<double>> randomFreqSources = random->getFreqModulationSources(); | |||
| randomFreqSources = deleteSourceFromTargetSources(randomFreqSources, definition, state.getModulationValueCallback); | |||
| random->setFreqModulationSources(randomFreqSources); | |||
| // Depth | |||
| std::vector<WECore::ModulationSourceWrapper<double>> randomDepthSources = random->getDepthModulationSources(); | |||
| randomDepthSources = deleteSourceFromTargetSources(randomDepthSources, definition, state.getModulationValueCallback); | |||
| random->setDepthModulationSources(randomDepthSources); | |||
| } | |||
| const int index {definition.id - 1}; | |||
| if (definition.type == MODULATION_TYPE::LFO) { | |||
| if (state.lfos.size() > index) { | |||
| state.lfos.erase(state.lfos.begin() + index); | |||
| return true; | |||
| } | |||
| } else if (definition.type == MODULATION_TYPE::ENVELOPE) { | |||
| if (state.envelopes.size() > index) { | |||
| state.envelopes.erase(state.envelopes.begin() + index); | |||
| return true; | |||
| } | |||
| } else if (definition.type == MODULATION_TYPE::RANDOM) { | |||
| if (state.randomSources.size() > index) { | |||
| state.randomSources.erase(state.randomSources.begin() + index); | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| #pragma once | |||
| #include "DataModelInterface.hpp" | |||
| namespace ModulationMutators { | |||
| // LFOs | |||
| void addLfo(std::shared_ptr<ModelInterface::ModulationSourcesState> sources); | |||
| bool setLfoTempoSyncSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, bool val); | |||
| bool setLfoInvertSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, bool val); | |||
| bool setLfoOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val); | |||
| bool setLfoWave(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val); | |||
| bool setLfoTempoNumer(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val); | |||
| bool setLfoTempoDenom(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int val); | |||
| bool setLfoFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, double val); | |||
| bool setLfoDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, double val); | |||
| bool setLfoManualPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, double val); | |||
| bool addSourceToLFOFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source); | |||
| bool removeSourceFromLFOFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source); | |||
| bool setLFOFreqModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFOFreqModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| bool addSourceToLFODepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source); | |||
| bool removeSourceFromLFODepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source); | |||
| bool setLFODepthModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFODepthModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| bool addSourceToLFOPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source); | |||
| bool removeSourceFromLFOPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, ModulationSourceDefinition source); | |||
| bool setLFOPhaseModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFOPhaseModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| bool getLfoTempoSyncSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| bool getLfoInvertSwitch(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| int getLfoOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| int getLfoWave(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLfoTempoNumer(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLfoTempoDenom(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLfoFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLFOModulatedFreqValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLfoDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLFOModulatedDepthValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLfoManualPhase(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| double getLFOModulatedPhaseValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int lfoIndex); | |||
| // Envelopes | |||
| void addEnvelope(std::shared_ptr<ModelInterface::ModulationSourcesState> sources); | |||
| bool setEnvAttackTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, double val); | |||
| bool setEnvReleaseTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, double val); | |||
| bool setEnvFilterEnabled(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, bool val); | |||
| bool setEnvFilterHz(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, double lowCut, double highCut); | |||
| bool setEnvAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, float val); | |||
| bool setEnvUseSidechainInput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex, bool val); | |||
| double getEnvAttackTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| double getEnvReleaseTimeMs(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| bool getEnvFilterEnabled(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| double getEnvLowCutHz(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| double getEnvHighCutHz(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| float getEnvAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| bool getEnvUseSidechainInput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| double getEnvLastOutput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int envIndex); | |||
| // Random | |||
| void addRandom(std::shared_ptr<ModelInterface::ModulationSourcesState> sources); | |||
| bool setRandomOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, int val); | |||
| bool setRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, double val); | |||
| bool setRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, double val); | |||
| bool addSourceToRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source); | |||
| bool removeSourceFromRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source); | |||
| bool setRandomFreqModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getRandomFreqModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| bool addSourceToRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source); | |||
| bool removeSourceFromRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, ModulationSourceDefinition source); | |||
| bool setRandomDepthModulationAmount(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getRandomDepthModulationSources(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| int getRandomOutputMode(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| double getRandomFreq(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| double getRandomModulatedFreqValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| double getRandomDepth(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| double getRandomModulatedDepthValue(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| double getRandomLastOutput(std::shared_ptr<ModelInterface::ModulationSourcesState> sources, int randomIndex); | |||
| bool removeModulationSource(ModelInterface::ModulationSourcesState& state, ModulationSourceDefinition definition); | |||
| } | |||
| @@ -0,0 +1,181 @@ | |||
| #pragma once | |||
| #include "DataModelInterface.hpp" | |||
| namespace ModelInterface { | |||
| bool setSplitType(StateManager& manager, SPLIT_TYPE splitType, HostConfiguration config); | |||
| SPLIT_TYPE getSplitType(StateManager& manager); | |||
| bool replacePlugin(StateManager& manager, std::shared_ptr<juce::AudioPluginInstance> plugin, int chainNumber, int positionInChain); | |||
| bool removeSlot(StateManager& manager, int chainNumber, int positionInChain); | |||
| bool insertGainStage(StateManager& manager, int chainNumber, int positionInChain); | |||
| std::shared_ptr<juce::AudioPluginInstance> getPlugin(StateManager& manager, int chainNumber, int positionInChain); | |||
| bool setGainLinear(StateManager& manager, int chainNumber, int positionInChain, float gain); | |||
| bool setPan(StateManager& manager, int chainNumber, int positionInChain, float pan); | |||
| std::tuple<float, float> getGainLinearAndPan(StateManager& manager, int chainNumber, int positionInChain); | |||
| float getGainStageOutputAmplitude(StateManager& manager, int chainNumber, int positionInChain, int channelNumber); | |||
| PluginModulationConfig getPluginModulationConfig(StateManager& manager, int chainNumber, int positionInChain); | |||
| void setPluginModulationIsActive(StateManager& manager, int chainNumber, int positionInChain, bool val); | |||
| void setModulationTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, juce::String targetName); | |||
| void removeModulationTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber); | |||
| void addModulationSourceToTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, ModulationSourceDefinition source); | |||
| void removeModulationSourceFromTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, ModulationSourceDefinition source); | |||
| void setModulationTargetValue(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, float val); | |||
| void setModulationSourceValue(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, int sourceNumber, float val); | |||
| void setSlotBypass(StateManager& manager, int chainNumber, int positionInChain, bool isBypassed); | |||
| bool getSlotBypass(StateManager& manager, int chainNumber, int positionInChain); | |||
| void setChainBypass(StateManager& manager, int chainNumber, bool val); | |||
| void setChainMute(StateManager& manager, int chainNumber, bool val); | |||
| void setChainSolo(StateManager& manager, int chainNumber, bool val); | |||
| bool getChainBypass(StateManager& manager, int chainNumber); | |||
| bool getChainMute(StateManager& manager, int chainNumber); | |||
| bool getChainSolo(StateManager& manager, int chainNumber); | |||
| void moveSlot(StateManager& manager, int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber); | |||
| void copySlot(StateManager& manager, | |||
| std::function<void()> onSuccess, | |||
| juce::AudioPluginFormatManager& formatManager, | |||
| int fromChainNumber, | |||
| int fromSlotNumber, | |||
| int toChainNumber, | |||
| int toSlotNumber); | |||
| void moveChain(StateManager& manager, int fromChainNumber, int toChainNumber); | |||
| void copyChain(StateManager& manager, | |||
| std::function<void()> onSuccess, | |||
| juce::AudioPluginFormatManager& formatManager, | |||
| int fromChainNumber, | |||
| int toChainNumber); | |||
| size_t getNumChains(StateManager& manager); | |||
| bool addParallelChain(StateManager& manager); | |||
| bool removeParallelChain(StateManager& manager, int chainNumber); | |||
| void addCrossoverBand(StateManager& manager); | |||
| bool removeCrossoverBand(StateManager& manager, int bandNumber); | |||
| bool setCrossoverFrequency(StateManager& manager, size_t index, float val); | |||
| float getCrossoverFrequency(StateManager& manager, size_t index); | |||
| bool setChainCustomName(StateManager& manager, int chainNumber, const juce::String& name); | |||
| juce::String getChainCustomName(StateManager& manager, int chainNumber); | |||
| std::pair<std::array<float, FFTProvider::NUM_OUTPUTS>, float> getFFTOutputs(StateManager& manager); | |||
| std::shared_ptr<PluginEditorBounds> getPluginEditorBounds(StateManager& manager, int chainNumber, int positionInChain); | |||
| void forEachChain(StateManager& manager, std::function<void(int, std::shared_ptr<PluginChain>)> callback); | |||
| void forEachCrossover(StateManager& manager, std::function<void(float)> callback); | |||
| void writeSplitterToXml(StateManager& manager, juce::XmlElement* element); | |||
| void restoreSplitterFromXml( | |||
| StateManager& manager, juce::XmlElement* element, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback, | |||
| HostConfiguration config, | |||
| const PluginConfigurator& pluginConfigurator, | |||
| juce::Array<juce::PluginDescription> availableTypes, | |||
| std::function<void(juce::String)> onErrorCallback); | |||
| void createDefaultSources(StateManager& manager); | |||
| void addLfo(StateManager& manager); | |||
| void addEnvelope(StateManager& manager); | |||
| void addRandom(StateManager& manager); | |||
| void removeModulationSource(StateManager& manager, ModulationSourceDefinition definition); | |||
| void forEachLfo(StateManager& manager, std::function<void(int)> callback); | |||
| void forEachEnvelope(StateManager& manager, std::function<void(int)> callback); | |||
| void forEachRandom(StateManager& manager, std::function<void(int)> callback); | |||
| // LFOs | |||
| void setLfoTempoSyncSwitch(StateManager& manager, int lfoIndex, bool val); | |||
| void setLfoInvertSwitch(StateManager& manager, int lfoIndex, bool val); | |||
| void setLfoOutputMode(StateManager& manager, int lfoIndex, int val); | |||
| void setLfoWave(StateManager& manager, int lfoIndex, int val); | |||
| void setLfoTempoNumer(StateManager& manager, int lfoIndex, int val); | |||
| void setLfoTempoDenom (StateManager& manager, int lfoIndex, int val); | |||
| void setLfoFreq(StateManager& manager, int lfoIndex, double val); | |||
| void setLfoDepth(StateManager& manager, int lfoIndex, double val); | |||
| void setLfoManualPhase(StateManager& manager, int lfoIndex, double val); | |||
| void addSourceToLFOFreq(StateManager& manager, int lfoIndex, ModulationSourceDefinition source); | |||
| void removeSourceFromLFOFreq(StateManager& manager, int lfoIndex, ModulationSourceDefinition source); | |||
| void setLFOFreqModulationAmount(StateManager& manager, int lfoIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFOFreqModulationSources(StateManager& manager, int lfoIndex); | |||
| void addSourceToLFODepth(StateManager& manager, int lfoIndex, ModulationSourceDefinition source); | |||
| void removeSourceFromLFODepth(StateManager& manager, int lfoIndex, ModulationSourceDefinition source); | |||
| void setLFODepthModulationAmount(StateManager& manager, int lfoIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFODepthModulationSources(StateManager& manager, int lfoIndex); | |||
| void addSourceToLFOPhase(StateManager& manager, int lfoIndex, ModulationSourceDefinition source); | |||
| void removeSourceFromLFOPhase(StateManager& manager, int lfoIndex, ModulationSourceDefinition source); | |||
| void setLFOPhaseModulationAmount(StateManager& manager, int lfoIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getLFOPhaseModulationSources(StateManager& manager, int lfoIndex); | |||
| bool getLfoTempoSyncSwitch(StateManager& manager, int lfoIndex); | |||
| bool getLfoInvertSwitch(StateManager& manager, int lfoIndex); | |||
| int getLfoOutputMode(StateManager& manager, int lfoIndex); | |||
| int getLfoWave(StateManager& manager, int lfoIndex); | |||
| double getLfoTempoNumer(StateManager& manager, int lfoIndex); | |||
| double getLfoTempoDenom(StateManager& manager, int lfoIndex); | |||
| double getLfoFreq(StateManager& manager, int lfoIndex); | |||
| double getLFOModulatedFreqValue(StateManager& manager, int lfoIndex); | |||
| double getLfoDepth(StateManager& manager, int lfoIndex); | |||
| double getLFOModulatedDepthValue(StateManager& manager, int lfoIndex); | |||
| double getLfoManualPhase(StateManager& manager, int lfoIndex); | |||
| double getLFOModulatedPhaseValue(StateManager& manager, int lfoIndex); | |||
| // Envelopes | |||
| void setEnvAttackTimeMs(StateManager& manager, int envIndex, double val); | |||
| void setEnvReleaseTimeMs(StateManager& manager, int envIndex, double val); | |||
| void setEnvFilterEnabled(StateManager& manager, int envIndex, bool val); | |||
| void setEnvFilterHz(StateManager& manager, int envIndex, double lowCut, double highCut); | |||
| void setEnvAmount(StateManager& manager, int envIndex, float val); | |||
| void setEnvUseSidechainInput(StateManager& manager, int envIndex, bool val); | |||
| double getEnvAttackTimeMs(StateManager& manager, int envIndex); | |||
| double getEnvReleaseTimeMs(StateManager& manager, int envIndex); | |||
| bool getEnvFilterEnabled(StateManager& manager, int envIndex); | |||
| double getEnvLowCutHz(StateManager& manager, int envIndex); | |||
| double getEnvHighCutHz(StateManager& manager, int envIndex); | |||
| float getEnvAmount(StateManager& manager, int envIndex); | |||
| bool getEnvUseSidechainInput(StateManager& manager, int envIndex); | |||
| double getEnvLastOutput(StateManager& manager, int envIndex); | |||
| // Random | |||
| void setRandomOutputMode(StateManager& manager, int randomIndex, int val); | |||
| void setRandomFreq(StateManager& manager, int randomIndex, double val); | |||
| void setRandomDepth(StateManager& manager, int randomIndex, double val); | |||
| void addSourceToRandomFreq(StateManager& manager, int randomIndex, ModulationSourceDefinition source); | |||
| void removeSourceFromRandomFreq(StateManager& manager, int randomIndex, ModulationSourceDefinition source); | |||
| void setRandomFreqModulationAmount(StateManager& manager, int randomIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getRandomFreqModulationSources(StateManager& manager, int randomIndex); | |||
| void addSourceToRandomDepth(StateManager& manager, int randomIndex, ModulationSourceDefinition source); | |||
| void removeSourceFromRandomDepth(StateManager& manager, int randomIndex, ModulationSourceDefinition source); | |||
| void setRandomDepthModulationAmount(StateManager& manager, int randomIndex, int sourceIndex, double val); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> getRandomDepthModulationSources(StateManager& manager, int randomIndex); | |||
| int getRandomOutputMode(StateManager& manager, int randomIndex); | |||
| double getRandomFreq(StateManager& manager, int randomIndex); | |||
| double getRandomModulatedFreqValue(StateManager& manager, int randomIndex); | |||
| double getRandomDepth(StateManager& manager, int randomIndex); | |||
| double getRandomModulatedDepthValue(StateManager& manager, int randomIndex); | |||
| double getRandomLastOutput(StateManager& manager, int randomIndex); | |||
| void resetAllState(StateManager& manager, | |||
| HostConfiguration config, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback); | |||
| void writeSourcesToXml(StateManager& manager, juce::XmlElement* element); | |||
| void restoreSourcesFromXml(StateManager& manager, juce::XmlElement* element, HostConfiguration config); | |||
| void undo(StateManager& manager, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout); | |||
| void redo(StateManager& manager, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout); | |||
| std::optional<juce::String> getUndoOperation(const StateManager& manager); | |||
| std::optional<juce::String> getRedoOperation(const StateManager& manager); | |||
| } | |||
| @@ -0,0 +1,474 @@ | |||
| #include "SplitterMutators.hpp" | |||
| #include "ChainMutators.hpp" | |||
| #include "ChainProcessors.hpp" | |||
| #include "MONSTRFilters/MONSTRParameters.h" | |||
| namespace { | |||
| void copyNextSlot(std::shared_ptr<PluginSplitter> splitter, std::function<void()> onSuccess, juce::AudioPluginFormatManager& formatManager, int fromChainNumber, int toChainNumber, int slotIndex) { | |||
| const int numSlotsToCopy = splitter->chains[fromChainNumber].chain->chain.size(); | |||
| if (slotIndex >= numSlotsToCopy) { | |||
| // We're done | |||
| onSuccess(); | |||
| return; | |||
| } | |||
| auto next = [splitter, onSuccess, &formatManager, fromChainNumber, toChainNumber, slotIndex]() { | |||
| copyNextSlot(splitter, onSuccess, formatManager, fromChainNumber, toChainNumber, slotIndex + 1); | |||
| }; | |||
| auto insertThisPlugin = [splitter, toChainNumber, slotIndex, next](std::shared_ptr<juce::AudioPluginInstance> sharedPlugin, juce::MemoryBlock sourceState, bool isBypassed, PluginModulationConfig sourceConfig) { | |||
| // Hand the plugin over to the splitter | |||
| if (SplitterMutators::insertPlugin(splitter, sharedPlugin, toChainNumber, slotIndex)) { | |||
| // Apply plugin state | |||
| sharedPlugin->setStateInformation(sourceState.getData(), sourceState.getSize()); | |||
| // Apply bypass | |||
| SplitterMutators::setSlotBypass(splitter, toChainNumber, slotIndex, isBypassed); | |||
| // Apply modulation | |||
| SplitterMutators::setPluginModulationConfig(splitter, sourceConfig, toChainNumber, slotIndex); | |||
| } else { | |||
| juce::Logger::writeToLog("SyndicateAudioProcessor::copySlot: Failed to insert plugin"); | |||
| } | |||
| next(); | |||
| }; | |||
| SplitterMutators::copySlot(splitter, insertThisPlugin, next, formatManager, fromChainNumber, slotIndex, toChainNumber, slotIndex); | |||
| } | |||
| } | |||
| namespace SplitterMutators { | |||
| bool insertPlugin(std::shared_ptr<PluginSplitter> splitter, std::shared_ptr<juce::AudioPluginInstance> plugin, int chainNumber, int positionInChain) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| ChainMutators::insertPlugin(splitter->chains[chainNumber].chain, plugin, positionInChain, splitter->config); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool replacePlugin(std::shared_ptr<PluginSplitter> splitter, std::shared_ptr<juce::AudioPluginInstance> plugin, int chainNumber, int positionInChain) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| ChainMutators::replacePlugin(splitter->chains[chainNumber].chain, plugin, positionInChain, splitter->config); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| bool removeSlot(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| return ChainMutators::removeSlot(splitter->chains[chainNumber].chain, positionInChain); | |||
| } | |||
| return false; | |||
| } | |||
| bool insertGainStage(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| ChainMutators::insertGainStage(splitter->chains[chainNumber].chain, positionInChain, splitter->config); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| std::shared_ptr<juce::AudioPluginInstance> getPlugin(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| return ChainMutators::getPlugin(splitter->chains[chainNumber].chain, positionInChain); | |||
| } | |||
| return nullptr; | |||
| } | |||
| bool setPluginModulationConfig(std::shared_ptr<PluginSplitter> splitter, PluginModulationConfig config, int chainNumber, int positionInChain) { | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return ChainMutators::setPluginModulationConfig(splitter->chains[chainNumber].chain, config, positionInChain); | |||
| } | |||
| return false; | |||
| } | |||
| PluginModulationConfig getPluginModulationConfig(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| return ChainMutators::getPluginModulationConfig(splitter->chains[chainNumber].chain, positionInChain); | |||
| } | |||
| return PluginModulationConfig(); | |||
| } | |||
| bool setSlotBypass(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, bool isBypassed) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| return ChainMutators::setSlotBypass(splitter->chains[chainNumber].chain, positionInChain, isBypassed); | |||
| } | |||
| return false; | |||
| } | |||
| bool getSlotBypass(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (splitter->chains.size() > chainNumber) { | |||
| return ChainMutators::getSlotBypass(splitter->chains[chainNumber].chain, positionInChain); | |||
| } | |||
| return false; | |||
| } | |||
| bool setChainSolo(std::shared_ptr<PluginSplitter> splitter, int chainNumber, bool val) { | |||
| // The multiband crossover can handle soloed bands, so let it do that first | |||
| if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| CrossoverMutators::setIsSoloed(multibandSplitter->crossover, chainNumber, val); | |||
| } | |||
| if (chainNumber < splitter->chains.size()) { | |||
| // If the new value is different to the existing one, update it and the counter | |||
| if (val != splitter->chains[chainNumber].isSoloed) { | |||
| splitter->chains[chainNumber].isSoloed = val; | |||
| if (val) { | |||
| splitter->numChainsSoloed++; | |||
| } else { | |||
| splitter->numChainsSoloed--; | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool getChainSolo(std::shared_ptr<PluginSplitter> splitter, int chainNumber) { | |||
| if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| return CrossoverMutators::getIsSoloed(multibandSplitter->crossover, chainNumber); | |||
| } | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return splitter->chains[chainNumber].isSoloed; | |||
| } | |||
| return false; | |||
| } | |||
| bool moveSlot(std::shared_ptr<PluginSplitter> splitter, int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber) { | |||
| // Copy everything we need | |||
| std::shared_ptr<juce::AudioPluginInstance> plugin = | |||
| SplitterMutators::getPlugin(splitter, fromChainNumber, fromSlotNumber); | |||
| if (fromChainNumber == toChainNumber && fromSlotNumber < toSlotNumber) { | |||
| // Decrement the target position if the original we removed was before it in the same chain | |||
| toSlotNumber--; | |||
| } | |||
| if (plugin != nullptr) { | |||
| // This is a plugin | |||
| const bool isBypassed {SplitterMutators::getSlotBypass(splitter, fromChainNumber, fromSlotNumber)}; | |||
| PluginModulationConfig config = | |||
| SplitterMutators::getPluginModulationConfig(splitter, fromChainNumber, fromSlotNumber); | |||
| // Remove it from the chain | |||
| if (SplitterMutators::removeSlot(splitter, fromChainNumber, fromSlotNumber)) { | |||
| // Add it in the new position | |||
| SplitterMutators::insertPlugin(splitter, plugin, toChainNumber, toSlotNumber); | |||
| SplitterMutators::setSlotBypass(splitter, toChainNumber, toSlotNumber, isBypassed); | |||
| SplitterMutators::setPluginModulationConfig(splitter, config, toChainNumber, toSlotNumber); | |||
| return true; | |||
| } | |||
| } else { | |||
| // This is a gain stage | |||
| const float gain {SplitterMutators::getGainLinear(splitter, fromChainNumber, fromSlotNumber)}; | |||
| const float pan {SplitterMutators::getPan(splitter, fromChainNumber, fromSlotNumber)}; | |||
| // Remove it from the chain | |||
| if (SplitterMutators::removeSlot(splitter, fromChainNumber, fromSlotNumber)) { | |||
| // Add it in the new position | |||
| SplitterMutators::insertGainStage(splitter, toChainNumber, toSlotNumber); | |||
| SplitterMutators::setGainLinear(splitter, toChainNumber, toSlotNumber, gain); | |||
| SplitterMutators::setPan(splitter, toChainNumber, toSlotNumber, pan); | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| void copySlot(std::shared_ptr<PluginSplitter> splitter, | |||
| std::function<void(std::shared_ptr<juce::AudioPluginInstance> sharedPlugin, juce::MemoryBlock sourceState, bool isBypassed, PluginModulationConfig sourceConfig)> insertPlugin, | |||
| std::function<void()> onSuccess, | |||
| juce::AudioPluginFormatManager& formatManager, | |||
| int fromChainNumber, | |||
| int fromSlotNumber, | |||
| int toChainNumber, | |||
| int toSlotNumber) { | |||
| std::shared_ptr<juce::AudioPluginInstance> sourcePlugin = | |||
| SplitterMutators::getPlugin(splitter, fromChainNumber, fromSlotNumber); | |||
| if (sourcePlugin != nullptr) { | |||
| // This is a plugin | |||
| // Get the state and config before making changes that might change the plugin's position | |||
| juce::MemoryBlock sourceState; | |||
| sourcePlugin->getStateInformation(sourceState); | |||
| const bool isBypassed {SplitterMutators::getSlotBypass(splitter, fromChainNumber, fromSlotNumber)}; | |||
| PluginModulationConfig sourceConfig = | |||
| SplitterMutators::getPluginModulationConfig(splitter, fromChainNumber, fromSlotNumber); | |||
| // Create the callback | |||
| // Be careful about what is used in this callback - anything in local scope needs to be captured by value | |||
| auto onPluginCreated = [splitter, insertPlugin, sourceState, isBypassed, sourceConfig, toChainNumber, toSlotNumber](std::unique_ptr<juce::AudioPluginInstance> plugin, const juce::String& error) { | |||
| if (plugin != nullptr) { | |||
| // Create the shared pointer here as we need it for the window | |||
| std::shared_ptr<juce::AudioPluginInstance> sharedPlugin = std::move(plugin); | |||
| PluginConfigurator pluginConfigurator; | |||
| if (pluginConfigurator.configure(sharedPlugin, splitter->config)) { | |||
| insertPlugin(sharedPlugin, sourceState, isBypassed, sourceConfig); | |||
| } else { | |||
| juce::Logger::writeToLog("SyndicateAudioProcessor::copySlot: Failed to configure plugin"); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("SyndicateAudioProcessor::copySlot: Failed to load plugin: " + error); | |||
| } | |||
| }; | |||
| // Try to load the plugin | |||
| formatManager.createPluginInstanceAsync( | |||
| sourcePlugin->getPluginDescription(), | |||
| splitter->config.sampleRate, | |||
| splitter->config.blockSize, | |||
| onPluginCreated); | |||
| } else { | |||
| // This is a gain stage | |||
| const float gain {SplitterMutators::getGainLinear(splitter, fromChainNumber, fromSlotNumber)}; | |||
| const float pan {SplitterMutators::getPan(splitter, fromChainNumber, fromSlotNumber)}; | |||
| // Add it in the new position | |||
| if (SplitterMutators::insertGainStage(splitter, toChainNumber, toSlotNumber)) { | |||
| SplitterMutators::setGainLinear(splitter, toChainNumber, toSlotNumber, gain); | |||
| SplitterMutators::setPan(splitter, toChainNumber, toSlotNumber, pan); | |||
| onSuccess(); | |||
| } | |||
| } | |||
| } | |||
| bool moveChain(std::shared_ptr<PluginSplitter> splitter, int fromChainNumber, int toChainNumber) { | |||
| if (fromChainNumber >= splitter->chains.size()) { | |||
| return false; | |||
| } | |||
| // Create a copy of the chain and remove the original | |||
| std::shared_ptr<PluginChain> chainToMove = splitter->chains[fromChainNumber].chain; | |||
| const bool isSoloed = splitter->chains[fromChainNumber].isSoloed; | |||
| splitter->chains.erase(splitter->chains.begin() + fromChainNumber); | |||
| if (toChainNumber > splitter->chains.size()) { | |||
| // Insert at the end | |||
| toChainNumber = splitter->chains.size(); | |||
| } else if (fromChainNumber < toChainNumber) { | |||
| // Decrement the target position if the original we removed was before it | |||
| toChainNumber--; | |||
| } | |||
| // Insert the copy at the new position | |||
| splitter->chains.emplace(splitter->chains.begin() + toChainNumber, chainToMove, isSoloed); | |||
| if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| // Update the crossover | |||
| for (int chainIndex {0}; chainIndex < splitter->chains.size(); chainIndex++) { | |||
| CrossoverMutators::setPluginChain(multibandSplitter->crossover, chainIndex, splitter->chains[chainIndex].chain); | |||
| CrossoverMutators::setIsSoloed(multibandSplitter->crossover, chainIndex, splitter->chains[chainIndex].isSoloed); | |||
| } | |||
| } | |||
| return true; | |||
| } | |||
| void copyChain(std::shared_ptr<PluginSplitter> splitter, std::function<void()> onSuccess, juce::AudioPluginFormatManager& formatManager, int fromChainNumber, int toChainNumber) { | |||
| if (fromChainNumber >= splitter->chains.size()) { | |||
| return; | |||
| } | |||
| // Create a copy of the chain and insert it at the new position | |||
| std::shared_ptr<PluginChain> chainToCopy = splitter->chains[fromChainNumber].chain; | |||
| const bool isSoloed = splitter->chains[fromChainNumber].isSoloed; | |||
| if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| // Add a new band | |||
| addBand(multibandSplitter); | |||
| } else { | |||
| // Add a new chain | |||
| addChain(splitter); | |||
| } | |||
| moveChain(splitter, splitter->chains.size() - 1, toChainNumber); | |||
| if (toChainNumber <= fromChainNumber) { | |||
| // When we use fromChainNumber later, we need to account for the original chain having | |||
| // been moved by inserting a chain below it | |||
| fromChainNumber++; | |||
| } | |||
| splitter->chains[toChainNumber].chain->isChainBypassed = chainToCopy->isChainBypassed; | |||
| splitter->chains[toChainNumber].chain->isChainMuted = chainToCopy->isChainMuted; | |||
| splitter->chains[toChainNumber].chain->customName = chainToCopy->customName; | |||
| splitter->chains[toChainNumber].isSoloed = isSoloed; | |||
| copyNextSlot(splitter, onSuccess, formatManager, fromChainNumber, toChainNumber, 0); | |||
| } | |||
| bool setGainLinear(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, float gain) { | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return ChainMutators::setGainLinear(splitter->chains[chainNumber].chain, positionInChain, gain); | |||
| } | |||
| return false; | |||
| } | |||
| float getGainLinear(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return ChainMutators::getGainLinear(splitter->chains[chainNumber].chain, positionInChain); | |||
| } | |||
| return 0.0f; | |||
| } | |||
| float getGainStageOutputAmplitude(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, int channelNumber) { | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return ChainMutators::getGainStageOutputAmplitude(splitter->chains[chainNumber].chain, positionInChain, channelNumber); | |||
| } | |||
| return 0.0f; | |||
| } | |||
| bool setPan(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, float pan) { | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return ChainMutators::setPan(splitter->chains[chainNumber].chain, positionInChain, pan); | |||
| } | |||
| return false; | |||
| } | |||
| float getPan(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return ChainMutators::getPan(splitter->chains[chainNumber].chain, positionInChain); | |||
| } | |||
| return 0.0f; | |||
| } | |||
| std::shared_ptr<PluginEditorBounds> getPluginEditorBounds(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain) { | |||
| if (chainNumber < splitter->chains.size()) { | |||
| return ChainMutators::getPluginEditorBounds(splitter->chains[chainNumber].chain, positionInChain); | |||
| } | |||
| return std::make_shared<PluginEditorBounds>(); | |||
| } | |||
| SPLIT_TYPE getSplitType(const std::shared_ptr<PluginSplitter> splitter) { | |||
| if (std::dynamic_pointer_cast<PluginSplitterSeries>(splitter)) { | |||
| return SPLIT_TYPE::SERIES; | |||
| } | |||
| if (std::dynamic_pointer_cast<PluginSplitterParallel>(splitter)) { | |||
| return SPLIT_TYPE::PARALLEL; | |||
| } | |||
| if (std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| return SPLIT_TYPE::MULTIBAND; | |||
| } | |||
| if (std::dynamic_pointer_cast<PluginSplitterLeftRight>(splitter)) { | |||
| return SPLIT_TYPE::LEFTRIGHT; | |||
| } | |||
| if (std::dynamic_pointer_cast<PluginSplitterMidSide>(splitter)) { | |||
| return SPLIT_TYPE::MIDSIDE; | |||
| } | |||
| return SPLIT_TYPE::SERIES; | |||
| } | |||
| void addChain(std::shared_ptr<PluginSplitter> splitter) { | |||
| splitter->chains.emplace_back(std::make_shared<PluginChain>(splitter->getModulationValueCallback), false); | |||
| ChainProcessors::prepareToPlay(*(splitter->chains[splitter->chains.size() - 1].chain.get()), splitter->config); | |||
| splitter->chains[splitter->chains.size() - 1].chain->latencyListener.setSplitter(splitter.get()); | |||
| splitter->onLatencyChange(); | |||
| } | |||
| bool removeChain(std::shared_ptr<PluginSplitterParallel> splitter, int chainNumber) { | |||
| if (splitter->chains.size() > 1 && chainNumber < splitter->chains.size()) { | |||
| splitter->chains[chainNumber].chain->latencyListener.removeSplitter(); | |||
| splitter->chains.erase(splitter->chains.begin() + chainNumber); | |||
| splitter->onLatencyChange(); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| void addBand(std::shared_ptr<PluginSplitterMultiband> splitter) { | |||
| // Create the chain first, then add the band and set the processor | |||
| splitter->chains.emplace_back(std::make_unique<PluginChain>(splitter->getModulationValueCallback), false); | |||
| CrossoverMutators::addBand(splitter->crossover); | |||
| std::shared_ptr<PluginChain> newChain {splitter->chains[splitter->chains.size() - 1].chain}; | |||
| ChainProcessors::prepareToPlay(*newChain, splitter->config); | |||
| CrossoverMutators::setPluginChain(splitter->crossover, CrossoverMutators::getNumBands(splitter->crossover) - 1, newChain); | |||
| newChain->latencyListener.setSplitter(splitter.get()); | |||
| splitter->onLatencyChange(); | |||
| } | |||
| size_t getNumBands(std::shared_ptr<PluginSplitterMultiband> splitter) { | |||
| return CrossoverMutators::getNumBands(splitter->crossover); | |||
| } | |||
| bool removeBand(std::shared_ptr<PluginSplitterMultiband> splitter, int bandNumber) { | |||
| if (CrossoverMutators::getNumBands(splitter->crossover) > WECore::MONSTR::Parameters::NUM_BANDS.minValue && | |||
| CrossoverMutators::getNumBands(splitter->crossover) > bandNumber) { | |||
| // Remove the band first, then the chain | |||
| if (CrossoverMutators::removeBand(splitter->crossover, bandNumber)) { | |||
| splitter->chains[bandNumber].chain->latencyListener.removeSplitter(); | |||
| splitter->chains.erase(splitter->chains.begin() + bandNumber); | |||
| splitter->onLatencyChange(); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| return false; | |||
| } | |||
| bool setCrossoverFrequency(std::shared_ptr<PluginSplitterMultiband> splitter, size_t index, double val) { | |||
| return CrossoverMutators::setCrossoverFrequency(splitter->crossover, index, val); | |||
| } | |||
| double getCrossoverFrequency(std::shared_ptr<PluginSplitterMultiband> splitter, size_t index) { | |||
| return CrossoverMutators::getCrossoverFrequency(splitter->crossover, index); | |||
| } | |||
| bool setChainCustomName(std::shared_ptr<PluginSplitter> splitter, int chainNumber, const juce::String& name) { | |||
| if (chainNumber >= splitter->chains.size()) { | |||
| return false; | |||
| } | |||
| splitter->chains[chainNumber].chain->customName = name; | |||
| return true; | |||
| } | |||
| juce::String getChainCustomName(std::shared_ptr<PluginSplitter> splitter, int chainNumber) { | |||
| if (chainNumber >= splitter->chains.size()) { | |||
| return juce::String(); | |||
| } | |||
| return splitter->chains[chainNumber].chain->customName; | |||
| } | |||
| } | |||
| @@ -0,0 +1,64 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginSplitter.hpp" | |||
| #include "SplitTypes.hpp" | |||
| namespace SplitterMutators { | |||
| // PluginSplitter | |||
| bool insertPlugin(std::shared_ptr<PluginSplitter> splitter, std::shared_ptr<juce::AudioPluginInstance> plugin, int chainNumber, int positionInChain); | |||
| bool replacePlugin(std::shared_ptr<PluginSplitter> splitter, std::shared_ptr<juce::AudioPluginInstance> plugin, int chainNumber, int positionInChain); | |||
| bool removeSlot(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| bool insertGainStage(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| std::shared_ptr<juce::AudioPluginInstance> getPlugin(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| bool setPluginModulationConfig(std::shared_ptr<PluginSplitter> splitter, PluginModulationConfig config, int chainNumber, int positionInChain); | |||
| PluginModulationConfig getPluginModulationConfig(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| inline size_t getNumChains(std::shared_ptr<PluginSplitter> splitter) { return splitter->chains.size(); } | |||
| bool setSlotBypass(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, bool isBypassed); | |||
| bool getSlotBypass(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| bool setChainSolo(std::shared_ptr<PluginSplitter> splitter, int chainNumber, bool val); | |||
| bool getChainSolo(std::shared_ptr<PluginSplitter> splitter, int chainNumber); | |||
| bool moveSlot(std::shared_ptr<PluginSplitter> splitter, int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber); | |||
| void copySlot(std::shared_ptr<PluginSplitter> splitter, | |||
| std::function<void(std::shared_ptr<juce::AudioPluginInstance> sharedPlugin, juce::MemoryBlock sourceState, bool isBypassed, PluginModulationConfig sourceConfig)> insertPlugin, | |||
| std::function<void()> onSuccess, | |||
| juce::AudioPluginFormatManager& formatManager, | |||
| int fromChainNumber, | |||
| int fromSlotNumber, | |||
| int toChainNumber, | |||
| int toSlotNumber); | |||
| bool moveChain(std::shared_ptr<PluginSplitter> splitter, int fromChainNumber, int toChainNumber); | |||
| void copyChain(std::shared_ptr<PluginSplitter> splitter, std::function<void()> onSuccess, juce::AudioPluginFormatManager& formatManager, int fromChainNumber, int toChainNumber); | |||
| bool setGainLinear(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, float gain); | |||
| float getGainLinear(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| float getGainStageOutputAmplitude(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, int channelNumber); | |||
| bool setPan(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain, float pan); | |||
| float getPan(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| std::shared_ptr<PluginEditorBounds> getPluginEditorBounds(std::shared_ptr<PluginSplitter> splitter, int chainNumber, int positionInChain); | |||
| SPLIT_TYPE getSplitType(const std::shared_ptr<PluginSplitter> splitter); | |||
| // PluginSplitterParallel | |||
| void addChain(std::shared_ptr<PluginSplitter> splitter); | |||
| bool removeChain(std::shared_ptr<PluginSplitterParallel> splitter, int chainNumber); | |||
| // PluginSplitterMultiband | |||
| void addBand(std::shared_ptr<PluginSplitterMultiband> splitter); | |||
| bool removeBand(std::shared_ptr<PluginSplitterMultiband> splitter, int bandNumber); | |||
| size_t getNumBands(std::shared_ptr<PluginSplitterMultiband> splitter); | |||
| bool setCrossoverFrequency(std::shared_ptr<PluginSplitterMultiband> splitter, size_t index, double val); | |||
| double getCrossoverFrequency(std::shared_ptr<PluginSplitterMultiband> splitter, size_t index); | |||
| bool setChainCustomName(std::shared_ptr<PluginSplitter> splitter, int chainNumber, const juce::String& name); | |||
| juce::String getChainCustomName(std::shared_ptr<PluginSplitter> splitter, int chainNumber); | |||
| } | |||
| @@ -0,0 +1,159 @@ | |||
| #pragma once | |||
| #include "SplitTypes.hpp" | |||
| inline const char* XML_SLOT_TYPE_STR {"SlotType"}; | |||
| inline const char* XML_SLOT_TYPE_PLUGIN_STR {"Plugin"}; | |||
| inline const char* XML_SLOT_TYPE_GAIN_STAGE_STR {"GainStage"}; | |||
| inline const char* XML_SLOT_IS_BYPASSED_STR {"isSlotBypassed"}; | |||
| inline const char* XML_GAIN_STAGE_GAIN_STR {"Gain"}; | |||
| inline const char* XML_GAIN_STAGE_PAN_STR {"Pan"}; | |||
| inline const char* XML_PLUGIN_DATA_STR {"PluginData"}; | |||
| inline const char* XML_MODULATION_CONFIG_STR {"ModulationConfig"}; | |||
| inline const char* XML_MODULATION_IS_ACTIVE_STR {"ModulationIsActive"}; | |||
| inline const char* XML_MODULATION_TARGET_PARAMETER_NAME_STR {"TargetParameterName"}; | |||
| inline const char* XML_MODULATION_REST_VALUE_STR {"RestValue"}; | |||
| inline const char* XML_MODULATION_SOURCE_AMOUNT {"SourceAmount"}; | |||
| inline const char* XML_PLUGIN_EDITOR_BOUNDS_STR {"PluginEditorBounds"}; | |||
| inline const char* XML_DISPLAY_AREA_STR {"DisplayArea"}; | |||
| inline const char* XML_MODULATION_SOURCE_ID {"SourceId"}; | |||
| inline const char* XML_MODULATION_SOURCE_TYPE {"SourceType"}; | |||
| inline const char* XML_IS_CHAIN_BYPASSED_STR {"isChainBypassed"}; | |||
| inline const char* XML_IS_CHAIN_MUTED_STR {"isChainMuted"}; | |||
| inline const char* XML_CHAIN_CUSTOM_NAME_STR {"ChainCustomName"}; | |||
| inline const char* XML_PLUGINS_STR {"Plugins"}; | |||
| inline const char* XML_CHAINS_STR {"Chains"}; | |||
| inline const char* XML_IS_CHAIN_SOLOED_STR {"isSoloed"}; | |||
| inline const char* XML_SPLITTER_STR {"Splitter"}; | |||
| inline const char* XML_SPLIT_TYPE_STR {"SplitType"}; | |||
| inline const char* XML_SPLIT_TYPE_SERIES_STR {"series"}; | |||
| inline const char* XML_SPLIT_TYPE_PARALLEL_STR {"parallel"}; | |||
| inline const char* XML_SPLIT_TYPE_MULTIBAND_STR {"multiband"}; | |||
| inline const char* XML_SPLIT_TYPE_LEFTRIGHT_STR {"leftright"}; | |||
| inline const char* XML_SPLIT_TYPE_MIDSIDE_STR {"midside"}; | |||
| inline const char* XML_CROSSOVERS_STR {"Crossovers"}; | |||
| inline const char* XML_CACHED_CROSSOVER_FREQUENCIES_STR {"CrossoverFrequencies"}; | |||
| inline const char* XML_LFOS_STR {"LFOs"}; | |||
| inline const char* XML_LFO_BYPASS_STR {"lfoBypass"}; | |||
| inline const char* XML_LFO_PHASE_SYNC_STR {"lfoPhaseSync"}; | |||
| inline const char* XML_LFO_TEMPO_SYNC_STR {"lfoTempoSync"}; | |||
| inline const char* XML_LFO_INVERT_STR {"lfoInvert"}; | |||
| inline const char* XML_LFO_OUTPUT_MODE_STR {"lfoOutputMode"}; | |||
| inline const char* XML_LFO_WAVE_STR {"lfoWave"}; | |||
| inline const char* XML_LFO_TEMPO_NUMER_STR {"lfoTempoNumer"}; | |||
| inline const char* XML_LFO_TEMPO_DENOM_STR {"lfoTempoDenom"}; | |||
| inline const char* XML_LFO_FREQ_STR {"lfoFreq"}; | |||
| inline const char* XML_LFO_DEPTH_STR {"lfoDepth"}; | |||
| inline const char* XML_LFO_MANUAL_PHASE_STR {"lfoManualPhase"}; | |||
| inline const char* XML_LFO_FREQ_MODULATION_SOURCES_STR {"lfoFreqModulationSources"}; | |||
| inline const char* XML_LFO_DEPTH_MODULATION_SOURCES_STR {"lfoDepthModulationSources"}; | |||
| inline const char* XML_LFO_PHASE_MODULATION_SOURCES_STR {"lfoPhaseModulationSources"}; | |||
| inline const char* XML_ENVELOPES_STR {"Envelopes"}; | |||
| inline const char* XML_ENV_ATTACK_TIME_STR {"envelopeAttack"}; | |||
| inline const char* XML_ENV_RELEASE_TIME_STR {"envelopeRelease"}; | |||
| inline const char* XML_ENV_FILTER_ENABLED_STR {"envelopeFilterEnabled"}; | |||
| inline const char* XML_ENV_LOW_CUT_STR {"envelopeLowCut"}; | |||
| inline const char* XML_ENV_HIGH_CUT_STR {"envelopeHighCut"}; | |||
| inline const char* XML_ENV_AMOUNT_STR {"envelopeAmount"}; | |||
| inline const char* XML_ENV_USE_SIDECHAIN_INPUT_STR {"envelopeUseSidechainInput"}; | |||
| inline const char* XML_RANDOMS_STR {"Randoms"}; | |||
| inline const char* XML_RANDOM_OUTPUT_MODE_STR {"randomOutputMode"}; | |||
| inline const char* XML_RANDOM_FREQ_STR {"randomFreq"}; | |||
| inline const char* XML_RANDOM_DEPTH_STR {"randomDepth"}; | |||
| inline const char* XML_RANDOM_FREQ_MODULATION_SOURCES_STR {"randomFreqModulationSources"}; | |||
| inline const char* XML_RANDOM_DEPTH_MODULATION_SOURCES_STR {"randomDepthModulationSources"}; | |||
| inline std::string getParameterModulationConfigXmlName(int configNumber) { | |||
| std::string retVal("ParamConfig_"); | |||
| retVal += std::to_string(configNumber); | |||
| return retVal; | |||
| } | |||
| inline std::string getParameterModulationSourceXmlName(int sourceNumber) { | |||
| std::string retVal("Source_"); | |||
| retVal += std::to_string(sourceNumber); | |||
| return retVal; | |||
| } | |||
| inline std::string getSlotXMLName(int pluginNumber) { | |||
| std::string retVal("Slot_"); | |||
| retVal += std::to_string(pluginNumber); | |||
| return retVal; | |||
| } | |||
| inline std::string getChainXMLName(int chainNumber) { | |||
| std::string retVal("Chain_"); | |||
| retVal += std::to_string(chainNumber); | |||
| return retVal; | |||
| } | |||
| inline const char* splitTypeToString(SPLIT_TYPE splitType) { | |||
| switch (splitType) { | |||
| case SPLIT_TYPE::SERIES: | |||
| return XML_SPLIT_TYPE_SERIES_STR; | |||
| case SPLIT_TYPE::PARALLEL: | |||
| return XML_SPLIT_TYPE_PARALLEL_STR; | |||
| case SPLIT_TYPE::MULTIBAND: | |||
| return XML_SPLIT_TYPE_MULTIBAND_STR; | |||
| case SPLIT_TYPE::LEFTRIGHT: | |||
| return XML_SPLIT_TYPE_LEFTRIGHT_STR; | |||
| case SPLIT_TYPE::MIDSIDE: | |||
| return XML_SPLIT_TYPE_MIDSIDE_STR; | |||
| } | |||
| } | |||
| inline SPLIT_TYPE stringToSplitType(juce::String splitTypeString) { | |||
| SPLIT_TYPE retVal {SPLIT_TYPE::SERIES}; | |||
| if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { | |||
| retVal = SPLIT_TYPE::PARALLEL; | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { | |||
| retVal = SPLIT_TYPE::MULTIBAND; | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR) { | |||
| retVal = SPLIT_TYPE::LEFTRIGHT; | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { | |||
| retVal = SPLIT_TYPE::MIDSIDE; | |||
| } | |||
| return retVal; | |||
| } | |||
| inline juce::String getCrossoverXMLName(int crossoverNumber) { | |||
| juce::String retVal("Crossover_"); | |||
| retVal += juce::String(crossoverNumber); | |||
| return retVal; | |||
| } | |||
| inline juce::String getCachedCrossoverFreqXMLName(int crossoverNumber) { | |||
| juce::String retVal("Crossover_"); | |||
| retVal += juce::String(crossoverNumber); | |||
| return retVal; | |||
| } | |||
| inline std::string getLfoXMLName(int lfoNumber) { | |||
| std::string retVal("LFO_"); | |||
| retVal += std::to_string(lfoNumber); | |||
| return retVal; | |||
| } | |||
| inline std::string getEnvelopeXMLName(int envelopeNumber) { | |||
| std::string retVal("Envelope_"); | |||
| retVal += std::to_string(envelopeNumber); | |||
| return retVal; | |||
| } | |||
| inline std::string getRandomXMLName(int randomNumber) { | |||
| std::string retVal("Random_"); | |||
| retVal += std::to_string(randomNumber); | |||
| return retVal; | |||
| } | |||
| @@ -0,0 +1,752 @@ | |||
| #include "XmlReader.hpp" | |||
| #include "XmlConsts.hpp" | |||
| #include "ChainSlotProcessors.hpp" | |||
| #include "ChainProcessors.hpp" | |||
| #include "SplitterMutators.hpp" | |||
| #include "SplitTypes.hpp" | |||
| #include "RichterLFO/RichterLFO.h" | |||
| namespace { | |||
| int comparePluginDescriptionAgainstTarget(const juce::PluginDescription& description, const juce::PluginDescription& target) { | |||
| // Format is most important for compatibility when loading settings | |||
| if (description.pluginFormatName == target.pluginFormatName) { | |||
| return 2; | |||
| } | |||
| if (description.version == target.version) { | |||
| return 1; | |||
| } | |||
| return 0; | |||
| } | |||
| class AvailableTypesSorter { | |||
| public: | |||
| AvailableTypesSorter(const juce::PluginDescription& target) : _target(target) {} | |||
| int compareElements(const juce::PluginDescription& a, const juce::PluginDescription& b) { | |||
| return comparePluginDescriptionAgainstTarget(b, _target) - comparePluginDescriptionAgainstTarget(a, _target); | |||
| } | |||
| private: | |||
| const juce::PluginDescription& _target; | |||
| }; | |||
| juce::String pluginTypesToString(const juce::Array<juce::PluginDescription>& types) { | |||
| juce::String retVal; | |||
| for (const juce::PluginDescription& type : types) { | |||
| retVal += type.pluginFormatName + " " + type.version + ", "; | |||
| } | |||
| return retVal; | |||
| } | |||
| } | |||
| namespace XmlReader { | |||
| std::shared_ptr<PluginSplitter> restoreSplitterFromXml( | |||
| juce::XmlElement* element, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback, | |||
| HostConfiguration configuration, | |||
| const PluginConfigurator& pluginConfigurator, | |||
| juce::Array<juce::PluginDescription> availableTypes, | |||
| std::function<void(juce::String)> onErrorCallback) { | |||
| // Default to series | |||
| SPLIT_TYPE splitType = SPLIT_TYPE::SERIES; | |||
| if (element->hasAttribute(XML_SPLIT_TYPE_STR)) { | |||
| const juce::String splitTypeString = element->getStringAttribute(XML_SPLIT_TYPE_STR); | |||
| juce::Logger::writeToLog("Restoring split type: " + splitTypeString); | |||
| // We need to check if the split type we're attempting to restore is supported. | |||
| // For example in Logic when switching from a stereo to mono plugin we may have saved to XML | |||
| // using a left/right split in a 2in2out configuration but will be restoring into a 1in1out | |||
| // configuration. | |||
| // In that case we move to a parallel split type. | |||
| splitType = stringToSplitType(splitTypeString); | |||
| const bool isExpecting2in2out {splitType == SPLIT_TYPE::LEFTRIGHT || splitType == SPLIT_TYPE::MIDSIDE}; | |||
| if (isExpecting2in2out && !canDoStereoSplitTypes(configuration.layout)) { | |||
| // Migrate to parallel | |||
| splitType = SPLIT_TYPE::PARALLEL; | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SPLIT_TYPE_STR)); | |||
| } | |||
| std::shared_ptr<PluginSplitter> splitter; | |||
| if (splitType == SPLIT_TYPE::SERIES) { | |||
| splitter = std::make_shared<PluginSplitterSeries>(configuration, getModulationValueCallback, latencyChangeCallback); | |||
| } else if (splitType == SPLIT_TYPE::PARALLEL) { | |||
| splitter = std::make_shared<PluginSplitterParallel>(configuration, getModulationValueCallback, latencyChangeCallback); | |||
| } else if (splitType == SPLIT_TYPE::MULTIBAND) { | |||
| splitter = std::make_shared<PluginSplitterMultiband>(configuration, getModulationValueCallback, latencyChangeCallback); | |||
| } else if (splitType == SPLIT_TYPE::LEFTRIGHT) { | |||
| splitter = std::make_shared<PluginSplitterLeftRight>(configuration, getModulationValueCallback, latencyChangeCallback); | |||
| } else if (splitType == SPLIT_TYPE::MIDSIDE) { | |||
| splitter = std::make_shared<PluginSplitterMidSide>(configuration, getModulationValueCallback, latencyChangeCallback); | |||
| } | |||
| // Reset state | |||
| while (!splitter->chains.empty()) { | |||
| splitter->chains.erase(splitter->chains.begin()); | |||
| } | |||
| // Restore each chain | |||
| juce::XmlElement* chainsElement = element->getChildByName(XML_CHAINS_STR); | |||
| const int numChains { | |||
| chainsElement == nullptr ? 0 : chainsElement->getNumChildElements() | |||
| }; | |||
| for (int chainNumber {0}; chainNumber < numChains; chainNumber++) { | |||
| juce::Logger::writeToLog("Restoring chain " + juce::String(chainNumber)); | |||
| const juce::String chainElementName = getChainXMLName(chainNumber); | |||
| juce::XmlElement* thisChainElement = chainsElement->getChildByName(chainElementName); | |||
| if (thisChainElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + chainElementName); | |||
| } else { | |||
| bool isSoloed {false}; | |||
| if (thisChainElement->hasAttribute(XML_IS_CHAIN_SOLOED_STR)) { | |||
| isSoloed = thisChainElement->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_IS_CHAIN_SOLOED_STR)); | |||
| } | |||
| // Add the chain to the vector | |||
| splitter->chains.emplace_back(std::make_shared<PluginChain>(getModulationValueCallback), false); | |||
| PluginChainWrapper& thisChain = splitter->chains[splitter->chains.size() - 1]; | |||
| thisChain.chain = XmlReader::restoreChainFromXml(thisChainElement, configuration, pluginConfigurator, getModulationValueCallback, availableTypes, onErrorCallback); | |||
| if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| // Since we deleted all chains at the start to make sure we have a | |||
| // clean starting point, that can mean the first few crossover bands could still exist | |||
| // and be pointing at chains that have been deleted. We handle this here. | |||
| if (multibandSplitter->chains.size() > CrossoverMutators::getNumBands(multibandSplitter->crossover)) { | |||
| // Need to add a new band and chain | |||
| CrossoverMutators::addBand(multibandSplitter->crossover); | |||
| } else { | |||
| // We already have the bands in the crossover | |||
| } | |||
| // Now assign the chain to the band | |||
| CrossoverMutators::setPluginChain(multibandSplitter->crossover, multibandSplitter->chains.size() - 1, thisChain.chain); | |||
| } | |||
| ChainProcessors::prepareToPlay(*thisChain.chain.get(), configuration); | |||
| thisChain.chain->latencyListener.setSplitter(splitter.get()); | |||
| SplitterMutators::setChainSolo(splitter, chainNumber, isSoloed); | |||
| } | |||
| } | |||
| if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| // Restore the crossover frequencies | |||
| juce::XmlElement* crossoversElement = element->getChildByName(XML_CROSSOVERS_STR); | |||
| if (crossoversElement != nullptr) { | |||
| for (int crossoverNumber {0}; crossoverNumber < multibandSplitter->chains.size() - 1; crossoverNumber++) { | |||
| const juce::String frequencyAttribute(getCrossoverXMLName(crossoverNumber)); | |||
| if (crossoversElement->hasAttribute(frequencyAttribute)) { | |||
| SplitterMutators::setCrossoverFrequency(multibandSplitter, crossoverNumber, crossoversElement->getDoubleAttribute(frequencyAttribute)); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(frequencyAttribute)); | |||
| } | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing element " + juce::String(XML_CROSSOVERS_STR)); | |||
| } | |||
| } | |||
| splitter->onLatencyChange(); | |||
| return splitter; | |||
| } | |||
| std::unique_ptr<PluginChain> restoreChainFromXml( | |||
| juce::XmlElement* element, | |||
| HostConfiguration configuration, | |||
| const PluginConfigurator& pluginConfigurator, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| juce::Array<juce::PluginDescription> availableTypes, | |||
| std::function<void(juce::String)> onErrorCallback) { | |||
| auto retVal = std::make_unique<PluginChain>(getModulationValueCallback); | |||
| // Restore chain level bypass, mute, and name | |||
| if (element->hasAttribute(XML_IS_CHAIN_BYPASSED_STR)) { | |||
| retVal->isChainBypassed = element->getBoolAttribute(XML_IS_CHAIN_BYPASSED_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_IS_CHAIN_BYPASSED_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_IS_CHAIN_MUTED_STR)) { | |||
| retVal->isChainMuted = element->getBoolAttribute(XML_IS_CHAIN_MUTED_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_IS_CHAIN_MUTED_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_CHAIN_CUSTOM_NAME_STR)) { | |||
| retVal->customName = element->getStringAttribute(XML_CHAIN_CUSTOM_NAME_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_CHAIN_CUSTOM_NAME_STR)); | |||
| } | |||
| // Load each plugin | |||
| juce::XmlElement* pluginsElement = element->getChildByName(XML_PLUGINS_STR); | |||
| if (pluginsElement == nullptr) { | |||
| juce::Logger::writeToLog("Missing child element " + juce::String(XML_PLUGINS_STR)); | |||
| } | |||
| const int numPlugins { | |||
| pluginsElement == nullptr ? 0 : pluginsElement->getNumChildElements() | |||
| }; | |||
| for (int pluginNumber {0}; pluginNumber < numPlugins; pluginNumber++) { | |||
| juce::Logger::writeToLog("Restoring slot " + juce::String(pluginNumber)); | |||
| const juce::String pluginElementName = getSlotXMLName(pluginNumber); | |||
| juce::XmlElement* thisPluginElement = pluginsElement->getChildByName(pluginElementName); | |||
| if (thisPluginElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + pluginElementName); | |||
| continue; | |||
| } | |||
| if (XmlReader::XmlElementIsPlugin(thisPluginElement)) { | |||
| auto loadPlugin = [&availableTypes](const juce::PluginDescription& description, const HostConfiguration& config) { | |||
| juce::AudioPluginFormatManager formatManager; | |||
| formatManager.addDefaultFormats(); | |||
| // First try the exact match | |||
| juce::String errorMessage; | |||
| std::unique_ptr<juce::AudioPluginInstance> thisPlugin = | |||
| formatManager.createPluginInstance( | |||
| description, config.sampleRate, config.blockSize, errorMessage); | |||
| // Failing that, get all possible matches from the available types | |||
| if (thisPlugin == nullptr) { | |||
| juce::Logger::writeToLog("Failed to load plugin " + description.name + ": " + errorMessage); | |||
| juce::Logger::writeToLog("Looking for alternatives"); | |||
| juce::Array<juce::PluginDescription> possibleTypes; | |||
| for (const juce::PluginDescription& availableType : availableTypes) { | |||
| if (description.manufacturerName == availableType.manufacturerName && | |||
| description.name == availableType.name) { | |||
| possibleTypes.add(availableType); | |||
| } | |||
| } | |||
| // Sort by best match first | |||
| AvailableTypesSorter sorter(description); | |||
| possibleTypes.sort(sorter); | |||
| const juce::String possibleTypesString = pluginTypesToString(possibleTypes); | |||
| juce::Logger::writeToLog("Found alternatives: " + possibleTypesString); | |||
| for (const juce::PluginDescription& possibleType : possibleTypes) { | |||
| juce::String newErrorMessage; | |||
| thisPlugin = formatManager.createPluginInstance( | |||
| possibleType, config.sampleRate, config.blockSize, newErrorMessage); | |||
| if (thisPlugin != nullptr) { | |||
| juce::Logger::writeToLog("Loaded " + possibleType.pluginFormatName + " " + possibleType.version); | |||
| break; | |||
| } | |||
| errorMessage += " - " + newErrorMessage; | |||
| } | |||
| } | |||
| return std::make_tuple<std::unique_ptr<juce::AudioPluginInstance>, juce::String>( | |||
| std::move(thisPlugin), juce::String(errorMessage)); | |||
| }; | |||
| auto newPlugin = XmlReader::restoreChainSlotPlugin( | |||
| thisPluginElement, getModulationValueCallback, configuration, pluginConfigurator, loadPlugin, onErrorCallback); | |||
| if (newPlugin != nullptr) { | |||
| newPlugin->plugin->addListener(&retVal->latencyListener); | |||
| retVal->chain.push_back(std::move(newPlugin)); | |||
| } | |||
| } else if (XmlReader::XmlElementIsGainStage(thisPluginElement)) { | |||
| auto newGainStage = XmlReader::restoreChainSlotGainStage(thisPluginElement, configuration.layout); | |||
| if (newGainStage != nullptr) { | |||
| // Call prepareToPlay since some hosts won't call it after restoring | |||
| ChainProcessors::prepareToPlay(*newGainStage.get(), configuration); | |||
| retVal->chain.push_back(std::move(newGainStage)); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Can't determine slot type"); | |||
| } | |||
| } | |||
| retVal->latencyListener.onPluginChainUpdate(); | |||
| return retVal; | |||
| } | |||
| bool XmlElementIsPlugin(juce::XmlElement* element) { | |||
| if (element->hasAttribute(XML_SLOT_TYPE_STR)) { | |||
| if (element->getStringAttribute(XML_SLOT_TYPE_STR) == XML_SLOT_TYPE_PLUGIN_STR) { | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| bool XmlElementIsGainStage(juce::XmlElement* element) { | |||
| if (element->hasAttribute(XML_SLOT_TYPE_STR)) { | |||
| if (element->getStringAttribute(XML_SLOT_TYPE_STR) == XML_SLOT_TYPE_GAIN_STAGE_STR) { | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| std::unique_ptr<ChainSlotGainStage> restoreChainSlotGainStage( | |||
| juce::XmlElement* element, const juce::AudioProcessor::BusesLayout& busesLayout) { | |||
| bool isSlotBypassed {false}; | |||
| if (element->hasAttribute(XML_SLOT_IS_BYPASSED_STR)) { | |||
| isSlotBypassed = element->getBoolAttribute(XML_SLOT_IS_BYPASSED_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SLOT_IS_BYPASSED_STR)); | |||
| } | |||
| float gain {1.0f}; | |||
| if (element->hasAttribute(XML_GAIN_STAGE_GAIN_STR)) { | |||
| gain = element->getDoubleAttribute(XML_GAIN_STAGE_GAIN_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_GAIN_STAGE_GAIN_STR)); | |||
| } | |||
| float pan {0.0f}; | |||
| if (element->hasAttribute(XML_GAIN_STAGE_PAN_STR)) { | |||
| pan = element->getDoubleAttribute(XML_GAIN_STAGE_PAN_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_GAIN_STAGE_PAN_STR)); | |||
| } | |||
| return std::make_unique<ChainSlotGainStage>(gain, pan, isSlotBypassed, busesLayout); | |||
| } | |||
| std::unique_ptr<ChainSlotPlugin> restoreChainSlotPlugin( | |||
| juce::XmlElement* element, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| HostConfiguration configuration, | |||
| const PluginConfigurator& pluginConfigurator, | |||
| LoadPluginFunction loadPlugin, | |||
| std::function<void(juce::String)> onErrorCallback) { | |||
| // Restore the plugin level bypass | |||
| bool isPluginBypassed {false}; | |||
| if (element->hasAttribute(XML_SLOT_IS_BYPASSED_STR)) { | |||
| isPluginBypassed = element->getBoolAttribute(XML_SLOT_IS_BYPASSED_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_SLOT_IS_BYPASSED_STR)); | |||
| } | |||
| if (element->getNumChildElements() == 0) { | |||
| juce::Logger::writeToLog("Plugin element missing description"); | |||
| return nullptr; | |||
| } | |||
| // Load the actual plugin | |||
| juce::XmlElement* pluginDescriptionXml = element->getChildElement(0); | |||
| juce::PluginDescription pluginDescription; | |||
| if (!pluginDescription.loadFromXml(*pluginDescriptionXml)) { | |||
| juce::Logger::writeToLog("Failed to parse plugin description"); | |||
| return nullptr; | |||
| } | |||
| auto [thisPlugin, errorMessage] = loadPlugin(pluginDescription, configuration); | |||
| if (thisPlugin == nullptr) { | |||
| juce::Logger::writeToLog("Failed to load plugin: " + errorMessage); | |||
| onErrorCallback("Failed to restore plugin: " + errorMessage); | |||
| return nullptr; | |||
| } | |||
| std::unique_ptr<ChainSlotPlugin> retVal; | |||
| std::shared_ptr<juce::AudioPluginInstance> sharedPlugin = std::move(thisPlugin); | |||
| if (pluginConfigurator.configure(sharedPlugin, configuration)) { | |||
| retVal.reset(new ChainSlotPlugin(sharedPlugin, isPluginBypassed, getModulationValueCallback, configuration)); | |||
| // Restore the editor bounds | |||
| if (element->hasAttribute(XML_PLUGIN_EDITOR_BOUNDS_STR)) { | |||
| const juce::String boundsString = element->getStringAttribute(XML_PLUGIN_EDITOR_BOUNDS_STR); | |||
| if (element->hasAttribute(XML_DISPLAY_AREA_STR)) { | |||
| const juce::String displayString = element->getStringAttribute(XML_DISPLAY_AREA_STR); | |||
| retVal->editorBounds.reset(new PluginEditorBounds()); | |||
| *(retVal->editorBounds.get()) = PluginEditorBoundsContainer( | |||
| juce::Rectangle<int>::fromString(boundsString), | |||
| juce::Rectangle<int>::fromString(displayString) | |||
| ); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_DISPLAY_AREA_STR)); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_PLUGIN_EDITOR_BOUNDS_STR)); | |||
| } | |||
| // Restore the plugin's internal state | |||
| if (element->hasAttribute(XML_PLUGIN_DATA_STR)) { | |||
| const juce::String pluginDataString = element->getStringAttribute(XML_PLUGIN_DATA_STR); | |||
| juce::MemoryBlock pluginData; | |||
| pluginData.fromBase64Encoding(pluginDataString); | |||
| sharedPlugin->setStateInformation(pluginData.getData(), pluginData.getSize()); | |||
| // Now that the plugin is restored, we can restore the modulation config | |||
| juce::XmlElement* modulationConfigElement = element->getChildByName(XML_MODULATION_CONFIG_STR); | |||
| if (modulationConfigElement != nullptr) { | |||
| retVal->modulationConfig = restorePluginModulationConfig(modulationConfigElement); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing element " + juce::String(XML_MODULATION_CONFIG_STR)); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_PLUGIN_DATA_STR)); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Failed to configure plugin: " + sharedPlugin->getPluginDescription().name); | |||
| onErrorCallback("Failed to restore " + sharedPlugin->getPluginDescription().name + " as it may be a mono only plugin being restored into a stereo instance of Syndicate or vice versa"); | |||
| } | |||
| return retVal; | |||
| } | |||
| std::unique_ptr<PluginModulationConfig> restorePluginModulationConfig(juce::XmlElement* element) { | |||
| auto retVal = std::make_unique<PluginModulationConfig>(); | |||
| if (element->hasAttribute(XML_MODULATION_IS_ACTIVE_STR)) { | |||
| retVal->isActive = element->getBoolAttribute(XML_MODULATION_IS_ACTIVE_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_IS_ACTIVE_STR)); | |||
| } | |||
| const int numParameterConfigs {element->getNumChildElements()}; | |||
| for (int index {0}; index < numParameterConfigs; index++) { | |||
| juce::Logger::writeToLog("Restoring parameter modulation config " + juce::String(index)); | |||
| const juce::String parameterConfigElementName = getParameterModulationConfigXmlName(index); | |||
| juce::XmlElement* thisParameterConfigElement = element->getChildByName(parameterConfigElementName); | |||
| retVal->parameterConfigs.push_back( | |||
| restorePluginParameterModulationConfig(thisParameterConfigElement)); | |||
| } | |||
| return retVal; | |||
| } | |||
| std::unique_ptr<PluginParameterModulationConfig> restorePluginParameterModulationConfig(juce::XmlElement* element) { | |||
| auto retVal = std::make_unique<PluginParameterModulationConfig>(); | |||
| if (element->hasAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR)) { | |||
| retVal->targetParameterName = element->getStringAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_TARGET_PARAMETER_NAME_STR)); | |||
| } | |||
| if (element->hasAttribute(XML_MODULATION_REST_VALUE_STR)) { | |||
| retVal->restValue = element->getDoubleAttribute(XML_MODULATION_REST_VALUE_STR); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_REST_VALUE_STR)); | |||
| } | |||
| const int numSources {element->getNumChildElements()}; | |||
| for (int index {0}; index < numSources; index++) { | |||
| juce::Logger::writeToLog("Restoring parameter modulation sources " + juce::String(index)); | |||
| const juce::String sourceElementName = getParameterModulationSourceXmlName(index); | |||
| juce::XmlElement* thisSourceElement = element->getChildByName(sourceElementName); | |||
| std::shared_ptr<PluginParameterModulationSource> thisSource = restorePluginParameterModulationSource(thisSourceElement); | |||
| retVal->sources.push_back(thisSource); | |||
| } | |||
| return retVal; | |||
| } | |||
| std::unique_ptr<PluginParameterModulationSource> restorePluginParameterModulationSource(juce::XmlElement* element) { | |||
| // TODO tidy definition construction | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(element); | |||
| float modulationAmount {0}; | |||
| if (element->hasAttribute(XML_MODULATION_SOURCE_AMOUNT)) { | |||
| modulationAmount = element->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } | |||
| return std::make_unique<PluginParameterModulationSource>(definition, modulationAmount); | |||
| } | |||
| void restoreModulationSourcesFromXml(ModelInterface::ModulationSourcesState& state, | |||
| juce::XmlElement* element, | |||
| HostConfiguration configuration) { | |||
| // Clear existing state | |||
| state.lfos.clear(); | |||
| state.envelopes.clear(); | |||
| state.randomSources.clear(); | |||
| // LFOs | |||
| juce::XmlElement* lfosElement = element->getChildByName(XML_LFOS_STR); | |||
| if (lfosElement != nullptr) { | |||
| const int numLfos {lfosElement->getNumChildElements()}; | |||
| for (int index {0}; index < numLfos; index++) { | |||
| juce::Logger::writeToLog("Restoring LFO " + juce::String(index)); | |||
| const juce::String lfoElementName = getLfoXMLName(index); | |||
| juce::XmlElement* thisLfoElement = lfosElement->getChildByName(lfoElementName); | |||
| if (thisLfoElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + lfoElementName); | |||
| continue; | |||
| } | |||
| std::shared_ptr<ModelInterface::CloneableLFO> newLfo {new ModelInterface::CloneableLFO()}; | |||
| newLfo->setBypassSwitch(thisLfoElement->getBoolAttribute(XML_LFO_BYPASS_STR)); | |||
| newLfo->setPhaseSyncSwitch(thisLfoElement->getBoolAttribute(XML_LFO_PHASE_SYNC_STR)); | |||
| newLfo->setTempoSyncSwitch(thisLfoElement->getBoolAttribute(XML_LFO_TEMPO_SYNC_STR)); | |||
| newLfo->setInvertSwitch(thisLfoElement->getBoolAttribute(XML_LFO_INVERT_STR)); | |||
| newLfo->setOutputMode(thisLfoElement->getIntAttribute(XML_LFO_OUTPUT_MODE_STR, WECore::Richter::Parameters::OutputModeParameter::BIPOLAR)); // This paramter was added later - default to bipolar if not found | |||
| newLfo->setWave(thisLfoElement->getIntAttribute(XML_LFO_WAVE_STR)); | |||
| newLfo->setTempoNumer(thisLfoElement->getIntAttribute(XML_LFO_TEMPO_NUMER_STR)); | |||
| newLfo->setTempoDenom(thisLfoElement->getIntAttribute(XML_LFO_TEMPO_DENOM_STR)); | |||
| newLfo->setFreq(thisLfoElement->getDoubleAttribute(XML_LFO_FREQ_STR)); | |||
| newLfo->setDepth(thisLfoElement->getDoubleAttribute(XML_LFO_DEPTH_STR)); | |||
| newLfo->setManualPhase(thisLfoElement->getDoubleAttribute(XML_LFO_MANUAL_PHASE_STR)); | |||
| newLfo->setSampleRate(configuration.sampleRate); | |||
| juce::XmlElement* freqModElement = thisLfoElement->getChildByName(XML_LFO_FREQ_MODULATION_SOURCES_STR); | |||
| if (freqModElement != nullptr) { | |||
| const int numFreqModSources {freqModElement->getNumChildElements()}; | |||
| for (int sourceIndex {0}; sourceIndex < numFreqModSources; sourceIndex++) { | |||
| juce::Logger::writeToLog("Restoring LFO freq modulation source " + juce::String(sourceIndex)); | |||
| const juce::String sourceElementName = getParameterModulationSourceXmlName(sourceIndex); | |||
| juce::XmlElement* thisSourceElement = freqModElement->getChildByName(sourceElementName); | |||
| if (thisSourceElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + sourceElementName); | |||
| continue; | |||
| } | |||
| // TODO error handling restoring definition | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(thisSourceElement); | |||
| auto newSource = std::make_shared<ModulationSourceProvider>(definition, state.getModulationValueCallback); | |||
| newLfo->addFreqModulationSource(newSource); | |||
| if (thisSourceElement->hasAttribute(XML_MODULATION_SOURCE_AMOUNT)) { | |||
| newLfo->setFreqModulationAmount(sourceIndex, thisSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } | |||
| } | |||
| } | |||
| juce::XmlElement* depthModElement = thisLfoElement->getChildByName(XML_LFO_DEPTH_MODULATION_SOURCES_STR); | |||
| if (depthModElement != nullptr) { | |||
| const int numDepthModSources {depthModElement->getNumChildElements()}; | |||
| for (int sourceIndex {0}; sourceIndex < numDepthModSources; sourceIndex++) { | |||
| juce::Logger::writeToLog("Restoring LFO depth modulation source " + juce::String(sourceIndex)); | |||
| const juce::String sourceElementName = getParameterModulationSourceXmlName(sourceIndex); | |||
| juce::XmlElement* thisSourceElement = depthModElement->getChildByName(sourceElementName); | |||
| if (thisSourceElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + sourceElementName); | |||
| continue; | |||
| } | |||
| // TODO error handling restoring definition | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(thisSourceElement); | |||
| auto newSource = std::make_shared<ModulationSourceProvider>(definition, state.getModulationValueCallback); | |||
| newLfo->addDepthModulationSource(newSource); | |||
| if (thisSourceElement->hasAttribute(XML_MODULATION_SOURCE_AMOUNT)) { | |||
| newLfo->setDepthModulationAmount(sourceIndex, thisSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } | |||
| } | |||
| } | |||
| juce::XmlElement* phaseModElement = thisLfoElement->getChildByName(XML_LFO_PHASE_MODULATION_SOURCES_STR); | |||
| if (phaseModElement != nullptr) { | |||
| const int numPhaseModSources {phaseModElement->getNumChildElements()}; | |||
| for (int sourceIndex {0}; sourceIndex < numPhaseModSources; sourceIndex++) { | |||
| juce::Logger::writeToLog("Restoring LFO phase modulation source " + juce::String(sourceIndex)); | |||
| const juce::String sourceElementName = getParameterModulationSourceXmlName(sourceIndex); | |||
| juce::XmlElement* thisSourceElement = phaseModElement->getChildByName(sourceElementName); | |||
| if (thisSourceElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + sourceElementName); | |||
| continue; | |||
| } | |||
| // TODO error handling restoring definition | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(thisSourceElement); | |||
| auto newSource = std::make_shared<ModulationSourceProvider>(definition, state.getModulationValueCallback); | |||
| newLfo->addPhaseModulationSource(newSource); | |||
| if (thisSourceElement->hasAttribute(XML_MODULATION_SOURCE_AMOUNT)) { | |||
| newLfo->setPhaseModulationAmount(sourceIndex, thisSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } | |||
| } | |||
| } | |||
| state.lfos.push_back(newLfo); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing element " + juce::String(XML_LFOS_STR)); | |||
| } | |||
| // Envelopes | |||
| juce::XmlElement* envelopesElement = element->getChildByName(XML_ENVELOPES_STR); | |||
| if (envelopesElement != nullptr) { | |||
| const int numEnvelopes {envelopesElement->getNumChildElements()}; | |||
| for (int index {0}; index < numEnvelopes; index++) { | |||
| juce::Logger::writeToLog("Restoring envelope " + juce::String(index)); | |||
| const juce::String envelopeElementName = getEnvelopeXMLName(index); | |||
| juce::XmlElement* thisEnvelopeElement = envelopesElement->getChildByName(envelopeElementName); | |||
| if (thisEnvelopeElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + envelopeElementName); | |||
| continue; | |||
| } | |||
| std::shared_ptr<ModelInterface::EnvelopeWrapper> newEnv(new ModelInterface::EnvelopeWrapper()); | |||
| newEnv->envelope->setAttackTimeMs(thisEnvelopeElement->getDoubleAttribute(XML_ENV_ATTACK_TIME_STR)); | |||
| newEnv->envelope->setReleaseTimeMs(thisEnvelopeElement->getDoubleAttribute(XML_ENV_RELEASE_TIME_STR)); | |||
| newEnv->envelope->setFilterEnabled(thisEnvelopeElement->getBoolAttribute(XML_ENV_FILTER_ENABLED_STR)); | |||
| newEnv->envelope->setLowCutHz(thisEnvelopeElement->getDoubleAttribute(XML_ENV_LOW_CUT_STR)); | |||
| newEnv->envelope->setHighCutHz(thisEnvelopeElement->getDoubleAttribute(XML_ENV_HIGH_CUT_STR)); | |||
| newEnv->envelope->setSampleRate(configuration.sampleRate); | |||
| newEnv->amount = thisEnvelopeElement->getDoubleAttribute(XML_ENV_AMOUNT_STR); | |||
| newEnv->useSidechainInput = thisEnvelopeElement->getBoolAttribute(XML_ENV_USE_SIDECHAIN_INPUT_STR); | |||
| state.envelopes.push_back(newEnv); | |||
| } | |||
| } else { | |||
| juce::Logger::writeToLog("Missing element " + juce::String(XML_ENVELOPES_STR)); | |||
| } | |||
| // Random | |||
| juce::XmlElement* randomsElement = element->getChildByName(XML_RANDOMS_STR); | |||
| if (randomsElement != nullptr) { | |||
| const int numRandoms {randomsElement->getNumChildElements()}; | |||
| for (int index {0}; index < numRandoms; index++) { | |||
| juce::Logger::writeToLog("Restoring randoms " + juce::String(index)); | |||
| const juce::String randomElementName = getRandomXMLName(index); | |||
| juce::XmlElement* thisRandomElement = randomsElement->getChildByName(randomElementName); | |||
| if (thisRandomElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + randomElementName); | |||
| continue; | |||
| } | |||
| std::shared_ptr<WECore::Perlin::PerlinSource> newRandom {new WECore::Perlin::PerlinSource()}; | |||
| newRandom->setOutputMode(thisRandomElement->getIntAttribute(XML_RANDOM_OUTPUT_MODE_STR)); | |||
| newRandom->setFreq(thisRandomElement->getDoubleAttribute(XML_RANDOM_FREQ_STR)); | |||
| newRandom->setDepth(thisRandomElement->getDoubleAttribute(XML_RANDOM_DEPTH_STR)); | |||
| newRandom->setSampleRate(configuration.sampleRate); | |||
| juce::XmlElement* freqModElement = thisRandomElement->getChildByName(XML_RANDOM_FREQ_MODULATION_SOURCES_STR); | |||
| if (freqModElement != nullptr) { | |||
| const int numFreqModSources {freqModElement->getNumChildElements()}; | |||
| for (int sourceIndex {0}; sourceIndex < numFreqModSources; sourceIndex++) { | |||
| juce::Logger::writeToLog("Restoring random freq modulation source " + juce::String(sourceIndex)); | |||
| const juce::String sourceElementName = getParameterModulationSourceXmlName(sourceIndex); | |||
| juce::XmlElement* thisSourceElement = freqModElement->getChildByName(sourceElementName); | |||
| if (thisSourceElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + sourceElementName); | |||
| continue; | |||
| } | |||
| // TODO error handling restoring definition | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(thisSourceElement); | |||
| auto newSource = std::make_shared<ModulationSourceProvider>(definition, state.getModulationValueCallback); | |||
| newRandom->addFreqModulationSource(newSource); | |||
| if (thisSourceElement->hasAttribute(XML_MODULATION_SOURCE_AMOUNT)) { | |||
| newRandom->setFreqModulationAmount(sourceIndex, thisSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } | |||
| } | |||
| } | |||
| juce::XmlElement* depthModElement = thisRandomElement->getChildByName(XML_RANDOM_DEPTH_MODULATION_SOURCES_STR); | |||
| if (depthModElement != nullptr) { | |||
| const int numDepthModSources {depthModElement->getNumChildElements()}; | |||
| for (int sourceIndex {0}; sourceIndex < numDepthModSources; sourceIndex++) { | |||
| juce::Logger::writeToLog("Restoring random depth modulation source " + juce::String(sourceIndex)); | |||
| const juce::String sourceElementName = getParameterModulationSourceXmlName(sourceIndex); | |||
| juce::XmlElement* thisSourceElement = depthModElement->getChildByName(sourceElementName); | |||
| if (thisSourceElement == nullptr) { | |||
| juce::Logger::writeToLog("Failed to get element " + sourceElementName); | |||
| continue; | |||
| } | |||
| // TODO error handling restoring definition | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(thisSourceElement); | |||
| auto newSource = std::make_shared<ModulationSourceProvider>(definition, state.getModulationValueCallback); | |||
| newRandom->addDepthModulationSource(newSource); | |||
| if (thisSourceElement->hasAttribute(XML_MODULATION_SOURCE_AMOUNT)) { | |||
| newRandom->setDepthModulationAmount(sourceIndex, thisSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } else { | |||
| juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MODULATION_SOURCE_AMOUNT)); | |||
| } | |||
| } | |||
| } | |||
| state.randomSources.push_back(newRandom); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "ChainSlots.hpp" | |||
| #include "PluginChain.hpp" | |||
| #include "PluginSplitter.hpp" | |||
| #include "DataModelInterface.hpp" | |||
| // TODO lock on entry so UI can't make changes | |||
| namespace XmlReader { | |||
| std::shared_ptr<PluginSplitter> restoreSplitterFromXml( | |||
| juce::XmlElement* element, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| std::function<void(int)> latencyChangeCallback, | |||
| HostConfiguration configuration, | |||
| const PluginConfigurator& pluginConfigurator, | |||
| juce::Array<juce::PluginDescription> availableTypes, | |||
| std::function<void(juce::String)> onErrorCallback); | |||
| std::unique_ptr<PluginChain> restoreChainFromXml( | |||
| juce::XmlElement* element, | |||
| HostConfiguration configuration, | |||
| const PluginConfigurator& pluginConfigurator, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| juce::Array<juce::PluginDescription> availableTypes, | |||
| std::function<void(juce::String)> onErrorCallback); | |||
| bool XmlElementIsPlugin(juce::XmlElement* element); | |||
| bool XmlElementIsGainStage(juce::XmlElement* element); | |||
| std::unique_ptr<ChainSlotGainStage> restoreChainSlotGainStage( | |||
| juce::XmlElement* element, const juce::AudioProcessor::BusesLayout& busesLayout); | |||
| typedef std::function< | |||
| std::tuple<std::unique_ptr<juce::AudioPluginInstance>, juce::String>( | |||
| const juce::PluginDescription&, const HostConfiguration&)> LoadPluginFunction; | |||
| std::unique_ptr<ChainSlotPlugin> restoreChainSlotPlugin( | |||
| juce::XmlElement* element, | |||
| std::function<float(int, MODULATION_TYPE)> getModulationValueCallback, | |||
| HostConfiguration configuration, | |||
| const PluginConfigurator& pluginConfigurator, | |||
| LoadPluginFunction loadPlugin, | |||
| std::function<void(juce::String)> onErrorCallback); | |||
| std::unique_ptr<PluginModulationConfig> restorePluginModulationConfig(juce::XmlElement* element); | |||
| std::unique_ptr<PluginParameterModulationConfig> restorePluginParameterModulationConfig(juce::XmlElement* element); | |||
| std::unique_ptr<PluginParameterModulationSource> restorePluginParameterModulationSource(juce::XmlElement* element); | |||
| void restoreModulationSourcesFromXml( | |||
| ModelInterface::ModulationSourcesState& state, | |||
| juce::XmlElement* element, | |||
| HostConfiguration configuration); | |||
| } | |||
| @@ -0,0 +1,926 @@ | |||
| #include "catch.hpp" | |||
| #include "XmlReader.hpp" | |||
| #include "XmlConsts.hpp" | |||
| #include "XmlWriter.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "SplitterMutators.hpp" | |||
| #include "ModulationMutators.hpp" | |||
| namespace { | |||
| // Use values that would never be sensible defaults so we know if they were actually set | |||
| constexpr int NUM_SAMPLES {10}; | |||
| constexpr int SAMPLE_RATE {2000}; | |||
| class XMLTestPluginInstance : public TestUtils::TestPluginInstance { | |||
| public: | |||
| juce::PluginDescription description; | |||
| std::string retrievedData; | |||
| bool shouldSetLayoutSuccessfully; | |||
| XMLTestPluginInstance() : shouldSetLayoutSuccessfully(true) { } | |||
| XMLTestPluginInstance(const juce::PluginDescription& newDescription) : description(newDescription) { | |||
| shouldSetLayoutSuccessfully = description.name == "failLayout" ? false : true; | |||
| } | |||
| void getStateInformation(juce::MemoryBlock& destData) override { | |||
| const std::string testString("testPluginData"); | |||
| destData.append(testString.c_str(), testString.size()); | |||
| } | |||
| void setStateInformation(const void* data, int sizeInBytes) override { retrievedData = std::string(static_cast<const char*>(data), sizeInBytes); } | |||
| protected: | |||
| bool isBusesLayoutSupported(const BusesLayout& arr) const override { return shouldSetLayoutSuccessfully; } | |||
| }; | |||
| } | |||
| SCENARIO("XmlReader: Can restore PluginSplitter") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| HostConfiguration config; | |||
| config.sampleRate = SAMPLE_RATE; | |||
| config.blockSize = NUM_SAMPLES; | |||
| PluginConfigurator configurator; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.234f; | |||
| }; | |||
| bool latencyCalled {false}; | |||
| auto latencyCallback = [&latencyCalled](int) { | |||
| latencyCalled = true; | |||
| }; | |||
| auto errorCallback = [](juce::String) { | |||
| // Do nothing | |||
| }; | |||
| WHEN("Asked to restore a PluginSplitter from it") { | |||
| const juce::Array<juce::PluginDescription> pluginTypes; | |||
| std::shared_ptr<PluginSplitter> splitter = XmlReader::restoreSplitterFromXml( | |||
| &e, modulationCallback, latencyCallback, config, configurator, pluginTypes, errorCallback); | |||
| THEN("A PluginSplitter with default values is created") { | |||
| CHECK(std::dynamic_pointer_cast<PluginSplitterSeries>(splitter) != nullptr); | |||
| CHECK(splitter->chains.size() == 0); | |||
| CHECK(splitter->numChainsSoloed == 0); | |||
| CHECK(splitter->config.sampleRate == SAMPLE_RATE); | |||
| CHECK(splitter->config.blockSize == NUM_SAMPLES); | |||
| CHECK(latencyCalled); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement with three chains, one soloed") { | |||
| juce::XmlElement e("test"); | |||
| HostConfiguration config; | |||
| config.sampleRate = SAMPLE_RATE; | |||
| config.blockSize = NUM_SAMPLES; | |||
| config.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| PluginConfigurator configurator; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.234f; | |||
| }; | |||
| bool latencyCalled {false}; | |||
| auto latencyCallback = [&latencyCalled](int) { | |||
| latencyCalled = true; | |||
| }; | |||
| auto errorCallback = [](juce::String) { | |||
| // Do nothing | |||
| }; | |||
| const juce::String splitTypeString = GENERATE( | |||
| juce::String(XML_SPLIT_TYPE_SERIES_STR), | |||
| juce::String(XML_SPLIT_TYPE_PARALLEL_STR), | |||
| juce::String(XML_SPLIT_TYPE_MULTIBAND_STR), | |||
| juce::String(XML_SPLIT_TYPE_LEFTRIGHT_STR), | |||
| juce::String(XML_SPLIT_TYPE_MIDSIDE_STR) | |||
| ); | |||
| // Build the XML elements | |||
| e.setAttribute(XML_SPLIT_TYPE_STR, splitTypeString); | |||
| juce::XmlElement* chainsElement = e.createNewChildElement(XML_CHAINS_STR); | |||
| // First chain | |||
| auto chain1 = std::make_shared<PluginChain>(modulationCallback); | |||
| ChainMutators::insertGainStage(chain1, 0, config); | |||
| juce::XmlElement* chainElement1 = chainsElement->createNewChildElement(getChainXMLName(0)); | |||
| chainElement1->setAttribute(XML_IS_CHAIN_SOLOED_STR, true); | |||
| XmlWriter::write(chain1, chainElement1); | |||
| // Second chain | |||
| auto chain2 = std::make_shared<PluginChain>(modulationCallback); | |||
| ChainMutators::insertGainStage(chain2, 0, config); | |||
| ChainMutators::insertGainStage(chain2, 1, config); | |||
| juce::XmlElement* chainElement2 = chainsElement->createNewChildElement(getChainXMLName(1)); | |||
| chainElement2->setAttribute(XML_IS_CHAIN_SOLOED_STR, false); | |||
| XmlWriter::write(chain2, chainElement2); | |||
| // Third chain | |||
| auto chain3 = std::make_shared<PluginChain>(modulationCallback); | |||
| ChainMutators::insertGainStage(chain3, 0, config); | |||
| juce::XmlElement* chainElement3 = chainsElement->createNewChildElement(getChainXMLName(2)); | |||
| chainElement3->setAttribute(XML_IS_CHAIN_SOLOED_STR, true); | |||
| XmlWriter::write(chain3, chainElement3); | |||
| if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { | |||
| juce::XmlElement* crossoverElement = e.createNewChildElement(XML_CROSSOVERS_STR); | |||
| crossoverElement->setAttribute(getCrossoverXMLName(0), 2500); | |||
| crossoverElement->setAttribute(getCrossoverXMLName(1), 4000); | |||
| } | |||
| WHEN("Asked to restore a PluginSplitter from it") { | |||
| const juce::Array<juce::PluginDescription> pluginTypes; | |||
| std::shared_ptr<PluginSplitter> splitter = XmlReader::restoreSplitterFromXml( | |||
| &e, modulationCallback, latencyCallback, config, configurator, pluginTypes, errorCallback); | |||
| THEN("A PluginSplitter with the correct values is created") { | |||
| if (splitTypeString == XML_SPLIT_TYPE_SERIES_STR) { | |||
| CHECK(std::dynamic_pointer_cast<PluginSplitterSeries>(splitter) != nullptr); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { | |||
| CHECK(std::dynamic_pointer_cast<PluginSplitterParallel>(splitter) != nullptr); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { | |||
| auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter); | |||
| REQUIRE(multibandSplitter != nullptr); | |||
| CHECK(SplitterMutators::getCrossoverFrequency(multibandSplitter, 0) == Approx(2500.0)); | |||
| CHECK(SplitterMutators::getCrossoverFrequency(multibandSplitter, 1) == Approx(4000.0)); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR) { | |||
| CHECK(std::dynamic_pointer_cast<PluginSplitterLeftRight>(splitter) != nullptr); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { | |||
| CHECK(std::dynamic_pointer_cast<PluginSplitterMidSide>(splitter) != nullptr); | |||
| } | |||
| CHECK(splitter->chains.size() == 3); | |||
| CHECK(splitter->chains[0].chain->chain.size() == 1); | |||
| CHECK(splitter->chains[1].chain->chain.size() == 2); | |||
| CHECK(splitter->chains[2].chain->chain.size() == 1); | |||
| CHECK(splitter->config.sampleRate == SAMPLE_RATE); | |||
| CHECK(splitter->config.blockSize == NUM_SAMPLES); | |||
| CHECK(latencyCalled); | |||
| CHECK(splitter->numChainsSoloed == 2); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore PluginChain") { | |||
| const juce::Array<juce::PluginDescription> pluginTypes; | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| HostConfiguration config; | |||
| config.sampleRate = 44100; | |||
| config.blockSize = 64; | |||
| PluginConfigurator configurator; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.234f; | |||
| }; | |||
| auto errorCallback = [](juce::String) { | |||
| // Do nothing | |||
| }; | |||
| WHEN("Asked to restore a PluginChain from it") { | |||
| auto chain = XmlReader::restoreChainFromXml( | |||
| &e, config, configurator, modulationCallback, pluginTypes, errorCallback); | |||
| THEN("A PluginChain with default values is created") { | |||
| CHECK(chain->isChainBypassed == false); | |||
| CHECK(chain->isChainMuted == false); | |||
| CHECK(chain->chain.size() == 0); | |||
| CHECK(chain->getModulationValueCallback(0, MODULATION_TYPE::MACRO) == 1.234f); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has an invalid plugin element") { | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_IS_CHAIN_BYPASSED_STR, false); | |||
| e.setAttribute(XML_IS_CHAIN_MUTED_STR, false); | |||
| auto* pluginsElement = e.createNewChildElement(XML_PLUGINS_STR); | |||
| auto* invalidPluginElement = pluginsElement->createNewChildElement(getSlotXMLName(0)); | |||
| invalidPluginElement->setAttribute(XML_SLOT_TYPE_STR, "some invalid type"); | |||
| HostConfiguration config; | |||
| config.sampleRate = 44100; | |||
| config.blockSize = 64; | |||
| PluginConfigurator configurator; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.234f; | |||
| }; | |||
| auto errorCallback = [](juce::String) { | |||
| // Do nothing | |||
| }; | |||
| WHEN("Asked to restore a PluginChain from it") { | |||
| auto chain = XmlReader::restoreChainFromXml( | |||
| &e, config, configurator, modulationCallback, pluginTypes, errorCallback); | |||
| THEN("A PluginChain with default values is created") { | |||
| CHECK(chain->isChainBypassed == false); | |||
| CHECK(chain->isChainMuted == false); | |||
| CHECK(chain->chain.size() == 0); | |||
| CHECK(chain->getModulationValueCallback(0, MODULATION_TYPE::MACRO) == 1.234f); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has attributes set correctly") { | |||
| const bool isChainBypassed = GENERATE(false, true); | |||
| const bool isChainMuted = GENERATE(false, true); | |||
| const bool includeCustomName = GENERATE(false, true); // Test for backwards compatibility as this was added in 1.3.0 | |||
| // Only test loading gain stages - we can't test loading a real plugin here | |||
| const int numGainStages = GENERATE(0, 1, 2); | |||
| juce::XmlElement e("test"); | |||
| HostConfiguration config; | |||
| config.sampleRate = 44100; | |||
| config.blockSize = 64; | |||
| PluginConfigurator configurator; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.234f; | |||
| }; | |||
| auto errorCallback = [](juce::String) { | |||
| // Do nothing | |||
| }; | |||
| e.setAttribute(XML_IS_CHAIN_BYPASSED_STR, isChainBypassed); | |||
| e.setAttribute(XML_IS_CHAIN_MUTED_STR, isChainMuted); | |||
| if (includeCustomName) { | |||
| e.setAttribute(XML_CHAIN_CUSTOM_NAME_STR, "testChainName"); | |||
| } | |||
| auto* pluginsElement = e.createNewChildElement(XML_PLUGINS_STR); | |||
| for (int index {0}; index < numGainStages; index++) { | |||
| auto* gainStageElement = pluginsElement->createNewChildElement(getSlotXMLName(index)); | |||
| gainStageElement->setAttribute(XML_SLOT_TYPE_STR, XML_SLOT_TYPE_GAIN_STAGE_STR); | |||
| gainStageElement->setAttribute(XML_SLOT_IS_BYPASSED_STR, index % 2 == 0); | |||
| gainStageElement->setAttribute(XML_GAIN_STAGE_GAIN_STR, 0.1 * index); | |||
| gainStageElement->setAttribute(XML_GAIN_STAGE_PAN_STR, 0.21 * index); | |||
| } | |||
| WHEN("Asked to restore a PluginChain from it") { | |||
| auto chain = XmlReader::restoreChainFromXml( | |||
| &e, config, configurator, modulationCallback, pluginTypes, errorCallback); | |||
| THEN("A PluginChain with default values is created") { | |||
| CHECK(chain->isChainBypassed == isChainBypassed); | |||
| CHECK(chain->isChainMuted == isChainMuted); | |||
| CHECK(chain->customName == (includeCustomName ? "testChainName" : "")); | |||
| CHECK(chain->chain.size() == numGainStages); | |||
| CHECK(chain->getModulationValueCallback(0, MODULATION_TYPE::MACRO) == 1.234f); | |||
| for (int index {0}; index < numGainStages; index++) { | |||
| CHECK(chain->chain[index]->isBypassed == (index % 2 == 0)); | |||
| auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(chain->chain[index]); | |||
| REQUIRE(gainStage != nullptr); | |||
| CHECK(gainStage->gain == Approx(0.1 * index)); | |||
| CHECK(gainStage->pan == Approx(0.21 * index)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can determine if an XmlElement is a gain stage or a plugin") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| WHEN("Checked as a plugin") { | |||
| THEN("It's not a plugin") { | |||
| CHECK_FALSE(XmlReader::XmlElementIsPlugin(&e)); | |||
| } | |||
| } | |||
| WHEN("Checked as a gain stage") { | |||
| THEN("It's not a gain stage") { | |||
| CHECK_FALSE(XmlReader::XmlElementIsGainStage(&e)); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has an invalid value in the slot type") { | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_SLOT_TYPE_STR, "some invalid value"); | |||
| WHEN("Checked as a plugin") { | |||
| THEN("It's not a plugin") { | |||
| CHECK_FALSE(XmlReader::XmlElementIsPlugin(&e)); | |||
| } | |||
| } | |||
| WHEN("Checked as a gain stage") { | |||
| THEN("It's not a gain stage") { | |||
| CHECK_FALSE(XmlReader::XmlElementIsGainStage(&e)); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that is a gain stage") { | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_SLOT_TYPE_STR, XML_SLOT_TYPE_GAIN_STAGE_STR); | |||
| WHEN("Checked as a plugin") { | |||
| THEN("It's not a plugin") { | |||
| CHECK_FALSE(XmlReader::XmlElementIsPlugin(&e)); | |||
| } | |||
| } | |||
| WHEN("Checked as a gain stage") { | |||
| THEN("It's a gain stage") { | |||
| CHECK(XmlReader::XmlElementIsGainStage(&e)); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that is a plugin") { | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_SLOT_TYPE_STR, XML_SLOT_TYPE_PLUGIN_STR); | |||
| WHEN("Checked as a plugin") { | |||
| THEN("It's a plugin") { | |||
| CHECK(XmlReader::XmlElementIsPlugin(&e)); | |||
| } | |||
| } | |||
| WHEN("Checked as a gain stage") { | |||
| THEN("It's not a gain stage") { | |||
| CHECK_FALSE(XmlReader::XmlElementIsGainStage(&e)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore ChainSlotGainStage") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::AudioProcessor::BusesLayout layout; | |||
| juce::XmlElement e("test"); | |||
| WHEN("Asked to restore a ChainSlotGainStage from it") { | |||
| auto gainStage = XmlReader::restoreChainSlotGainStage(&e, layout); | |||
| THEN("A ChainSlotGainStage with default values is created") { | |||
| CHECK(gainStage->gain == 1.0f); | |||
| CHECK(gainStage->pan == 0.0f); | |||
| CHECK(gainStage->isBypassed == false); | |||
| CHECK(gainStage->numMainChannels == 0); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has attributes set correctly") { | |||
| juce::AudioProcessor::BusesLayout layout = GENERATE( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo())); | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_SLOT_TYPE_STR, XML_SLOT_TYPE_GAIN_STAGE_STR); | |||
| e.setAttribute(XML_GAIN_STAGE_GAIN_STR, 0.5f); | |||
| e.setAttribute(XML_GAIN_STAGE_PAN_STR, 0.6f); | |||
| e.setAttribute(XML_SLOT_IS_BYPASSED_STR, true); | |||
| WHEN("Asked to restore a ChainSlotGainStage from it") { | |||
| auto gainStage = XmlReader::restoreChainSlotGainStage(&e, layout); | |||
| THEN("A ChainSlotGainStage with the correct values is created") { | |||
| CHECK(gainStage->gain == 0.5f); | |||
| CHECK(gainStage->pan == 0.6f); | |||
| CHECK(gainStage->isBypassed == true); | |||
| CHECK(gainStage->numMainChannels == layout.getMainInputChannels()); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore ChainSlotPlugin") { | |||
| auto createDefaultTestData = []() { | |||
| return std::make_tuple< | |||
| std::function<float(int, MODULATION_TYPE)>, | |||
| HostConfiguration, | |||
| PluginConfigurator, | |||
| XmlReader::LoadPluginFunction, | |||
| std::function<void(juce::String)> | |||
| >( | |||
| [](int, MODULATION_TYPE) { return 0.0f; }, | |||
| HostConfiguration{TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), 60000, 100}, // Deliberately use values the code would never default to so we can check they are actually set correctly | |||
| PluginConfigurator(), | |||
| [](const juce::PluginDescription& description, const HostConfiguration& config) { | |||
| return std::make_tuple<std::unique_ptr<juce::AudioPluginInstance>, juce::String>( | |||
| std::make_unique<XMLTestPluginInstance>(description), "" | |||
| ); | |||
| }, | |||
| [](juce::String errorMsg) { } | |||
| ); | |||
| }; | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| WHEN("Asked to restore a ChainSlotPlugin from it") { | |||
| auto [modulationCallback, config, configurator, loadPlugin, onError] = | |||
| createDefaultTestData(); | |||
| auto slot = XmlReader::restoreChainSlotPlugin( | |||
| &e, modulationCallback, config, configurator, loadPlugin, onError); | |||
| THEN("A ChainSlotPlugin isn't created") { | |||
| CHECK(slot == nullptr); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has an empty description element") { | |||
| juce::XmlElement e("test"); | |||
| auto empty = e.createNewChildElement("empty"); | |||
| WHEN("Asked to restore a ChainSlotPlugin from it") { | |||
| auto [modulationCallback, config, configurator, loadPlugin, onError] = | |||
| createDefaultTestData(); | |||
| auto slot = XmlReader::restoreChainSlotPlugin( | |||
| &e, modulationCallback, config, configurator, loadPlugin, onError); | |||
| THEN("A ChainSlotPlugin isn't created") { | |||
| CHECK(slot == nullptr); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has a valid plugin description element") { | |||
| juce::XmlElement e("test"); | |||
| juce::PluginDescription description; | |||
| description.name = "testPlugin"; | |||
| auto descriptionElement = description.createXml(); | |||
| e.addChildElement(descriptionElement.release()); | |||
| WHEN("Asked to restore a ChainSlotPlugin from it but loadPlugin fails") { | |||
| auto [modulationCallback, config, configurator, loadPlugin, onError] = | |||
| createDefaultTestData(); | |||
| loadPlugin = [](const juce::PluginDescription& description, const HostConfiguration& config) { | |||
| return std::make_tuple<std::unique_ptr<juce::AudioPluginInstance>, juce::String>( | |||
| nullptr, "Test won't load plugin" | |||
| ); | |||
| }; | |||
| onError = [](juce::String errorMsg) { | |||
| CHECK(errorMsg == "Failed to restore plugin: Test won't load plugin"); | |||
| }; | |||
| auto slot = XmlReader::restoreChainSlotPlugin( | |||
| &e, modulationCallback, config, configurator, loadPlugin, onError); | |||
| THEN("A ChainSlotPlugin isn't created") { | |||
| CHECK(slot == nullptr); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has a valid plugin description element (that secretly signals to the test plugin to fail attempts to set layout)") { | |||
| juce::XmlElement e("test"); | |||
| juce::PluginDescription description; | |||
| description.name = "failLayout"; | |||
| auto descriptionElement = description.createXml(); | |||
| e.addChildElement(descriptionElement.release()); | |||
| WHEN("Asked to restore a ChainSlotPlugin from it but it can't find a valid layout") { | |||
| auto [modulationCallback, config, configurator, loadPlugin, onError] = | |||
| createDefaultTestData(); | |||
| onError = [](juce::String errorMsg) { | |||
| CHECK(errorMsg == "Failed to restore TestPlugin as it may be a mono only plugin being restored into a stereo instance of Syndicate or vice versa"); | |||
| }; | |||
| auto slot = XmlReader::restoreChainSlotPlugin( | |||
| &e, modulationCallback, config, configurator, loadPlugin, onError); | |||
| THEN("A ChainSlotPlugin isn't created") { | |||
| CHECK(slot == nullptr); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has all the correct data") { | |||
| const bool isBypassed = GENERATE(false, true); | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_SLOT_IS_BYPASSED_STR, isBypassed); | |||
| juce::PluginDescription description; | |||
| description.name = "TestPlugin"; | |||
| auto descriptionElement = description.createXml(); | |||
| e.addChildElement(descriptionElement.release()); | |||
| const juce::Rectangle<int> bounds(150, 200); | |||
| e.setAttribute(XML_PLUGIN_EDITOR_BOUNDS_STR, bounds.toString()); | |||
| const juce::Rectangle<int> displayArea(2000, 1000); | |||
| e.setAttribute(XML_DISPLAY_AREA_STR, displayArea.toString()); | |||
| const std::string testString("testPluginData"); | |||
| juce::MemoryBlock block(testString.size(), true); | |||
| block.copyFrom(testString.c_str(), 0, testString.size()); | |||
| e.setAttribute(XML_PLUGIN_DATA_STR, block.toBase64Encoding()); | |||
| auto configElement = e.createNewChildElement(XML_MODULATION_CONFIG_STR); | |||
| configElement->setAttribute(XML_MODULATION_IS_ACTIVE_STR, true); | |||
| auto parameterConfigElement1 = configElement->createNewChildElement("ParamConfig_0"); | |||
| parameterConfigElement1->setAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR, "testParam1"); | |||
| auto parameterConfigElement2 = configElement->createNewChildElement("ParamConfig_1"); | |||
| parameterConfigElement2->setAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR, "testParam2"); | |||
| WHEN("Asked to restore a ChainSlotPlugin from it") { | |||
| auto [modulationCallback, config, configurator, loadPlugin, onError] = | |||
| createDefaultTestData(); | |||
| auto slot = XmlReader::restoreChainSlotPlugin( | |||
| &e, modulationCallback, config, configurator, loadPlugin, onError); | |||
| THEN("A ChainSlotPlugin is created with the correct fields") { | |||
| REQUIRE(slot != nullptr); | |||
| CHECK(slot->isBypassed == isBypassed); | |||
| CHECK(slot->editorBounds->value().editorBounds == bounds); | |||
| CHECK(slot->editorBounds->value().displayArea == displayArea); | |||
| auto plugin = dynamic_cast<XMLTestPluginInstance*>(slot->plugin.get()); | |||
| REQUIRE(plugin != nullptr); | |||
| CHECK(plugin->retrievedData == testString); | |||
| CHECK(slot->plugin->getSampleRate() == 60000); | |||
| CHECK(slot->plugin->getBlockSize() == 100); | |||
| CHECK(slot->modulationConfig->isActive); | |||
| CHECK(slot->modulationConfig->parameterConfigs.size() == 2); | |||
| CHECK(slot->modulationConfig->parameterConfigs[0]->targetParameterName == "testParam1"); | |||
| CHECK(slot->modulationConfig->parameterConfigs[1]->targetParameterName == "testParam2"); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore PluginModulationConfig") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| WHEN("Asked to restore a PluginModulationConfig from it") { | |||
| auto config = XmlReader::restorePluginModulationConfig(&e); | |||
| THEN("A PluginModulationConfig with default values is created") { | |||
| CHECK_FALSE(config->isActive); | |||
| CHECK(config->parameterConfigs.size() == 0); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has attributes set correctly") { | |||
| const bool isActive = GENERATE(false, true); | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_MODULATION_IS_ACTIVE_STR, isActive); | |||
| auto config1 = e.createNewChildElement("ParamConfig_0"); | |||
| config1->setAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR, "testParam1"); | |||
| auto config2 = e.createNewChildElement("ParamConfig_1"); | |||
| config2->setAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR, "testParam2"); | |||
| WHEN("Asked to restore a PluginModulationConfig from it") { | |||
| auto config = XmlReader::restorePluginModulationConfig(&e); | |||
| THEN("A PluginModulationConfig with the correct values is created") { | |||
| CHECK(config->isActive == isActive); | |||
| CHECK(config->parameterConfigs.size() == 2); | |||
| CHECK(config->parameterConfigs[0]->targetParameterName == "testParam1"); | |||
| CHECK(config->parameterConfigs[1]->targetParameterName == "testParam2"); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore PluginParameterModulationConfig") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| WHEN("Asked to restore a PluginParameterModulationConfig from it") { | |||
| auto config = XmlReader::restorePluginParameterModulationConfig(&e); | |||
| THEN("A PluginParameterModulationConfig with default values is created") { | |||
| CHECK(config->targetParameterName == ""); | |||
| CHECK(config->restValue == 0); | |||
| CHECK(config->sources.size() == 0); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has attributes set correctly") { | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR, "testName"); | |||
| e.setAttribute(XML_MODULATION_REST_VALUE_STR, 0.5); | |||
| auto source1 = e.createNewChildElement("Source_0"); | |||
| source1->setAttribute(XML_MODULATION_SOURCE_AMOUNT, -0.5); | |||
| auto source2 = e.createNewChildElement("Source_1"); | |||
| source2->setAttribute(XML_MODULATION_SOURCE_AMOUNT, 0.5); | |||
| WHEN("Asked to restore a PluginParameterModulationConfig from it") { | |||
| auto config = XmlReader::restorePluginParameterModulationConfig(&e); | |||
| THEN("A PluginParameterModulationConfig with the correct values is created") { | |||
| CHECK(config->targetParameterName == "testName"); | |||
| CHECK(config->restValue == 0.5); | |||
| CHECK(config->sources.size() == 2); | |||
| CHECK(config->sources[0]->modulationAmount == -0.5f); | |||
| CHECK(config->sources[1]->modulationAmount == 0.5f); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore PluginParameterModulationSource") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| WHEN("Asked to restore a PluginParameterModulationSource from it") { | |||
| auto source = XmlReader::restorePluginParameterModulationSource(&e); | |||
| THEN("A PluginParameterModulationSource with default values is created") { | |||
| CHECK(source->definition == ModulationSourceDefinition(0, MODULATION_TYPE::MACRO)); | |||
| CHECK(source->modulationAmount == 0.0f); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has attributes set correctly") { | |||
| const float amount = GENERATE(-1, 0.5, 1); | |||
| juce::XmlElement e("test"); | |||
| e.setAttribute(XML_MODULATION_SOURCE_ID, 1); | |||
| e.setAttribute(XML_MODULATION_SOURCE_TYPE, "lfo"); | |||
| e.setAttribute(XML_MODULATION_SOURCE_AMOUNT, amount); | |||
| WHEN("Asked to restore a PluginParameterModulationSource from it") { | |||
| auto source = XmlReader::restorePluginParameterModulationSource(&e); | |||
| THEN("A PluginParameterModulationSource with the correct values is created") { | |||
| CHECK(source->definition == ModulationSourceDefinition(1, MODULATION_TYPE::LFO)); | |||
| CHECK(source->modulationAmount == amount); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore ModulationSourceDefinition") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| WHEN("Asked to restore a ModulationSourceDefinition from it") { | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(&e); | |||
| THEN("Nothing is changed") { | |||
| CHECK(definition.id == 0); | |||
| CHECK(definition.type == MODULATION_TYPE::MACRO); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement that has attributes set correctly") { | |||
| juce::XmlElement e("test"); | |||
| const int modulationId = GENERATE(1, 2, 3); | |||
| auto [modulationType, modulationTypeString] = GENERATE( | |||
| std::pair<MODULATION_TYPE, juce::String>(MODULATION_TYPE::MACRO, "macro"), | |||
| std::pair<MODULATION_TYPE, juce::String>(MODULATION_TYPE::LFO, "lfo"), | |||
| std::pair<MODULATION_TYPE, juce::String>(MODULATION_TYPE::ENVELOPE, "envelope"), | |||
| std::pair<MODULATION_TYPE, juce::String>(MODULATION_TYPE::RANDOM, "random") | |||
| ); | |||
| e.setAttribute(XML_MODULATION_SOURCE_ID, modulationId); | |||
| e.setAttribute(XML_MODULATION_SOURCE_TYPE, modulationTypeString); | |||
| WHEN("Asked to restore a PluginParameterModulationSource from it") { | |||
| ModulationSourceDefinition definition(0, MODULATION_TYPE::MACRO); | |||
| definition.restoreFromXml(&e); | |||
| THEN("A PluginParameterModulationSource with the correct values is created") { | |||
| CHECK(definition.id == modulationId); | |||
| CHECK(definition.type == modulationType); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlReader: Can restore ModulationSourcesState") { | |||
| GIVEN("An XmlElement that has no attributes") { | |||
| juce::XmlElement e("test"); | |||
| HostConfiguration config; | |||
| config.sampleRate = 44100; | |||
| config.blockSize = 64; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| WHEN("Asked to restore a ModulationSourcesState from it") { | |||
| ModelInterface::ModulationSourcesState state(modulationCallback); | |||
| XmlReader::restoreModulationSourcesFromXml(state, &e, config); | |||
| THEN("Nothing is restored") { | |||
| CHECK(state.lfos.size() == 0); | |||
| CHECK(state.envelopes.size() == 0); | |||
| CHECK(state.randomSources.size() == 0); | |||
| } | |||
| } | |||
| } | |||
| GIVEN("An XmlElement with an LFO and an envelope follower") { | |||
| juce::XmlElement e("test"); | |||
| HostConfiguration config; | |||
| config.sampleRate = 44100; | |||
| config.blockSize = 64; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| // LFOs | |||
| { | |||
| auto lfosElement = e.createNewChildElement(XML_LFOS_STR); | |||
| auto thisLfoElement = lfosElement->createNewChildElement("LFO_0"); | |||
| thisLfoElement->setAttribute(XML_LFO_BYPASS_STR, true); | |||
| thisLfoElement->setAttribute(XML_LFO_PHASE_SYNC_STR, true); | |||
| thisLfoElement->setAttribute(XML_LFO_TEMPO_SYNC_STR, true); | |||
| thisLfoElement->setAttribute(XML_LFO_INVERT_STR, true); | |||
| thisLfoElement->setAttribute(XML_LFO_WAVE_STR, 2); | |||
| thisLfoElement->setAttribute(XML_LFO_TEMPO_NUMER_STR, 3); | |||
| thisLfoElement->setAttribute(XML_LFO_TEMPO_DENOM_STR, 4); | |||
| thisLfoElement->setAttribute(XML_LFO_FREQ_STR, 0.5); | |||
| thisLfoElement->setAttribute(XML_LFO_DEPTH_STR, 0.6); | |||
| thisLfoElement->setAttribute(XML_LFO_MANUAL_PHASE_STR, 100); | |||
| thisLfoElement->setAttribute(XML_LFO_OUTPUT_MODE_STR, WECore::Richter::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| juce::XmlElement* freqModSourcesElement = thisLfoElement->createNewChildElement(XML_LFO_FREQ_MODULATION_SOURCES_STR); | |||
| juce::XmlElement* thisFreqSourceElement = freqModSourcesElement->createNewChildElement("Source_0"); | |||
| thisFreqSourceElement->setAttribute(XML_MODULATION_SOURCE_ID, 2); | |||
| thisFreqSourceElement->setAttribute(XML_MODULATION_SOURCE_TYPE, "lfo"); | |||
| thisFreqSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, 0.3); | |||
| juce::XmlElement* depthModSourcesElement = thisLfoElement->createNewChildElement(XML_LFO_DEPTH_MODULATION_SOURCES_STR); | |||
| juce::XmlElement* thisDepthSourceElement = depthModSourcesElement->createNewChildElement("Source_0"); | |||
| thisDepthSourceElement->setAttribute(XML_MODULATION_SOURCE_ID, 3); | |||
| thisDepthSourceElement->setAttribute(XML_MODULATION_SOURCE_TYPE, "envelope"); | |||
| thisDepthSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, 0.4); | |||
| juce::XmlElement* phaseModsourcesElement = thisLfoElement->createNewChildElement(XML_LFO_PHASE_MODULATION_SOURCES_STR); | |||
| juce::XmlElement* thisPhaseSourceElement = phaseModsourcesElement->createNewChildElement("Source_0"); | |||
| thisPhaseSourceElement->setAttribute(XML_MODULATION_SOURCE_ID, 4); | |||
| thisPhaseSourceElement->setAttribute(XML_MODULATION_SOURCE_TYPE, "macro"); | |||
| thisPhaseSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, 0.5); | |||
| } | |||
| // Envelopes | |||
| { | |||
| auto envelopesElement = e.createNewChildElement(XML_ENVELOPES_STR); | |||
| auto thisEnvelopeElement = envelopesElement->createNewChildElement("Envelope_0"); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_ATTACK_TIME_STR, 0.1); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_RELEASE_TIME_STR, 0.2); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_FILTER_ENABLED_STR, true); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_LOW_CUT_STR, 100); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_HIGH_CUT_STR, 200); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_AMOUNT_STR, 0.3); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_USE_SIDECHAIN_INPUT_STR, true); | |||
| } | |||
| // Randoms | |||
| { | |||
| auto randomsElement = e.createNewChildElement(XML_RANDOMS_STR); | |||
| auto thisRandomElement = randomsElement->createNewChildElement("Random_0"); | |||
| thisRandomElement->setAttribute(XML_RANDOM_OUTPUT_MODE_STR, WECore::Perlin::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| thisRandomElement->setAttribute(XML_RANDOM_FREQ_STR, 0.1); | |||
| thisRandomElement->setAttribute(XML_RANDOM_DEPTH_STR, 0.2); | |||
| juce::XmlElement* freqModSourcesElement = thisRandomElement->createNewChildElement(XML_RANDOM_FREQ_MODULATION_SOURCES_STR); | |||
| juce::XmlElement* thisFreqSourceElement = freqModSourcesElement->createNewChildElement("Source_0"); | |||
| thisFreqSourceElement->setAttribute(XML_MODULATION_SOURCE_ID, 5); | |||
| thisFreqSourceElement->setAttribute(XML_MODULATION_SOURCE_TYPE, "envelope"); | |||
| thisFreqSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, 0.6); | |||
| juce::XmlElement* depthModSourcesElement = thisRandomElement->createNewChildElement(XML_RANDOM_DEPTH_MODULATION_SOURCES_STR); | |||
| juce::XmlElement* thisDepthSourceElement = depthModSourcesElement->createNewChildElement("Source_0"); | |||
| thisDepthSourceElement->setAttribute(XML_MODULATION_SOURCE_ID, 6); | |||
| thisDepthSourceElement->setAttribute(XML_MODULATION_SOURCE_TYPE, "macro"); | |||
| thisDepthSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, 0.7); | |||
| } | |||
| WHEN("Asked to restore a ModulationSourcesState from it") { | |||
| auto state = std::make_shared<ModelInterface::ModulationSourcesState>(modulationCallback); | |||
| XmlReader::restoreModulationSourcesFromXml(*state.get(), &e, config); | |||
| THEN("The LFO and envelope are restored") { | |||
| // LFOs | |||
| { | |||
| REQUIRE(state->lfos.size() == 1); | |||
| auto lfo = state->lfos[0]; | |||
| CHECK(lfo->getBypassSwitch() == true); | |||
| CHECK(lfo->getPhaseSyncSwitch() == true); | |||
| CHECK(lfo->getTempoSyncSwitch() == true); | |||
| CHECK(lfo->getInvertSwitch() == true); | |||
| CHECK(lfo->getWave() == 2); | |||
| CHECK(lfo->getTempoNumer() == 3); | |||
| CHECK(lfo->getTempoDenom() == 4); | |||
| CHECK(lfo->getFreq() == 0.5); | |||
| CHECK(lfo->getDepth() == 0.6); | |||
| CHECK(lfo->getManualPhase() == 100); | |||
| CHECK(lfo->getOutputMode() == WECore::Richter::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> freqModSources = ModulationMutators::getLFOFreqModulationSources(state, 0); | |||
| REQUIRE(freqModSources.size() == 1); | |||
| CHECK(freqModSources[0]->definition == ModulationSourceDefinition(2, MODULATION_TYPE::LFO)); | |||
| CHECK(freqModSources[0]->modulationAmount == 0.3f); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> depthModSources = ModulationMutators::getLFODepthModulationSources(state, 0); | |||
| REQUIRE(depthModSources.size() == 1); | |||
| CHECK(depthModSources[0]->definition == ModulationSourceDefinition(3, MODULATION_TYPE::ENVELOPE)); | |||
| CHECK(depthModSources[0]->modulationAmount == 0.4f); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> phaseModSources = ModulationMutators::getLFOPhaseModulationSources(state, 0); | |||
| REQUIRE(phaseModSources.size() == 1); | |||
| CHECK(phaseModSources[0]->definition == ModulationSourceDefinition(4, MODULATION_TYPE::MACRO)); | |||
| CHECK(phaseModSources[0]->modulationAmount == 0.5f); | |||
| } | |||
| // Envelopes | |||
| { | |||
| REQUIRE(state->envelopes.size() == 1); | |||
| auto envelope = state->envelopes[0]; | |||
| CHECK(envelope->envelope->getAttackTimeMs() == 0.1); | |||
| CHECK(envelope->envelope->getReleaseTimeMs() == 0.2); | |||
| CHECK(envelope->envelope->getFilterEnabled() == true); | |||
| CHECK(envelope->envelope->getLowCutHz() == 100); | |||
| CHECK(envelope->envelope->getHighCutHz() == 200); | |||
| CHECK(envelope->amount == 0.3f); | |||
| CHECK(envelope->useSidechainInput == true); | |||
| } | |||
| // Randoms | |||
| { | |||
| REQUIRE(state->randomSources.size() == 1); | |||
| auto random = state->randomSources[0]; | |||
| CHECK(random->getOutputMode() == WECore::Perlin::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| CHECK(random->getFreq() == 0.1); | |||
| CHECK(random->getDepth() == 0.2); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> freqModSources = ModulationMutators::getRandomFreqModulationSources(state, 0); | |||
| REQUIRE(freqModSources.size() == 1); | |||
| CHECK(freqModSources[0]->definition == ModulationSourceDefinition(5, MODULATION_TYPE::ENVELOPE)); | |||
| CHECK(freqModSources[0]->modulationAmount == 0.6f); | |||
| std::vector<std::shared_ptr<PluginParameterModulationSource>> depthModSources = ModulationMutators::getRandomDepthModulationSources(state, 0); | |||
| REQUIRE(depthModSources.size() == 1); | |||
| CHECK(depthModSources[0]->definition == ModulationSourceDefinition(6, MODULATION_TYPE::MACRO)); | |||
| CHECK(depthModSources[0]->modulationAmount == 0.7f); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,245 @@ | |||
| #include "XmlWriter.hpp" | |||
| #include "XmlConsts.hpp" | |||
| #include "SplitterMutators.hpp" | |||
| #include "ModulationMutators.hpp" | |||
| namespace XmlWriter { | |||
| void write(std::shared_ptr<PluginSplitter> splitter, juce::XmlElement* element) { | |||
| juce::Logger::writeToLog("Storing splitter state"); | |||
| const char* splitTypeString = XML_SPLIT_TYPE_SERIES_STR; | |||
| if (auto seriesSplitter = std::dynamic_pointer_cast<PluginSplitterSeries>(splitter)) { | |||
| splitTypeString = XML_SPLIT_TYPE_SERIES_STR; | |||
| } else if (auto parallelSplitter = std::dynamic_pointer_cast<PluginSplitterParallel>(splitter)) { | |||
| splitTypeString = XML_SPLIT_TYPE_PARALLEL_STR; | |||
| } else if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| splitTypeString = XML_SPLIT_TYPE_MULTIBAND_STR; | |||
| } else if (auto leftRightSplitter = std::dynamic_pointer_cast<PluginSplitterLeftRight>(splitter)) { | |||
| splitTypeString = XML_SPLIT_TYPE_LEFTRIGHT_STR; | |||
| } else if (auto midSideSplitter = std::dynamic_pointer_cast<PluginSplitterMidSide>(splitter)) { | |||
| splitTypeString = XML_SPLIT_TYPE_MIDSIDE_STR; | |||
| } | |||
| element->setAttribute(XML_SPLIT_TYPE_STR, splitTypeString); | |||
| juce::XmlElement* chainsElement = element->createNewChildElement(XML_CHAINS_STR); | |||
| for (int chainNumber {0}; chainNumber < splitter->chains.size(); chainNumber++) { | |||
| juce::Logger::writeToLog("Storing chain " + juce::String(chainNumber)); | |||
| juce::XmlElement* thisChainElement = chainsElement->createNewChildElement(getChainXMLName(chainNumber)); | |||
| PluginChainWrapper& thisChain = splitter->chains[chainNumber]; | |||
| thisChainElement->setAttribute(XML_IS_CHAIN_SOLOED_STR, thisChain.isSoloed); | |||
| XmlWriter::write(thisChain.chain, thisChainElement); | |||
| } | |||
| if (auto multibandSplitter = std::dynamic_pointer_cast<PluginSplitterMultiband>(splitter)) { | |||
| // Store the crossover frequencies | |||
| juce::XmlElement* crossoversElement = element->createNewChildElement(XML_CROSSOVERS_STR); | |||
| for (int crossoverNumber {0}; crossoverNumber < multibandSplitter->chains.size() - 1; crossoverNumber++) { | |||
| crossoversElement->setAttribute(getCrossoverXMLName(crossoverNumber), SplitterMutators::getCrossoverFrequency(multibandSplitter, crossoverNumber)); | |||
| } | |||
| } | |||
| } | |||
| void write(std::shared_ptr<PluginChain> chain, juce::XmlElement* element) { | |||
| // Store chain level bypass, mute, and name | |||
| element->setAttribute(XML_IS_CHAIN_BYPASSED_STR, chain->isChainBypassed); | |||
| element->setAttribute(XML_IS_CHAIN_MUTED_STR, chain->isChainMuted); | |||
| element->setAttribute(XML_CHAIN_CUSTOM_NAME_STR, chain->customName); | |||
| // Store each plugin | |||
| juce::XmlElement* pluginsElement = element->createNewChildElement(XML_PLUGINS_STR); | |||
| for (int pluginNumber {0}; pluginNumber < chain->chain.size(); pluginNumber++) { | |||
| juce::Logger::writeToLog("Storing plugin " + juce::String(pluginNumber)); | |||
| juce::XmlElement* thisPluginElement = pluginsElement->createNewChildElement(getSlotXMLName(pluginNumber)); | |||
| if (auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(chain->chain[pluginNumber])) { | |||
| XmlWriter::write(gainStage, thisPluginElement); | |||
| } else if (auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(chain->chain[pluginNumber])) { | |||
| XmlWriter::write(pluginSlot, thisPluginElement); | |||
| } | |||
| } | |||
| } | |||
| void write(std::shared_ptr<ChainSlotGainStage> gainStage, juce::XmlElement* element) { | |||
| element->setAttribute(XML_SLOT_TYPE_STR, XML_SLOT_TYPE_GAIN_STAGE_STR); | |||
| element->setAttribute(XML_SLOT_IS_BYPASSED_STR, gainStage->isBypassed); | |||
| element->setAttribute(XML_GAIN_STAGE_GAIN_STR, gainStage->gain); | |||
| element->setAttribute(XML_GAIN_STAGE_PAN_STR, gainStage->pan); | |||
| } | |||
| void write(std::shared_ptr<ChainSlotPlugin> chainSlot, juce::XmlElement* element) { | |||
| element->setAttribute(XML_SLOT_TYPE_STR, XML_SLOT_TYPE_PLUGIN_STR); | |||
| // Store the plugin level bypass | |||
| element->setAttribute(XML_SLOT_IS_BYPASSED_STR, chainSlot->isBypassed); | |||
| // Store the plugin description | |||
| std::unique_ptr<juce::XmlElement> pluginDescriptionXml = | |||
| chainSlot->plugin->getPluginDescription().createXml(); | |||
| element->addChildElement(pluginDescriptionXml.release()); | |||
| // Store the plugin's internal state | |||
| juce::MemoryBlock pluginMemoryBlock; | |||
| chainSlot->plugin->getStateInformation(pluginMemoryBlock); | |||
| element->setAttribute(XML_PLUGIN_DATA_STR, pluginMemoryBlock.toBase64Encoding()); | |||
| // Store the modulation config | |||
| juce::XmlElement* modulationConfigElement = element->createNewChildElement(XML_MODULATION_CONFIG_STR); | |||
| write(chainSlot->modulationConfig, modulationConfigElement); | |||
| // Store the editor bounds | |||
| if (chainSlot->editorBounds->has_value()) { | |||
| element->setAttribute(XML_PLUGIN_EDITOR_BOUNDS_STR, chainSlot->editorBounds->value().editorBounds.toString()); | |||
| element->setAttribute(XML_DISPLAY_AREA_STR, chainSlot->editorBounds->value().displayArea.toString()); | |||
| } | |||
| } | |||
| void write(std::shared_ptr<PluginModulationConfig> config, juce::XmlElement* element) { | |||
| element->setAttribute(XML_MODULATION_IS_ACTIVE_STR, config->isActive); | |||
| for (int index {0}; index < config->parameterConfigs.size(); index++) { | |||
| juce::XmlElement* thisParameterConfigElement = | |||
| element->createNewChildElement(getParameterModulationConfigXmlName(index)); | |||
| write(config->parameterConfigs[index], thisParameterConfigElement); | |||
| } | |||
| } | |||
| void write(std::shared_ptr<PluginParameterModulationConfig> config, juce::XmlElement* element) { | |||
| element->setAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR, config->targetParameterName); | |||
| element->setAttribute(XML_MODULATION_REST_VALUE_STR, config->restValue); | |||
| for (int index {0}; index < config->sources.size(); index++ ) { | |||
| juce::XmlElement* thisSourceElement = | |||
| element->createNewChildElement(getParameterModulationSourceXmlName(index)); | |||
| write(config->sources[index], thisSourceElement); | |||
| } | |||
| } | |||
| void write(std::shared_ptr<PluginParameterModulationSource> source, juce::XmlElement* element) { | |||
| source->definition.writeToXml(element); | |||
| element->setAttribute(XML_MODULATION_SOURCE_AMOUNT, source->modulationAmount); | |||
| } | |||
| void write(ModelInterface::ModulationSourcesState& state, juce::XmlElement* element) { | |||
| // LFOs | |||
| juce::XmlElement* lfosElement = element->createNewChildElement(XML_LFOS_STR); | |||
| for (int index {0}; index < state.lfos.size(); index++) { | |||
| juce::XmlElement* thisLfoElement = lfosElement->createNewChildElement(getLfoXMLName(index)); | |||
| std::shared_ptr<WECore::Richter::RichterLFO> thisLfo = state.lfos[index]; | |||
| thisLfoElement->setAttribute(XML_LFO_BYPASS_STR, thisLfo->getBypassSwitch()); | |||
| thisLfoElement->setAttribute(XML_LFO_PHASE_SYNC_STR, thisLfo->getPhaseSyncSwitch()); | |||
| thisLfoElement->setAttribute(XML_LFO_TEMPO_SYNC_STR, thisLfo->getTempoSyncSwitch()); | |||
| thisLfoElement->setAttribute(XML_LFO_INVERT_STR, thisLfo->getInvertSwitch()); | |||
| thisLfoElement->setAttribute(XML_LFO_OUTPUT_MODE_STR, thisLfo->getOutputMode()); | |||
| thisLfoElement->setAttribute(XML_LFO_WAVE_STR, thisLfo->getWave()); | |||
| thisLfoElement->setAttribute(XML_LFO_TEMPO_NUMER_STR, thisLfo->getTempoNumer()); | |||
| thisLfoElement->setAttribute(XML_LFO_TEMPO_DENOM_STR, thisLfo->getTempoDenom()); | |||
| thisLfoElement->setAttribute(XML_LFO_FREQ_STR, thisLfo->getFreq()); | |||
| thisLfoElement->setAttribute(XML_LFO_DEPTH_STR, thisLfo->getDepth()); | |||
| thisLfoElement->setAttribute(XML_LFO_MANUAL_PHASE_STR, thisLfo->getManualPhase()); | |||
| // Freq modulation sources | |||
| juce::XmlElement* freqModSourcesElement = thisLfoElement->createNewChildElement(XML_LFO_FREQ_MODULATION_SOURCES_STR); | |||
| const std::vector<WECore::ModulationSourceWrapper<double>> freqModSources = thisLfo->getFreqModulationSources(); | |||
| for (int sourceIndex {0}; sourceIndex < freqModSources.size(); sourceIndex++) { | |||
| juce::XmlElement* thisSourceElement = freqModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(freqModSources[sourceIndex].source); | |||
| if (thisSource != nullptr) { | |||
| thisSource->definition.writeToXml(thisSourceElement); | |||
| thisSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, freqModSources[sourceIndex].amount); | |||
| } | |||
| } | |||
| // Depth modulation sources | |||
| juce::XmlElement* depthModSourcesElement = thisLfoElement->createNewChildElement(XML_LFO_DEPTH_MODULATION_SOURCES_STR); | |||
| const std::vector<WECore::ModulationSourceWrapper<double>> depthModSources = thisLfo->getDepthModulationSources(); | |||
| for (int sourceIndex {0}; sourceIndex < depthModSources.size(); sourceIndex++) { | |||
| juce::XmlElement* thisSourceElement = depthModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(depthModSources[sourceIndex].source); | |||
| if (thisSource != nullptr) { | |||
| thisSource->definition.writeToXml(thisSourceElement); | |||
| thisSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, depthModSources[sourceIndex].amount); | |||
| } | |||
| } | |||
| // Phase modulation sources | |||
| juce::XmlElement* phaseModSourcesElement = thisLfoElement->createNewChildElement(XML_LFO_PHASE_MODULATION_SOURCES_STR); | |||
| const std::vector<WECore::ModulationSourceWrapper<double>> phaseModSources = thisLfo->getPhaseModulationSources(); | |||
| for (int sourceIndex {0}; sourceIndex < phaseModSources.size(); sourceIndex++) { | |||
| juce::XmlElement* thisSourceElement = phaseModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(phaseModSources[sourceIndex].source); | |||
| if (thisSource != nullptr) { | |||
| thisSource->definition.writeToXml(thisSourceElement); | |||
| thisSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, phaseModSources[sourceIndex].amount); | |||
| } | |||
| } | |||
| } | |||
| // Envelopes | |||
| juce::XmlElement* envelopesElement = element->createNewChildElement(XML_ENVELOPES_STR); | |||
| for (int index {0}; index < state.envelopes.size(); index++) { | |||
| juce::XmlElement* thisEnvelopeElement = envelopesElement->createNewChildElement(getEnvelopeXMLName(index)); | |||
| std::shared_ptr<ModelInterface::EnvelopeWrapper> thisEnvelope = state.envelopes[index]; | |||
| thisEnvelopeElement->setAttribute(XML_ENV_ATTACK_TIME_STR, thisEnvelope->envelope->getAttackTimeMs()); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_RELEASE_TIME_STR, thisEnvelope->envelope->getReleaseTimeMs()); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_FILTER_ENABLED_STR, thisEnvelope->envelope->getFilterEnabled()); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_LOW_CUT_STR, thisEnvelope->envelope->getLowCutHz()); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_HIGH_CUT_STR, thisEnvelope->envelope->getHighCutHz()); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_AMOUNT_STR, thisEnvelope->amount); | |||
| thisEnvelopeElement->setAttribute(XML_ENV_USE_SIDECHAIN_INPUT_STR, thisEnvelope->useSidechainInput); | |||
| } | |||
| // Random | |||
| juce::XmlElement* randomsElement = element->createNewChildElement(XML_RANDOMS_STR); | |||
| for (int index {0}; index < state.randomSources.size(); index++) { | |||
| juce::XmlElement* thisRandomElement = randomsElement->createNewChildElement(getRandomXMLName(index)); | |||
| std::shared_ptr<WECore::Perlin::PerlinSource> thisRandom = state.randomSources[index]; | |||
| thisRandomElement->setAttribute(XML_RANDOM_OUTPUT_MODE_STR, thisRandom->getOutputMode()); | |||
| thisRandomElement->setAttribute(XML_RANDOM_FREQ_STR, thisRandom->getFreq()); | |||
| thisRandomElement->setAttribute(XML_RANDOM_DEPTH_STR, thisRandom->getDepth()); | |||
| // Freq modulation sources | |||
| juce::XmlElement* freqModSourcesElement = thisRandomElement->createNewChildElement(XML_RANDOM_FREQ_MODULATION_SOURCES_STR); | |||
| const std::vector<WECore::ModulationSourceWrapper<double>> freqModSources = thisRandom->getFreqModulationSources(); | |||
| for (int sourceIndex {0}; sourceIndex < freqModSources.size(); sourceIndex++) { | |||
| juce::XmlElement* thisSourceElement = freqModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(freqModSources[sourceIndex].source); | |||
| if (thisSource != nullptr) { | |||
| thisSource->definition.writeToXml(thisSourceElement); | |||
| thisSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, freqModSources[sourceIndex].amount); | |||
| } | |||
| } | |||
| // Depth modulation sources | |||
| juce::XmlElement* depthModSourcesElement = thisRandomElement->createNewChildElement(XML_RANDOM_DEPTH_MODULATION_SOURCES_STR); | |||
| const std::vector<WECore::ModulationSourceWrapper<double>> depthModSources = thisRandom->getDepthModulationSources(); | |||
| for (int sourceIndex {0}; sourceIndex < depthModSources.size(); sourceIndex++) { | |||
| juce::XmlElement* thisSourceElement = depthModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); | |||
| auto thisSource = std::dynamic_pointer_cast<ModulationSourceProvider>(depthModSources[sourceIndex].source); | |||
| if (thisSource != nullptr) { | |||
| thisSource->definition.writeToXml(thisSourceElement); | |||
| thisSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, depthModSources[sourceIndex].amount); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "ChainSlots.hpp" | |||
| #include "PluginChain.hpp" | |||
| #include "PluginSplitter.hpp" | |||
| #include "DataModelInterface.hpp" | |||
| // TODO lock on entry so UI can't make changes | |||
| namespace XmlWriter { | |||
| void write(std::shared_ptr<PluginSplitter> splitter, juce::XmlElement* element); | |||
| void write(std::shared_ptr<PluginChain> chain, juce::XmlElement* element); | |||
| void write(std::shared_ptr<ChainSlotGainStage> gainStage, juce::XmlElement* element); | |||
| void write(std::shared_ptr<ChainSlotPlugin> chainSlot, juce::XmlElement* element); | |||
| void write(std::shared_ptr<PluginModulationConfig> config, juce::XmlElement* element); | |||
| void write(std::shared_ptr<PluginParameterModulationConfig> config, juce::XmlElement* element); | |||
| void write(std::shared_ptr<PluginParameterModulationSource> source, juce::XmlElement* element); | |||
| void write(ModelInterface::ModulationSourcesState& state, juce::XmlElement* element); | |||
| } | |||
| @@ -0,0 +1,643 @@ | |||
| #include "catch.hpp" | |||
| #include "XmlWriter.hpp" | |||
| #include "XmlConsts.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "SplitterMutators.hpp" | |||
| #include "ModulationMutators.hpp" | |||
| namespace { | |||
| // Use values that would never be sensible defaults so we know if they were actually set | |||
| constexpr int NUM_SAMPLES {10}; | |||
| constexpr int SAMPLE_RATE {2000}; | |||
| class XMLTestPluginInstance : public TestUtils::TestPluginInstance { | |||
| public: | |||
| XMLTestPluginInstance() = default; | |||
| void getStateInformation(juce::MemoryBlock& destData) override { | |||
| const std::string testString("testPluginData"); | |||
| destData.append(testString.c_str(), testString.size()); | |||
| } | |||
| }; | |||
| } | |||
| SCENARIO("XmlWriter: Can write PluginSplitter") { | |||
| GIVEN("A PluginSplitterParallel with 3 chains") { | |||
| HostConfiguration config; | |||
| config.sampleRate = SAMPLE_RATE; | |||
| config.blockSize = NUM_SAMPLES; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| auto latencyCallback = [](int) { | |||
| // Do nothing | |||
| }; | |||
| auto splitterParallel = std::make_shared<PluginSplitterParallel>(config, modulationCallback, latencyCallback); | |||
| SplitterMutators::addChain(splitterParallel); | |||
| SplitterMutators::addChain(splitterParallel); | |||
| auto splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterParallel); | |||
| // Chain 0 | |||
| SplitterMutators::insertPlugin(splitter, std::make_shared<XMLTestPluginInstance>(), 0, 0); | |||
| // Chain 1 | |||
| SplitterMutators::insertGainStage(splitter, 1, 0); | |||
| SplitterMutators::insertPlugin(splitter, std::make_shared<XMLTestPluginInstance>(), 1, 1); | |||
| SplitterMutators::insertGainStage(splitter, 1, 2); | |||
| // Chain 2 | |||
| SplitterMutators::insertGainStage(splitter, 2, 0); | |||
| SplitterMutators::setChainSolo(splitter, 2, true); | |||
| WHEN("Asked to write it to XML") { | |||
| // WHEN("The splitter is unmodified") | |||
| { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(splitter, &e); | |||
| // THEN("An XmlElement with the correct attributes is created") | |||
| CHECK(e.getStringAttribute(XML_SPLIT_TYPE_STR) == XML_SPLIT_TYPE_PARALLEL_STR); | |||
| juce::XmlElement* chainsElement = e.getChildByName(XML_CHAINS_STR); | |||
| REQUIRE(chainsElement != nullptr); | |||
| CHECK(chainsElement->getNumChildElements() == 3); | |||
| juce::XmlElement* chain0Element = chainsElement->getChildByName(getChainXMLName(0)); | |||
| CHECK(chain0Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain0Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain1Element = chainsElement->getChildByName(getChainXMLName(1)); | |||
| CHECK(chain1Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 3); | |||
| CHECK(chain1Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain2Element = chainsElement->getChildByName(getChainXMLName(2)); | |||
| CHECK(chain2Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain2Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == true); | |||
| } | |||
| splitterParallel.reset(); // Still in scope so reset it | |||
| // WHEN("The splitter is changed to mid/side") | |||
| { | |||
| auto splitterMidSide = std::make_shared<PluginSplitterMidSide>(splitter); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterMidSide); | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(splitter, &e); | |||
| // THEN("An XmlElement with the same attributes is created apart from the type") | |||
| CHECK(e.getStringAttribute(XML_SPLIT_TYPE_STR) == XML_SPLIT_TYPE_MIDSIDE_STR); | |||
| juce::XmlElement* chainsElement = e.getChildByName(XML_CHAINS_STR); | |||
| REQUIRE(chainsElement != nullptr); | |||
| CHECK(chainsElement->getNumChildElements() == 3); | |||
| juce::XmlElement* chain0Element = chainsElement->getChildByName(getChainXMLName(0)); | |||
| CHECK(chain0Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain0Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain1Element = chainsElement->getChildByName(getChainXMLName(1)); | |||
| CHECK(chain1Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 3); | |||
| CHECK(chain1Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain2Element = chainsElement->getChildByName(getChainXMLName(2)); | |||
| CHECK(chain2Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain2Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == true); | |||
| } | |||
| // WHEN("The splitter is changed to left/right") | |||
| { | |||
| auto splitterLeftRight = std::make_shared<PluginSplitterLeftRight>(splitter); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterLeftRight); | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(splitter, &e); | |||
| // THEN("An XmlElement with the same attributes is created apart from the type") | |||
| CHECK(e.getStringAttribute(XML_SPLIT_TYPE_STR) == XML_SPLIT_TYPE_LEFTRIGHT_STR); | |||
| juce::XmlElement* chainsElement = e.getChildByName(XML_CHAINS_STR); | |||
| REQUIRE(chainsElement != nullptr); | |||
| CHECK(chainsElement->getNumChildElements() == 3); | |||
| juce::XmlElement* chain0Element = chainsElement->getChildByName(getChainXMLName(0)); | |||
| CHECK(chain0Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain0Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain1Element = chainsElement->getChildByName(getChainXMLName(1)); | |||
| CHECK(chain1Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 3); | |||
| CHECK(chain1Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain2Element = chainsElement->getChildByName(getChainXMLName(2)); | |||
| CHECK(chain2Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain2Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == true); | |||
| } | |||
| // WHEN("The splitter is changed to multiband") | |||
| { | |||
| auto splitterMultiband = std::make_shared<PluginSplitterMultiband>(splitter, std::optional<std::vector<float>>()); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterMultiband); | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(splitter, &e); | |||
| // THEN("An XmlElement with the same attributes is created apart from the type") | |||
| CHECK(e.getStringAttribute(XML_SPLIT_TYPE_STR) == XML_SPLIT_TYPE_MULTIBAND_STR); | |||
| juce::XmlElement* chainsElement = e.getChildByName(XML_CHAINS_STR); | |||
| REQUIRE(chainsElement != nullptr); | |||
| CHECK(chainsElement->getNumChildElements() == 3); | |||
| juce::XmlElement* chain0Element = chainsElement->getChildByName(getChainXMLName(0)); | |||
| CHECK(chain0Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain0Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain1Element = chainsElement->getChildByName(getChainXMLName(1)); | |||
| CHECK(chain1Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 3); | |||
| CHECK(chain1Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain2Element = chainsElement->getChildByName(getChainXMLName(2)); | |||
| CHECK(chain2Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain2Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == true); | |||
| } | |||
| // WHEN("The splitter is changed to series") | |||
| { | |||
| auto splitterSeries = std::make_shared<PluginSplitterSeries>(splitter); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterSeries); | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(splitter, &e); | |||
| // THEN("An XmlElement with the same attributes is created apart from the type") | |||
| CHECK(e.getStringAttribute(XML_SPLIT_TYPE_STR) == XML_SPLIT_TYPE_SERIES_STR); | |||
| juce::XmlElement* chainsElement = e.getChildByName(XML_CHAINS_STR); | |||
| REQUIRE(chainsElement != nullptr); | |||
| CHECK(chainsElement->getNumChildElements() == 3); | |||
| juce::XmlElement* chain0Element = chainsElement->getChildByName(getChainXMLName(0)); | |||
| CHECK(chain0Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain0Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain1Element = chainsElement->getChildByName(getChainXMLName(1)); | |||
| CHECK(chain1Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 3); | |||
| CHECK(chain1Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == false); | |||
| juce::XmlElement* chain2Element = chainsElement->getChildByName(getChainXMLName(2)); | |||
| CHECK(chain2Element->getChildByName(XML_PLUGINS_STR)->getNumChildElements() == 1); | |||
| CHECK(chain2Element->getBoolAttribute(XML_IS_CHAIN_SOLOED_STR) == true); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write PluginChain") { | |||
| GIVEN("A PluginChain") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.234f; | |||
| }; | |||
| const auto isChainBypassed = GENERATE(false, true); | |||
| const auto isChainMuted = GENERATE(false, true); | |||
| const auto chainLayout = GENERATE( | |||
| std::vector<std::string>{}, | |||
| std::vector<std::string>{"gain", "plugin"}, | |||
| std::vector<std::string>{"plugin", "gain"} | |||
| ); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| chain->isChainBypassed = isChainBypassed; | |||
| chain->isChainMuted = isChainMuted; | |||
| chain->customName = "testChainName"; | |||
| for (int slotIndex {0}; slotIndex < chainLayout.size(); slotIndex++) { | |||
| if (chainLayout[slotIndex] == "gain") { | |||
| juce::AudioProcessor::BusesLayout layout; | |||
| ChainMutators::insertGainStage(chain, slotIndex, {layout, 44100, 64}); | |||
| // Add something unique so we know it actually wrote this slot | |||
| ChainMutators::setSlotBypass(chain, slotIndex, true); | |||
| ChainMutators::setGainLinear(chain, slotIndex, 0.1); | |||
| } else if (chainLayout[slotIndex] == "plugin") { | |||
| auto plugin = std::make_shared<XMLTestPluginInstance>(); | |||
| ChainMutators::insertPlugin(chain, plugin, slotIndex, hostConfig); | |||
| ChainMutators::setSlotBypass(chain, slotIndex, true); | |||
| } | |||
| } | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(chain, &e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| CHECK(e.getBoolAttribute(XML_IS_CHAIN_BYPASSED_STR) == isChainBypassed); | |||
| CHECK(e.getBoolAttribute(XML_IS_CHAIN_MUTED_STR) == isChainMuted); | |||
| CHECK(e.getStringAttribute(XML_CHAIN_CUSTOM_NAME_STR) == "testChainName"); | |||
| auto* pluginsElement = e.getChildByName(XML_PLUGINS_STR); | |||
| REQUIRE(pluginsElement != nullptr); | |||
| CHECK(pluginsElement->getNumChildElements() == chainLayout.size()); | |||
| for (int slotIndex {0}; slotIndex < chainLayout.size(); slotIndex++) { | |||
| auto* thisSlotElement = pluginsElement->getChildByName(getSlotXMLName(slotIndex)); | |||
| if (chainLayout[slotIndex] == "gain") { | |||
| REQUIRE(thisSlotElement != nullptr); | |||
| CHECK(thisSlotElement->getStringAttribute(XML_SLOT_TYPE_STR) == XML_SLOT_TYPE_GAIN_STAGE_STR); | |||
| CHECK(thisSlotElement->getBoolAttribute(XML_SLOT_IS_BYPASSED_STR) == true); | |||
| CHECK(thisSlotElement->getDoubleAttribute(XML_GAIN_STAGE_GAIN_STR) == Approx(0.1)); | |||
| } else if (chainLayout[slotIndex] == "plugin") { | |||
| REQUIRE(thisSlotElement != nullptr); | |||
| CHECK(thisSlotElement->getStringAttribute(XML_SLOT_TYPE_STR) == XML_SLOT_TYPE_PLUGIN_STR); | |||
| CHECK(thisSlotElement->getBoolAttribute(XML_SLOT_IS_BYPASSED_STR) == true); | |||
| auto descriptionElement = thisSlotElement->getChildByName("PLUGIN"); | |||
| CHECK(descriptionElement->getStringAttribute("name") == "TestPlugin"); | |||
| juce::MemoryBlock pluginState; | |||
| pluginState.fromBase64Encoding(thisSlotElement->getStringAttribute(XML_PLUGIN_DATA_STR)); | |||
| std::string pluginStateStr(pluginState.begin(), pluginState.getSize()); | |||
| CHECK(pluginStateStr == "testPluginData"); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write ChainSlotGainStage") { | |||
| GIVEN("A ChainSlotGainStage") { | |||
| juce::AudioProcessor::BusesLayout layout; | |||
| layout.inputBuses.add(juce::AudioChannelSet::mono()); | |||
| const bool isBypassed = GENERATE(true, false); | |||
| auto gainStage = std::make_shared<ChainSlotGainStage>(0.5, 0.6, isBypassed, layout); | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(gainStage, &e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| CHECK(e.getStringAttribute(XML_SLOT_TYPE_STR) == XML_SLOT_TYPE_GAIN_STAGE_STR); | |||
| CHECK(e.getDoubleAttribute(XML_GAIN_STAGE_GAIN_STR) == 0.5f); | |||
| CHECK(e.getDoubleAttribute(XML_GAIN_STAGE_PAN_STR) == 0.6f); | |||
| CHECK(e.getBoolAttribute(XML_SLOT_IS_BYPASSED_STR) == isBypassed); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write ChainSlotPlugin") { | |||
| GIVEN("A ChainSlotPlugin") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| const bool isBypassed = GENERATE(false, true); | |||
| std::shared_ptr<juce::AudioPluginInstance> plugin = | |||
| std::make_shared<XMLTestPluginInstance>(); | |||
| auto config = std::make_shared<PluginModulationConfig>(); | |||
| config->parameterConfigs.push_back(std::make_shared<PluginParameterModulationConfig>()); | |||
| config->parameterConfigs[0]->targetParameterName = "testConfig1"; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { return 0.0f; }; | |||
| auto slot = std::make_shared<ChainSlotPlugin>(plugin, isBypassed, modulationCallback, hostConfig); | |||
| slot->modulationConfig = config; | |||
| slot->editorBounds.reset(new PluginEditorBounds()); | |||
| *(slot->editorBounds.get()) = PluginEditorBoundsContainer( | |||
| juce::Rectangle<int>(150, 200), | |||
| juce::Rectangle<int>(2000, 1000)); | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(slot, &e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| CHECK(e.getStringAttribute(XML_SLOT_TYPE_STR) == XML_SLOT_TYPE_PLUGIN_STR); | |||
| CHECK(e.getBoolAttribute(XML_SLOT_IS_BYPASSED_STR) == isBypassed); | |||
| auto descriptionElement = e.getChildByName("PLUGIN"); | |||
| CHECK(descriptionElement->getStringAttribute("name") == "TestPlugin"); | |||
| juce::MemoryBlock pluginState; | |||
| pluginState.fromBase64Encoding(e.getStringAttribute(XML_PLUGIN_DATA_STR)); | |||
| std::string pluginStateStr(pluginState.begin(), pluginState.getSize()); | |||
| CHECK(pluginStateStr == "testPluginData"); | |||
| auto configElement = e.getChildByName(XML_MODULATION_CONFIG_STR); | |||
| auto paramConfigElement = configElement->getChildByName("ParamConfig_0"); | |||
| CHECK(paramConfigElement->getStringAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR) == "testConfig1"); | |||
| CHECK(juce::Rectangle<int>::fromString(e.getStringAttribute(XML_PLUGIN_EDITOR_BOUNDS_STR)) == slot->editorBounds->value().editorBounds); | |||
| CHECK(juce::Rectangle<int>::fromString(e.getStringAttribute(XML_DISPLAY_AREA_STR)) == slot->editorBounds->value().displayArea); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write PluginModulationConfig") { | |||
| GIVEN("A PluginModulationConfig") { | |||
| const bool isActive = GENERATE(false, true); | |||
| auto config = std::make_shared<PluginModulationConfig>(); | |||
| config->isActive = isActive; | |||
| config->parameterConfigs.push_back(std::make_shared<PluginParameterModulationConfig>()); | |||
| config->parameterConfigs[0]->targetParameterName = "testConfig1"; | |||
| config->parameterConfigs.push_back(std::make_shared<PluginParameterModulationConfig>()); | |||
| config->parameterConfigs[1]->targetParameterName = "testConfig2"; | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(config, &e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| CHECK(e.getBoolAttribute(XML_MODULATION_IS_ACTIVE_STR) == isActive); | |||
| juce::XmlElement* firstConfig = e.getChildByName("ParamConfig_0"); | |||
| CHECK(firstConfig->getStringAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR) == "testConfig1"); | |||
| juce::XmlElement* secondSource = e.getChildByName("ParamConfig_1"); | |||
| CHECK(secondSource->getStringAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR) == "testConfig2"); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write PluginParameterModulationConfig") { | |||
| GIVEN("A PluginParameterModulationConfig") { | |||
| typedef std::tuple<std::string, float> testData; | |||
| auto [parameterName, restValue] = GENERATE( | |||
| testData("parameterName", 0), | |||
| testData(" ", 0.5), | |||
| testData("name with spaces", 1)); | |||
| auto config = std::make_shared<PluginParameterModulationConfig>(); | |||
| config->targetParameterName = parameterName; | |||
| config->restValue = restValue; | |||
| config->sources.push_back(std::make_shared<PluginParameterModulationSource>( | |||
| ModulationSourceDefinition(1, MODULATION_TYPE::LFO), -0.5)); | |||
| config->sources.push_back(std::make_shared<PluginParameterModulationSource>( | |||
| ModulationSourceDefinition(2, MODULATION_TYPE::ENVELOPE), 0.5)); | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(config, &e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| CHECK(e.getStringAttribute(XML_MODULATION_TARGET_PARAMETER_NAME_STR) == juce::String(parameterName)); | |||
| CHECK(e.getDoubleAttribute(XML_MODULATION_REST_VALUE_STR) == restValue); | |||
| juce::XmlElement* firstSource = e.getChildByName("Source_0"); | |||
| CHECK(firstSource->getIntAttribute(XML_MODULATION_SOURCE_ID) == 1); | |||
| CHECK(firstSource->getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "lfo"); | |||
| CHECK(firstSource->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == -0.5); | |||
| juce::XmlElement* secondSource = e.getChildByName("Source_1"); | |||
| CHECK(secondSource->getIntAttribute(XML_MODULATION_SOURCE_ID) == 2); | |||
| CHECK(secondSource->getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "envelope"); | |||
| CHECK(secondSource->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == 0.5); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write PluginParameterModulationSource") { | |||
| GIVEN("A PluginParameterModulationSource") { | |||
| const double modulationAmount = GENERATE(-1, -0.5, 0, 0.5, 1); | |||
| auto source = std::make_shared<PluginParameterModulationSource>( | |||
| ModulationSourceDefinition(1, MODULATION_TYPE::LFO), modulationAmount); | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(source, &e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| CHECK(e.getIntAttribute(XML_MODULATION_SOURCE_ID) == 1); | |||
| CHECK(e.getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "lfo"); | |||
| CHECK(e.getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == modulationAmount); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write ModulationSourceDefinition") { | |||
| GIVEN("A ModulationSourceDefinition") { | |||
| const int modulationId = GENERATE(1, 2, 3); | |||
| typedef std::pair<MODULATION_TYPE, juce::String> testData; | |||
| auto [modulationType, modulationTypeString] = GENERATE( | |||
| testData(MODULATION_TYPE::MACRO, "macro"), | |||
| testData(MODULATION_TYPE::LFO, "lfo"), | |||
| testData(MODULATION_TYPE::ENVELOPE, "envelope"), | |||
| testData(MODULATION_TYPE::RANDOM, "random") | |||
| ); | |||
| ModulationSourceDefinition definition(modulationId, modulationType); | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| definition.writeToXml(&e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| CHECK(e.getIntAttribute(XML_MODULATION_SOURCE_ID) == modulationId); | |||
| CHECK(e.getStringAttribute(XML_MODULATION_SOURCE_TYPE) == modulationTypeString); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("XmlWriter: Can write ModulationSourcesState") { | |||
| GIVEN("A ModulationSourcesState") { | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| auto state = std::make_shared<ModelInterface::ModulationSourcesState>(modulationCallback); | |||
| ModulationMutators::addLfo(state); | |||
| ModulationMutators::addEnvelope(state); | |||
| ModulationMutators::addRandom(state); | |||
| // LFO | |||
| ModulationMutators::setLfoFreq(state, 0, 0.5); | |||
| ModulationMutators::setLfoDepth(state, 0, 0.6); | |||
| ModulationMutators::setLfoManualPhase(state, 0, 0.7); | |||
| ModulationMutators::setLfoTempoDenom(state, 0, 8); | |||
| ModulationMutators::setLfoTempoNumer(state, 0, 4); | |||
| ModulationMutators::setLfoWave(state, 0, 1); | |||
| ModulationMutators::setLfoInvertSwitch(state, 0, true); | |||
| ModulationMutators::setLfoTempoSyncSwitch(state, 0, true); | |||
| ModulationMutators::setLfoOutputMode(state, 0, WECore::Richter::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| // Envelope | |||
| ModulationMutators::setEnvAmount(state, 0, 0.8); | |||
| ModulationMutators::setEnvAttackTimeMs(state, 0, 0.9); | |||
| ModulationMutators::setEnvReleaseTimeMs(state, 0, 1.0); | |||
| ModulationMutators::setEnvFilterEnabled(state, 0, true); | |||
| ModulationMutators::setEnvFilterHz(state, 0, 20, 200); | |||
| ModulationMutators::setEnvUseSidechainInput(state, 0, true); | |||
| // Random | |||
| ModulationMutators::setRandomOutputMode(state, 0, WECore::Perlin::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| ModulationMutators::setRandomFreq(state, 0, 0.5); | |||
| ModulationMutators::setRandomDepth(state, 0, 0.6); | |||
| // Modulation | |||
| ModulationMutators::addSourceToLFOFreq(state, 0, ModulationSourceDefinition(2, MODULATION_TYPE::LFO)); | |||
| ModulationMutators::setLFOFreqModulationAmount(state, 0, 0, 0.2); | |||
| ModulationMutators::addSourceToLFODepth(state, 0, ModulationSourceDefinition(3, MODULATION_TYPE::LFO)); | |||
| ModulationMutators::setLFODepthModulationAmount(state, 0, 0, 0.3); | |||
| ModulationMutators::addSourceToLFOPhase(state, 0, ModulationSourceDefinition(4, MODULATION_TYPE::ENVELOPE)); | |||
| ModulationMutators::setLFOPhaseModulationAmount(state, 0, 0, 0.4); | |||
| ModulationMutators::addSourceToRandomFreq(state, 0, ModulationSourceDefinition(5, MODULATION_TYPE::LFO)); | |||
| ModulationMutators::setRandomFreqModulationAmount(state, 0, 0, 0.5); | |||
| ModulationMutators::addSourceToRandomDepth(state, 0, ModulationSourceDefinition(6, MODULATION_TYPE::ENVELOPE)); | |||
| ModulationMutators::setRandomDepthModulationAmount(state, 0, 0, 0.6); | |||
| WHEN("Asked to write it to XML") { | |||
| juce::XmlElement e("test"); | |||
| XmlWriter::write(*state, &e); | |||
| THEN("An XmlElement with the correct attributes is created") { | |||
| { | |||
| // LFO | |||
| juce::XmlElement* lfosElement = e.getChildByName(XML_LFOS_STR); | |||
| REQUIRE(lfosElement != nullptr); | |||
| juce::XmlElement* lfoElement = lfosElement->getChildByName(getLfoXMLName(0)); | |||
| REQUIRE(lfoElement != nullptr); | |||
| CHECK(lfoElement->getDoubleAttribute(XML_LFO_FREQ_STR) == Approx(0.5)); | |||
| CHECK(lfoElement->getDoubleAttribute(XML_LFO_DEPTH_STR) == Approx(0.6)); | |||
| CHECK(lfoElement->getDoubleAttribute(XML_LFO_MANUAL_PHASE_STR) == Approx(0.7)); | |||
| CHECK(lfoElement->getIntAttribute(XML_LFO_TEMPO_DENOM_STR) == 8); | |||
| CHECK(lfoElement->getIntAttribute(XML_LFO_TEMPO_NUMER_STR) == 4); | |||
| CHECK(lfoElement->getIntAttribute(XML_LFO_WAVE_STR) == 1); | |||
| CHECK(lfoElement->getBoolAttribute(XML_LFO_INVERT_STR) == true); | |||
| CHECK(lfoElement->getBoolAttribute(XML_LFO_TEMPO_SYNC_STR) == true); | |||
| CHECK(lfoElement->getIntAttribute(XML_LFO_OUTPUT_MODE_STR) == WECore::Richter::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| // LFO freq modulation | |||
| juce::XmlElement* freqModElement = lfoElement->getChildByName(XML_LFO_FREQ_MODULATION_SOURCES_STR); | |||
| REQUIRE(freqModElement != nullptr); | |||
| CHECK(freqModElement->getNumChildElements() == 1); | |||
| juce::XmlElement* lfoFreqSourceElement = freqModElement->getChildByName("Source_0"); | |||
| REQUIRE(lfoFreqSourceElement != nullptr); | |||
| CHECK(lfoFreqSourceElement->getIntAttribute(XML_MODULATION_SOURCE_ID) == 2); | |||
| CHECK(lfoFreqSourceElement->getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "lfo"); | |||
| CHECK(lfoFreqSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == Approx(0.2)); | |||
| // LFO depth modulation | |||
| juce::XmlElement* depthModElement = lfoElement->getChildByName(XML_LFO_DEPTH_MODULATION_SOURCES_STR); | |||
| REQUIRE(depthModElement != nullptr); | |||
| CHECK(depthModElement->getNumChildElements() == 1); | |||
| juce::XmlElement* lfoDepthSourceElement = depthModElement->getChildByName("Source_0"); | |||
| REQUIRE(lfoDepthSourceElement != nullptr); | |||
| CHECK(lfoDepthSourceElement->getIntAttribute(XML_MODULATION_SOURCE_ID) == 3); | |||
| CHECK(lfoDepthSourceElement->getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "lfo"); | |||
| CHECK(lfoDepthSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == Approx(0.3)); | |||
| // LFO phase modulation | |||
| juce::XmlElement* phaseModElement = lfoElement->getChildByName(XML_LFO_PHASE_MODULATION_SOURCES_STR); | |||
| REQUIRE(phaseModElement != nullptr); | |||
| CHECK(phaseModElement->getNumChildElements() == 1); | |||
| juce::XmlElement* lfoPhaseSourceElement = phaseModElement->getChildByName("Source_0"); | |||
| REQUIRE(lfoPhaseSourceElement != nullptr); | |||
| CHECK(lfoPhaseSourceElement->getIntAttribute(XML_MODULATION_SOURCE_ID) == 4); | |||
| CHECK(lfoPhaseSourceElement->getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "envelope"); | |||
| CHECK(lfoPhaseSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == Approx(0.4)); | |||
| } | |||
| { | |||
| // Envelope | |||
| juce::XmlElement* envsElement = e.getChildByName(XML_ENVELOPES_STR); | |||
| REQUIRE(envsElement != nullptr); | |||
| juce::XmlElement* envElement = envsElement->getChildByName(getEnvelopeXMLName(0)); | |||
| REQUIRE(envElement != nullptr); | |||
| CHECK(envElement->getDoubleAttribute(XML_ENV_AMOUNT_STR) == Approx(0.8)); | |||
| CHECK(envElement->getDoubleAttribute(XML_ENV_ATTACK_TIME_STR) == Approx(0.9)); | |||
| CHECK(envElement->getDoubleAttribute(XML_ENV_RELEASE_TIME_STR) == Approx(1.0)); | |||
| CHECK(envElement->getBoolAttribute(XML_ENV_FILTER_ENABLED_STR) == true); | |||
| CHECK(envElement->getDoubleAttribute(XML_ENV_LOW_CUT_STR) == 20); | |||
| CHECK(envElement->getDoubleAttribute(XML_ENV_HIGH_CUT_STR) == 200); | |||
| CHECK(envElement->getBoolAttribute(XML_ENV_USE_SIDECHAIN_INPUT_STR) == true); | |||
| } | |||
| { | |||
| // Random | |||
| juce::XmlElement* randomsElement = e.getChildByName(XML_RANDOMS_STR); | |||
| REQUIRE(randomsElement != nullptr); | |||
| juce::XmlElement* randomElement = randomsElement->getChildByName(getRandomXMLName(0)); | |||
| REQUIRE(randomElement != nullptr); | |||
| CHECK(randomElement->getIntAttribute(XML_RANDOM_OUTPUT_MODE_STR) == WECore::Perlin::Parameters::OUTPUTMODE.UNIPOLAR); | |||
| CHECK(randomElement->getDoubleAttribute(XML_RANDOM_FREQ_STR) == Approx(0.5)); | |||
| CHECK(randomElement->getDoubleAttribute(XML_RANDOM_DEPTH_STR) == Approx(0.6)); | |||
| // Random freq modulation | |||
| juce::XmlElement* freqModElement = randomElement->getChildByName(XML_RANDOM_FREQ_MODULATION_SOURCES_STR); | |||
| REQUIRE(freqModElement != nullptr); | |||
| CHECK(freqModElement->getNumChildElements() == 1); | |||
| juce::XmlElement* randomFreqSourceElement = freqModElement->getChildByName("Source_0"); | |||
| REQUIRE(randomFreqSourceElement != nullptr); | |||
| CHECK(randomFreqSourceElement->getIntAttribute(XML_MODULATION_SOURCE_ID) == 5); | |||
| CHECK(randomFreqSourceElement->getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "lfo"); | |||
| CHECK(randomFreqSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == Approx(0.5)); | |||
| // Random depth modulation | |||
| juce::XmlElement* depthModElement = randomElement->getChildByName(XML_RANDOM_DEPTH_MODULATION_SOURCES_STR); | |||
| REQUIRE(depthModElement != nullptr); | |||
| CHECK(depthModElement->getNumChildElements() == 1); | |||
| juce::XmlElement* randomDepthSourceElement = depthModElement->getChildByName("Source_0"); | |||
| REQUIRE(randomDepthSourceElement != nullptr); | |||
| CHECK(randomDepthSourceElement->getIntAttribute(XML_MODULATION_SOURCE_ID) == 6); | |||
| CHECK(randomDepthSourceElement->getStringAttribute(XML_MODULATION_SOURCE_TYPE) == "envelope"); | |||
| CHECK(randomDepthSourceElement->getDoubleAttribute(XML_MODULATION_SOURCE_AMOUNT) == Approx(0.6)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| #include "ChainProcessors.hpp" | |||
| #include "ChainSlotProcessors.hpp" | |||
| namespace ChainProcessors { | |||
| void prepareToPlay(PluginChain& chain, HostConfiguration config) { | |||
| chain.latencyCompLine->prepare({ | |||
| config.sampleRate, | |||
| static_cast<juce::uint32>(config.blockSize), | |||
| static_cast<juce::uint32>(getTotalNumInputChannels(config.layout)) | |||
| }); | |||
| for (std::shared_ptr<ChainSlotBase> slot : chain.chain) { | |||
| if (auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(slot)) { | |||
| ChainProcessors::prepareToPlay(*gainStage.get(), config); | |||
| } else if (auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(slot)) { | |||
| ChainProcessors::prepareToPlay(*pluginSlot.get(), config); | |||
| } | |||
| } | |||
| } | |||
| void releaseResources(PluginChain& chain) { | |||
| for (std::shared_ptr<ChainSlotBase> slot : chain.chain) { | |||
| if (auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(slot)) { | |||
| ChainProcessors::releaseResources(*gainStage.get()); | |||
| } else if (auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(slot)) { | |||
| ChainProcessors::releaseResources(*pluginSlot.get()); | |||
| } | |||
| } | |||
| } | |||
| void reset(PluginChain& chain) { | |||
| for (std::shared_ptr<ChainSlotBase> slot : chain.chain) { | |||
| if (auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(slot)) { | |||
| ChainProcessors::reset(*gainStage.get()); | |||
| } else if (auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(slot)) { | |||
| ChainProcessors::reset(*pluginSlot.get()); | |||
| } | |||
| } | |||
| } | |||
| void processBlock(PluginChain& chain, | |||
| juce::AudioBuffer<float>& buffer, | |||
| juce::MidiBuffer& midiMessages, | |||
| juce::AudioPlayHead* newPlayHead) { | |||
| // Add the latency compensation | |||
| juce::dsp::AudioBlock<float> bufferBlock(buffer); | |||
| juce::dsp::ProcessContextReplacing<float> context(bufferBlock); | |||
| { | |||
| WECore::AudioSpinTryLock lock(chain.latencyCompLineMutex); | |||
| if (lock.isLocked()) { | |||
| chain.latencyCompLine->process(context); | |||
| } | |||
| } | |||
| // Mute gets priority over bypass | |||
| if (chain.isChainMuted) { | |||
| // Muted - return empty buffers (including sidechains since we don't need them) | |||
| for (int channelIndex {0}; channelIndex < buffer.getNumChannels(); channelIndex++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIndex), 0, buffer.getNumSamples()); | |||
| } | |||
| } else if (chain.isChainBypassed) { | |||
| // Bypassed - do nothing | |||
| } else { | |||
| // Chain is active - process as normal | |||
| for (std::shared_ptr<ChainSlotBase> slot : chain.chain) { | |||
| if (auto gainStage = std::dynamic_pointer_cast<ChainSlotGainStage>(slot)) { | |||
| ChainProcessors::processBlock(*gainStage.get(), buffer); | |||
| } else if (auto pluginSlot = std::dynamic_pointer_cast<ChainSlotPlugin>(slot)) { | |||
| ChainProcessors::processBlock(*pluginSlot.get(), buffer, midiMessages, newPlayHead); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginChain.hpp" | |||
| namespace ChainProcessors { | |||
| void prepareToPlay(PluginChain& chain, HostConfiguration config); | |||
| void releaseResources(PluginChain& chain); | |||
| void reset(PluginChain& chain); | |||
| void processBlock(PluginChain& chain, | |||
| juce::AudioBuffer<float>& buffer, | |||
| juce::MidiBuffer& midiMessages, | |||
| juce::AudioPlayHead* newPlayHead); | |||
| } | |||
| @@ -0,0 +1,376 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "ChainMutators.hpp" | |||
| #include "ChainProcessors.hpp" | |||
| namespace { | |||
| constexpr int NUM_SAMPLES {64}; | |||
| constexpr int SAMPLE_RATE {44100}; | |||
| class ProcessorTestPluginInstance : public TestUtils::TestPluginInstance { | |||
| public: | |||
| std::optional<std::function<void(double, int)>> onPrepareToPlay; | |||
| std::optional<std::function<void()>> onReleaseResources; | |||
| std::optional<std::function<void()>> onReset; | |||
| std::optional<std::function<void(juce::AudioBuffer<float>&, juce::MidiBuffer&)>> onProcess; | |||
| ProcessorTestPluginInstance() = default; | |||
| void prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock) override { | |||
| if (onPrepareToPlay.has_value()) { | |||
| onPrepareToPlay.value()(sampleRate, maximumExpectedSamplesPerBlock); | |||
| } | |||
| } | |||
| void releaseResources() override { | |||
| if (onReleaseResources.has_value()) { | |||
| onReleaseResources.value()(); | |||
| } | |||
| } | |||
| void reset() override { | |||
| if (onReset.has_value()) { | |||
| onReset.value()(); | |||
| } | |||
| } | |||
| void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override { | |||
| if (onProcess.has_value()) { | |||
| onProcess.value()(buffer, midiMessages); | |||
| } | |||
| } | |||
| }; | |||
| } | |||
| SCENARIO("ChainProcessors: Silence in = silence out") { | |||
| GIVEN("Some slots and a buffer of silence") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| // Different chain slots | |||
| const auto chainLayout = GENERATE( | |||
| std::vector<std::string>{}, | |||
| std::vector<std::string>{"gain", "plugin"}, | |||
| std::vector<std::string>{"plugin", "gain"} | |||
| ); | |||
| const bool isChainBypassed = GENERATE(false, true); | |||
| const bool isChainMuted = GENERATE(false, true); | |||
| // Mono and stereo | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| buffer.clear(); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| chain->isChainBypassed = isChainBypassed; | |||
| chain->isChainMuted = isChainMuted; | |||
| bool didCallPluginProcess {false}; | |||
| const int expectedNumChannels {buffer.getNumChannels()}; | |||
| for (int slotIndex {0}; slotIndex < chainLayout.size(); slotIndex++) { | |||
| if (chainLayout[slotIndex] == "gain") { | |||
| ChainMutators::insertGainStage(chain, slotIndex, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| } else if (chainLayout[slotIndex] == "plugin") { | |||
| auto plugin = std::make_shared<ProcessorTestPluginInstance>(); | |||
| plugin->onProcess = [&didCallPluginProcess, expectedNumChannels](juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| didCallPluginProcess = true; | |||
| // TODO Add checks for midi | |||
| CHECK(buffer.getNumChannels() == expectedNumChannels); | |||
| CHECK(buffer.getNumSamples() == NUM_SAMPLES); | |||
| }; | |||
| ChainMutators::insertPlugin(chain, plugin, slotIndex, hostConfig); | |||
| } | |||
| } | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| ChainProcessors::prepareToPlay(*(chain.get()), {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(*(chain.get()), buffer, midiBuffer, nullptr); | |||
| THEN("The buffer contains silence") { | |||
| const bool expectDidCallPluginProcess { | |||
| !(isChainBypassed || isChainMuted) && ChainMutators::getNumSlots(chain) != 0 | |||
| }; | |||
| CHECK(didCallPluginProcess == expectDidCallPluginProcess); | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == 0.0f); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Gain stage and plugin processing is applied correctly") { | |||
| GIVEN("A gain stage and a buffer of 1's") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| // Different chain layouts | |||
| auto [chainLayout, expectedOutput] = GENERATE( | |||
| std::make_pair<std::vector<std::string>, float>({}, 1), | |||
| std::make_pair<std::vector<std::string>, float>({"gain", "plugin"}, 0.8), | |||
| std::make_pair<std::vector<std::string>, float>({"plugin", "gain"}, 0.65) | |||
| ); | |||
| const bool isChainBypassed = GENERATE(false, true); | |||
| const bool isChainMuted = GENERATE(false, true); | |||
| // Mono and stereo | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| // Do this after calling GENERATE so it gets reset for each test | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIdx), 1, buffer.getNumSamples()); | |||
| } | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| chain->isChainBypassed = isChainBypassed; | |||
| chain->isChainMuted = isChainMuted; | |||
| for (int slotIndex {0}; slotIndex < chainLayout.size(); slotIndex++) { | |||
| if (chainLayout[slotIndex] == "gain") { | |||
| ChainMutators::insertGainStage(chain, slotIndex, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainMutators::setGainLinear(chain, slotIndex, 0.5); | |||
| // ChainMutators::setPan(-0.5); | |||
| } else if (chainLayout[slotIndex] == "plugin") { | |||
| auto plugin = std::make_shared<ProcessorTestPluginInstance>(); | |||
| plugin->onProcess = [](juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::add(buffer.getWritePointer(channelIdx), 0.3, buffer.getNumSamples()); | |||
| } | |||
| }; | |||
| ChainMutators::insertPlugin(chain, plugin, slotIndex, hostConfig); | |||
| } | |||
| } | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| ChainProcessors::prepareToPlay(*(chain.get()), {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(*(chain.get()), buffer, midiBuffer, nullptr); | |||
| THEN("The buffer contains silence") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| const float finalExpectedOutput { | |||
| isChainMuted ? 0 : isChainBypassed ? 1 : expectedOutput | |||
| }; | |||
| CHECK(readPtr[sampleIdx] == Approx(finalExpectedOutput)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Latency is applied correctly") { | |||
| GIVEN("An empty chain and a buffer with a single value") { | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| const bool isChainBypassed = GENERATE(false, true); | |||
| const bool isChainMuted = GENERATE(false, true); | |||
| // Mono and stereo | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| buffer.clear(); | |||
| // Add a single sample | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| buffer.getWritePointer(channelIdx)[0] = 1; | |||
| } | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| chain->isChainBypassed = isChainBypassed; | |||
| chain->isChainMuted = isChainMuted; | |||
| constexpr int sampleDelay {10}; | |||
| WHEN("The latency mutex is locked and the buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| ChainProcessors::prepareToPlay(*(chain.get()), {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainMutators::setRequiredLatency(chain, sampleDelay, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| { | |||
| WECore::AudioSpinTryLock lock(chain->latencyCompLineMutex); | |||
| ChainProcessors::processBlock(*(chain.get()), buffer, midiBuffer, nullptr); | |||
| } | |||
| THEN("The buffer is unchanged") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| const float expectedSample { | |||
| sampleIdx == 0 && !isChainMuted ? 1.0f : 0.0f | |||
| }; | |||
| CHECK(readPtr[sampleIdx] == expectedSample); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| ChainProcessors::prepareToPlay(*(chain.get()), {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainMutators::setRequiredLatency(chain, sampleDelay, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(*(chain.get()), buffer, midiBuffer, nullptr); | |||
| THEN("The buffer is delayed correctly") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| const float expectedSample { | |||
| sampleIdx == sampleDelay && !isChainMuted ? 1.0f : 0.0f | |||
| }; | |||
| CHECK(readPtr[sampleIdx] == expectedSample); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Plugin methods are called correctly") { | |||
| GIVEN("A chain of plugins") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| const auto layout = TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()); | |||
| auto chain = std::make_shared<PluginChain>(modulationCallback); | |||
| auto plugin1 = std::make_shared<ProcessorTestPluginInstance>(); | |||
| auto plugin2 = std::make_shared<ProcessorTestPluginInstance>(); | |||
| ChainMutators::insertPlugin(chain, plugin1, 0, hostConfig); | |||
| ChainMutators::insertPlugin(chain, plugin2, 1, hostConfig); | |||
| WHEN("prepareToPlay is called") { | |||
| bool calledPrepareToPlay1 {false}; | |||
| plugin1->onPrepareToPlay = [&calledPrepareToPlay1](double sampleRate, int samplesPerBlock) { | |||
| calledPrepareToPlay1 = true; | |||
| CHECK(sampleRate == SAMPLE_RATE); | |||
| CHECK(samplesPerBlock == NUM_SAMPLES); | |||
| }; | |||
| bool calledPrepareToPlay2 {false}; | |||
| plugin2->onPrepareToPlay = [&calledPrepareToPlay2](double sampleRate, int samplesPerBlock) { | |||
| calledPrepareToPlay2 = true; | |||
| CHECK(sampleRate == SAMPLE_RATE); | |||
| CHECK(samplesPerBlock == NUM_SAMPLES); | |||
| }; | |||
| ChainProcessors::prepareToPlay(*(chain.get()), {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| THEN("The plugin's prepareToPlay is called with the correct arguments") { | |||
| CHECK(calledPrepareToPlay1); | |||
| CHECK(calledPrepareToPlay2); | |||
| } | |||
| } | |||
| WHEN("releaseResources is called") { | |||
| bool calledReleaseResources1 {false}; | |||
| plugin1->onReleaseResources = [&calledReleaseResources1]() { | |||
| calledReleaseResources1 = true; | |||
| }; | |||
| bool calledReleaseResources2 {false}; | |||
| plugin2->onReleaseResources = [&calledReleaseResources2]() { | |||
| calledReleaseResources2 = true; | |||
| }; | |||
| ChainProcessors::releaseResources(*(chain.get())); | |||
| THEN("The plugin's releaseResources is called with the correct arguments") { | |||
| CHECK(calledReleaseResources1); | |||
| CHECK(calledReleaseResources2); | |||
| } | |||
| } | |||
| WHEN("reset is called") { | |||
| bool calledReset1 {false}; | |||
| plugin1->onReset = [&calledReset1]() { | |||
| calledReset1 = true; | |||
| }; | |||
| bool calledReset2 {false}; | |||
| plugin2->onReset = [&calledReset2]() { | |||
| calledReset2 = true; | |||
| }; | |||
| ChainProcessors::reset(*(chain.get())); | |||
| THEN("The plugin's reset is called with the correct arguments") { | |||
| CHECK(calledReset1); | |||
| CHECK(calledReset2); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,134 @@ | |||
| #include "ChainSlotProcessors.hpp" | |||
| #include <assert.h> | |||
| #include "PluginUtils.h" | |||
| namespace { | |||
| void applyModulationForParamter(ChainSlotPlugin& slot, | |||
| juce::AudioProcessorParameter* targetParameter, | |||
| const std::shared_ptr<PluginParameterModulationConfig> parameterConfig) { | |||
| // Start from the rest value | |||
| float paramValue {parameterConfig->restValue}; | |||
| for (const auto source : parameterConfig->sources) { | |||
| // Add the modulation from each source | |||
| const float valueForThisSource { | |||
| slot.getModulationValueCallback(source->definition.id, source->definition.type) | |||
| * source->modulationAmount | |||
| }; | |||
| paramValue += valueForThisSource; | |||
| } | |||
| // Assign the value to the parameter | |||
| targetParameter->setValue(paramValue); | |||
| } | |||
| } | |||
| namespace ChainProcessors { | |||
| void prepareToPlay(ChainSlotGainStage& gainStage, HostConfiguration config) { | |||
| gainStage.numMainChannels = config.layout.getMainInputChannels(); | |||
| assert(gainStage.numMainChannels <= 2); | |||
| for (auto& env : gainStage.meterEnvelopes) { | |||
| env.setSampleRate(config.sampleRate); | |||
| } | |||
| } | |||
| void releaseResources(ChainSlotGainStage& /*gainStage*/) { | |||
| // Do nothing | |||
| } | |||
| void reset(ChainSlotGainStage& gainStage) { | |||
| for (auto& env : gainStage.meterEnvelopes) { | |||
| env.reset(); | |||
| } | |||
| } | |||
| void processBlock(ChainSlotGainStage& gainStage, juce::AudioBuffer<float>& buffer) { | |||
| if (!gainStage.isBypassed) { | |||
| // Apply gain | |||
| for (int channel {0}; channel < gainStage.numMainChannels; channel++) { | |||
| juce::FloatVectorOperations::multiply(buffer.getWritePointer(channel), | |||
| gainStage.gain, | |||
| buffer.getNumSamples()); | |||
| } | |||
| if (gainStage.numMainChannels == 2) { | |||
| // Stereo input - apply balance | |||
| Utils::processBalance(gainStage.pan, buffer); | |||
| } | |||
| } | |||
| // Update the envelope follower | |||
| for (int sampleIndex {0}; sampleIndex < buffer.getNumSamples(); sampleIndex++) { | |||
| for (int channel {0}; channel < gainStage.numMainChannels; channel++) { | |||
| gainStage.meterEnvelopes[channel].getNextOutput(buffer.getReadPointer(channel)[sampleIndex]); | |||
| } | |||
| } | |||
| } | |||
| void prepareToPlay(ChainSlotPlugin& slot, HostConfiguration config) { | |||
| slot.plugin->setRateAndBufferSizeDetails(config.sampleRate, config.blockSize); | |||
| slot.plugin->prepareToPlay(config.sampleRate, config.blockSize); | |||
| slot.spareSCBuffer.reset(new juce::AudioBuffer<float>(config.layout.getMainInputChannels() * 2, config.blockSize)); | |||
| } | |||
| void releaseResources(ChainSlotPlugin& slot) { | |||
| slot.plugin->releaseResources(); | |||
| } | |||
| void reset(ChainSlotPlugin& slot) { | |||
| slot.plugin->reset(); | |||
| slot.spareSCBuffer->clear(); | |||
| } | |||
| void processBlock(ChainSlotPlugin& slot, | |||
| juce::AudioBuffer<float>& buffer, | |||
| juce::MidiBuffer& midiMessages, | |||
| juce::AudioPlayHead* newPlayHead) { | |||
| if (newPlayHead != nullptr) { | |||
| slot.plugin->setPlayHead(newPlayHead); | |||
| } | |||
| if (!slot.isBypassed) { | |||
| // Apply parameter modulation | |||
| if (slot.modulationConfig->isActive) { | |||
| // TODO we need a faster way of retrieving parameters | |||
| // Try again to get PluginParameterModulationConfig to hold a pointer to the parameter directly | |||
| const juce::Array<juce::AudioProcessorParameter*>& parameters = slot.plugin->getParameters(); | |||
| for (const auto parameterConfig : slot.modulationConfig->parameterConfigs) { | |||
| for (juce::AudioProcessorParameter* targetParameter : parameters) { | |||
| if (targetParameter->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT) == parameterConfig->targetParameterName) { | |||
| applyModulationForParamter(slot, targetParameter, parameterConfig); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| const int numPluginInputs {getTotalNumInputChannels(slot.plugin->getBusesLayout())}; | |||
| const bool useSpareSidechainBuffer = { | |||
| buffer.getNumChannels() < numPluginInputs && slot.spareSCBuffer->getNumChannels() == numPluginInputs | |||
| }; | |||
| if (useSpareSidechainBuffer) { | |||
| // Copy from the real input buffer into our buffer with the correct number of channels | |||
| for (int channelIndex {0}; channelIndex < buffer.getNumChannels(); channelIndex++) { | |||
| slot.spareSCBuffer->copyFrom(channelIndex, 0, buffer, channelIndex, 0, buffer.getNumSamples()); | |||
| } | |||
| // Do processing | |||
| slot.plugin->processBlock(*slot.spareSCBuffer, midiMessages); | |||
| // Copy back to the real buffer | |||
| for (int channelIndex {0}; channelIndex < buffer.getNumChannels(); channelIndex++) { | |||
| buffer.copyFrom(channelIndex, 0, *slot.spareSCBuffer, channelIndex, 0, buffer.getNumSamples()); | |||
| } | |||
| } else { | |||
| // Do processing | |||
| slot.plugin->processBlock(buffer, midiMessages); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "ChainSlots.hpp" | |||
| namespace ChainProcessors { | |||
| void prepareToPlay(ChainSlotGainStage& gainStage, HostConfiguration config); | |||
| void releaseResources(ChainSlotGainStage& gainStage); | |||
| void reset(ChainSlotGainStage& gainStage); | |||
| void processBlock(ChainSlotGainStage& gainStage, juce::AudioBuffer<float>& buffer); | |||
| void prepareToPlay(ChainSlotPlugin& slot, HostConfiguration config); | |||
| void releaseResources(ChainSlotPlugin& slot); | |||
| void reset(ChainSlotPlugin& slot); | |||
| void processBlock(ChainSlotPlugin& slot, | |||
| juce::AudioBuffer<float>& buffer, | |||
| juce::MidiBuffer& midiMessages, | |||
| juce::AudioPlayHead* newPlayHead); | |||
| } | |||
| @@ -0,0 +1,437 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "ChainSlotProcessors.hpp" | |||
| namespace { | |||
| constexpr int NUM_SAMPLES {64}; | |||
| constexpr int SAMPLE_RATE {44100}; | |||
| class ProcessorTestPluginInstance : public TestUtils::TestPluginInstance { | |||
| public: | |||
| class PluginParameter : public Parameter { | |||
| public: | |||
| float value; | |||
| juce::String name; | |||
| PluginParameter(juce::String newName) : value(0), name(newName) { } | |||
| float getValue() const override { return value; } | |||
| void setValue(float newValue) override { value = newValue; } | |||
| float getDefaultValue() const override { return 0; } | |||
| juce::String getName(int maximumStringLength) const override { return name; } | |||
| juce::String getLabel() const override { return name; } | |||
| juce::String getParameterID() const override { return name; } | |||
| }; | |||
| std::function<void(juce::AudioBuffer<float>&, juce::MidiBuffer&)> onProcess; | |||
| void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| onProcess(buffer, midiMessages); | |||
| } | |||
| static ProcessorTestPluginInstance* create() { | |||
| return new ProcessorTestPluginInstance( | |||
| juce::AudioProcessor::BusesProperties().withInput("Input", juce::AudioChannelSet::stereo(), true) | |||
| .withOutput("Output", juce::AudioChannelSet::stereo(), true)); | |||
| } | |||
| static ProcessorTestPluginInstance* createWithSidechain() { | |||
| return new ProcessorTestPluginInstance( | |||
| juce::AudioProcessor::BusesProperties().withInput("Input", juce::AudioChannelSet::stereo(), true) | |||
| .withOutput("Output", juce::AudioChannelSet::stereo(), true) | |||
| .withInput("Sidechain", juce::AudioChannelSet::stereo(), true)); | |||
| } | |||
| private: | |||
| ProcessorTestPluginInstance(BusesProperties buses) : TestUtils::TestPluginInstance(buses) { | |||
| addHostedParameter(std::make_unique<PluginParameter>("param1")); | |||
| addHostedParameter(std::make_unique<PluginParameter>("param2")); | |||
| addHostedParameter(std::make_unique<PluginParameter>("param3")); | |||
| } | |||
| }; | |||
| } | |||
| SCENARIO("ChainProcessors: Gain stage silence in = silence out") { | |||
| GIVEN("A gain stage and a buffer of silence") { | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| buffer.clear(); | |||
| ChainSlotGainStage gainStage(1, 0, false, layout); | |||
| WHEN("The buffer is processed") { | |||
| ChainProcessors::prepareToPlay(gainStage, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(gainStage, buffer); | |||
| THEN("The buffer contains silence") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == 0.0f); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Gain stage gain is applied correctly") { | |||
| GIVEN("A gain stage and a buffer of 1's") { | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| const float gain = GENERATE(0, 0.5, 1, 2); | |||
| // Do this after calling GENERATE for the gain so it gets reset for each test | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIdx), 1, buffer.getNumSamples()); | |||
| } | |||
| ChainSlotGainStage gainStage(gain, 0, false, layout); | |||
| WHEN("The buffer is processed") { | |||
| ChainProcessors::prepareToPlay(gainStage, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(gainStage, buffer); | |||
| THEN("The buffer has the correct gain applied") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == gain); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Gain stage gain isn't applied when bypassed") { | |||
| GIVEN("A gain stage with a gain other than 1 but bypassed and a buffer of 1's") { | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIdx), 1, buffer.getNumSamples()); | |||
| } | |||
| ChainSlotGainStage gainStage(0.5, 0, true, layout); | |||
| WHEN("The buffer is processed") { | |||
| ChainProcessors::prepareToPlay(gainStage, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(gainStage, buffer); | |||
| THEN("The buffer is unchanged") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == 1.0f); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Gain stage panning is applied correctly") { | |||
| GIVEN("A gain stage panned left and a buffer of 1's") { | |||
| juce::AudioProcessor::BusesLayout layout; | |||
| layout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| juce::AudioBuffer<float> buffer(2, NUM_SAMPLES); | |||
| const float pan = GENERATE(-1, 1); | |||
| // Do this after calling GENERATE for the gain so it gets reset for each test | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIdx), 1, buffer.getNumSamples()); | |||
| } | |||
| ChainSlotGainStage gainStage(1, pan, false, layout); | |||
| WHEN("The buffer is processed") { | |||
| ChainProcessors::prepareToPlay(gainStage, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(gainStage, buffer); | |||
| THEN("The buffer has been panned correctly") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| const bool bufferShouldBeZero { | |||
| (pan == -1 && channelIdx == 1) || (pan == 1 && channelIdx == 0) | |||
| }; | |||
| const float expectedValue {bufferShouldBeZero ? 0.0f : 1.0f}; | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == expectedValue); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Gain stage panning isn't applied when bypassed") { | |||
| GIVEN("A gain stage panned left and a buffer of 1's") { | |||
| juce::AudioProcessor::BusesLayout layout; | |||
| layout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| juce::AudioBuffer<float> buffer(2, NUM_SAMPLES); | |||
| const float pan = GENERATE(-1, 1); | |||
| // Do this after calling GENERATE for the gain so it gets reset for each test | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIdx), 1, buffer.getNumSamples()); | |||
| } | |||
| ChainSlotGainStage gainStage(1, pan, true, layout); | |||
| WHEN("The buffer is processed") { | |||
| ChainProcessors::prepareToPlay(gainStage, {layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(gainStage, buffer); | |||
| THEN("The buffer is unchanged ") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == 1.0f); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Plugin isn't applied when bypassed") { | |||
| GIVEN("A plugin but bypassed and a buffer of 1's") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| juce::AudioBuffer<float> buffer(2, NUM_SAMPLES); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(0), 1, buffer.getNumSamples()); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(1), 1, buffer.getNumSamples()); | |||
| std::shared_ptr<ProcessorTestPluginInstance> plugin(ProcessorTestPluginInstance::create()); | |||
| plugin->onProcess = [](juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| // This should never be called while bypassed | |||
| CHECK(false); | |||
| }; | |||
| ChainSlotPlugin slot(plugin, | |||
| true, | |||
| [](int, MODULATION_TYPE) { return 0.0f; }, | |||
| hostConfig); | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| ChainProcessors::prepareToPlay(slot, {hostConfig.layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(slot, buffer, midiBuffer, nullptr); | |||
| THEN("The buffer is unchanged") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == 1.0f); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Plugin is applied correctly with modulation") { | |||
| GIVEN("A plugin with three parameters (two which are modulated) and a buffer of 1's") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| juce::AudioBuffer<float> buffer(2, NUM_SAMPLES); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(0), 1, buffer.getNumSamples()); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(1), 1, buffer.getNumSamples()); | |||
| bool didCallProcess {false}; | |||
| std::shared_ptr<ProcessorTestPluginInstance> plugin(ProcessorTestPluginInstance::create()); | |||
| plugin->onProcess = [&didCallProcess](juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| didCallProcess = true; | |||
| // TODO Add checks for midi | |||
| CHECK(buffer.getNumChannels() == 2); | |||
| CHECK(buffer.getNumSamples() == 64); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(0), 0.1, buffer.getNumSamples()); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(1), 0.2, buffer.getNumSamples()); | |||
| }; | |||
| bool didCallModulation {false}; | |||
| ChainSlotPlugin slot(plugin, | |||
| false, | |||
| [&didCallModulation](int id, MODULATION_TYPE type) { | |||
| didCallModulation = true; | |||
| if (type == MODULATION_TYPE::MACRO) { | |||
| CHECK(id == 1); | |||
| return 0.13f; | |||
| } else if (type == MODULATION_TYPE::LFO) { | |||
| CHECK(id == 2); | |||
| return 0.23f; | |||
| } else if (type == MODULATION_TYPE::ENVELOPE) { | |||
| CHECK(id == 3); | |||
| return 0.33f; | |||
| } | |||
| CHECK(false); | |||
| return 0.0f; | |||
| }, | |||
| hostConfig); | |||
| // Configure the plugin without a sidechain input | |||
| juce::AudioProcessor::BusesLayout pluginLayout; | |||
| pluginLayout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| pluginLayout.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| REQUIRE(plugin->setBusesLayout(pluginLayout)); | |||
| // Set up the modulation | |||
| slot.modulationConfig->isActive = true; | |||
| slot.modulationConfig->parameterConfigs.push_back(std::make_shared<PluginParameterModulationConfig>()); | |||
| slot.modulationConfig->parameterConfigs.push_back(std::make_shared<PluginParameterModulationConfig>()); | |||
| // The first parameter has 1 modulation source | |||
| slot.modulationConfig->parameterConfigs[0]->targetParameterName = "param1"; | |||
| slot.modulationConfig->parameterConfigs[0]->restValue = 0.11; | |||
| slot.modulationConfig->parameterConfigs[0]->sources.push_back( | |||
| std::make_shared<PluginParameterModulationSource>(ModulationSourceDefinition(1, MODULATION_TYPE::MACRO), 0.12)); | |||
| // The second parameter has 2 modulation sources | |||
| slot.modulationConfig->parameterConfigs[1]->targetParameterName = "param2"; | |||
| slot.modulationConfig->parameterConfigs[1]->restValue = 0.21; | |||
| slot.modulationConfig->parameterConfigs[1]->sources.push_back( | |||
| std::make_shared<PluginParameterModulationSource>(ModulationSourceDefinition(2, MODULATION_TYPE::LFO), 0.22)); | |||
| slot.modulationConfig->parameterConfigs[1]->sources.push_back( | |||
| std::make_shared<PluginParameterModulationSource>(ModulationSourceDefinition(3, MODULATION_TYPE::ENVELOPE), 0.32)); | |||
| slot.plugin->getHostedParameter(2)->setValue(0.5); | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| ChainProcessors::prepareToPlay(slot, {hostConfig.layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(slot, buffer, midiBuffer, nullptr); | |||
| THEN("The buffer is processed and modulation is applied correctly") { | |||
| CHECK(didCallProcess); | |||
| // Check the buffer | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| const float expectedValue = channelIdx == 0 ? 0.1 : 0.2; | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == expectedValue); | |||
| } | |||
| } | |||
| // Check the parameter modulation | |||
| CHECK(slot.plugin->getHostedParameter(0)->getValue() == Approx(0.11 + 0.12 * 0.13)); | |||
| CHECK(slot.plugin->getHostedParameter(1)->getValue() == Approx(0.21 + 0.22 * 0.23 + 0.32 * 0.33)); | |||
| CHECK(slot.plugin->getHostedParameter(2)->getValue() == Approx(0.5)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("ChainProcessors: Spare SC buffer is used when plugin is expecting a sidechain input and Syndicate can't provide one") { | |||
| GIVEN("A plugin with three parameters (two which are modulated) and a buffer of 1's") { | |||
| HostConfiguration hostConfig; | |||
| hostConfig.sampleRate = 44100; | |||
| hostConfig.blockSize = 10; | |||
| hostConfig.layout = TestUtils::createLayoutWithChannels( | |||
| juce::AudioChannelSet::stereo(), juce::AudioChannelSet::stereo()); | |||
| juce::AudioBuffer<float> buffer(2, NUM_SAMPLES); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(0), 1, buffer.getNumSamples()); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(1), 1, buffer.getNumSamples()); | |||
| bool didCallProcess {false}; | |||
| std::shared_ptr<ProcessorTestPluginInstance> plugin(ProcessorTestPluginInstance::createWithSidechain()); | |||
| plugin->onProcess = [&didCallProcess](juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| didCallProcess = true; | |||
| // TODO Add checks for midi | |||
| CHECK(buffer.getNumChannels() == 4); | |||
| CHECK(buffer.getNumSamples() == 64); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(0), 0.1, buffer.getNumSamples()); | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(1), 0.2, buffer.getNumSamples()); | |||
| }; | |||
| // Configure the plugin to expect a sidechain input | |||
| juce::AudioProcessor::BusesLayout pluginLayout; | |||
| pluginLayout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| pluginLayout.inputBuses.add(juce::AudioChannelSet::stereo()); | |||
| pluginLayout.outputBuses.add(juce::AudioChannelSet::stereo()); | |||
| REQUIRE(plugin->setBusesLayout(pluginLayout)); | |||
| ChainSlotPlugin slot(plugin, false, [](int id, MODULATION_TYPE type) { return 0.0f; }, hostConfig); | |||
| REQUIRE(slot.spareSCBuffer->getNumChannels() == 4); | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| ChainProcessors::prepareToPlay(slot, {hostConfig.layout, SAMPLE_RATE, NUM_SAMPLES}); | |||
| ChainProcessors::processBlock(slot, buffer, midiBuffer, nullptr); | |||
| THEN("The buffer is processed and modulation is applied correctly") { | |||
| CHECK(didCallProcess); | |||
| // Check the buffer | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| const float expectedValue = channelIdx == 0 ? 0.1 : 0.2; | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == expectedValue); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,106 @@ | |||
| #include "CrossoverProcessors.hpp" | |||
| #include "ChainProcessors.hpp" | |||
| namespace CrossoverProcessors { | |||
| void prepareToPlay(CrossoverState& state, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout) { | |||
| // We don't filter sidechain channels but do copy them to each buffer - so filters may need less channels than the buffers do | |||
| const int numFilterChannels {canDoStereoSplitTypes(layout) ? 2 : 1}; | |||
| for (std::shared_ptr<CloneableLRFilter<float>>& filter : state.lowpassFilters) { | |||
| filter->prepare({sampleRate, static_cast<juce::uint32>(samplesPerBlock), static_cast<juce::uint32>(numFilterChannels)}); | |||
| } | |||
| for (std::shared_ptr<CloneableLRFilter<float>>& filter : state.highpassFilters) { | |||
| filter->prepare({sampleRate, static_cast<juce::uint32>(samplesPerBlock), static_cast<juce::uint32>(numFilterChannels)}); | |||
| } | |||
| for (std::shared_ptr<CloneableLRFilter<float>>& filter : state.allpassFilters) { | |||
| filter->prepare({sampleRate, static_cast<juce::uint32>(samplesPerBlock), static_cast<juce::uint32>(numFilterChannels)}); | |||
| } | |||
| for (juce::AudioBuffer<float>& buffer : state.buffers) { | |||
| buffer.setSize(getTotalNumInputChannels(layout), samplesPerBlock); | |||
| } | |||
| state.config.sampleRate = sampleRate; | |||
| state.config.blockSize = samplesPerBlock; | |||
| state.config.layout = layout; | |||
| } | |||
| void reset(CrossoverState& state) { | |||
| for (std::shared_ptr<CloneableLRFilter<float>>& filter : state.lowpassFilters) { | |||
| filter->reset(); | |||
| } | |||
| for (std::shared_ptr<CloneableLRFilter<float>>& filter : state.highpassFilters) { | |||
| filter->reset(); | |||
| } | |||
| for (std::shared_ptr<CloneableLRFilter<float>>& filter : state.allpassFilters) { | |||
| filter->reset(); | |||
| } | |||
| for (juce::AudioBuffer<float>& buffer : state.buffers) { | |||
| buffer.clear(); | |||
| } | |||
| } | |||
| void processBlock(CrossoverState& state, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead) { | |||
| const int numFilterChannels {canDoStereoSplitTypes(state.config.layout) ? 2 : 1}; | |||
| const size_t numCrossovers {state.bands.size() - 1}; | |||
| // First split everything into bands and apply the processing | |||
| for (int crossoverNumber {0}; crossoverNumber < numCrossovers; crossoverNumber++) { | |||
| // We need to make a copy of the input buffer before processing | |||
| // lowBuffer will be lowpassed, highBuffer will be highpassed | |||
| juce::AudioBuffer<float>& lowBuffer = crossoverNumber == 0 ? buffer : state.buffers[crossoverNumber - 1]; | |||
| juce::AudioBuffer<float>& highBuffer = state.buffers[crossoverNumber]; | |||
| highBuffer.makeCopyOf(lowBuffer); | |||
| { | |||
| juce::dsp::AudioBlock<float> block(juce::dsp::AudioBlock<float>(lowBuffer).getSubsetChannelBlock(0, numFilterChannels)); | |||
| juce::dsp::ProcessContextReplacing context(block); | |||
| state.lowpassFilters[crossoverNumber]->process(context); | |||
| // Crop the internal buffer in case the DAW has provided a buffer smaller than the specified block size in prepareToPlay | |||
| juce::AudioBuffer<float> lowBufferCropped(lowBuffer.getArrayOfWritePointers(), lowBuffer.getNumChannels(), buffer.getNumSamples()); | |||
| ChainProcessors::processBlock(*state.bands[crossoverNumber].chain.get(), lowBufferCropped, midiMessages, newPlayHead); | |||
| } | |||
| { | |||
| juce::dsp::AudioBlock<float> block(juce::dsp::AudioBlock<float>(highBuffer).getSubsetChannelBlock(0, numFilterChannels)); | |||
| juce::dsp::ProcessContextReplacing context(block); | |||
| state.highpassFilters[crossoverNumber]->process(context); | |||
| // If this is the last band we need to apply the processing | |||
| if (crossoverNumber + 1 == numCrossovers) { | |||
| // Crop the internal buffer in case the DAW has provided a buffer smaller than the specified block size in prepareToPlay | |||
| juce::AudioBuffer<float> highBufferCropped(highBuffer.getArrayOfWritePointers(), highBuffer.getNumChannels(), buffer.getNumSamples()); | |||
| ChainProcessors::processBlock(*state.bands[crossoverNumber + 1].chain.get(), highBufferCropped, midiMessages, newPlayHead); | |||
| } | |||
| } | |||
| } | |||
| // Finally add the bands back together | |||
| if (state.numBandsSoloed > 0 && !state.bands[0].isSoloed) { | |||
| buffer.clear(); | |||
| } | |||
| for (int crossoverNumber {0}; crossoverNumber < numCrossovers; crossoverNumber++) { | |||
| // If there is another crossover after this one, we need to use an allpass to rotate the phase of the lower bands | |||
| if (crossoverNumber + 1 < numCrossovers) { | |||
| juce::dsp::AudioBlock<float> block(juce::dsp::AudioBlock<float>(buffer).getSubsetChannelBlock(0, numFilterChannels)); | |||
| juce::dsp::ProcessContextReplacing context(block); | |||
| state.allpassFilters[crossoverNumber]->process(context); | |||
| } | |||
| if (state.numBandsSoloed == 0 || state.bands[crossoverNumber + 1].isSoloed) { | |||
| for (int channelNumber {0}; channelNumber < numFilterChannels; channelNumber++) { | |||
| buffer.addFrom(channelNumber, 0, state.buffers[crossoverNumber], channelNumber, 0, buffer.getNumSamples()); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| #pragma once | |||
| #include "CrossoverState.hpp" | |||
| namespace CrossoverProcessors { | |||
| void prepareToPlay(CrossoverState& state, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout); | |||
| void reset(CrossoverState& state); | |||
| void processBlock(CrossoverState& state, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead); | |||
| } | |||
| @@ -0,0 +1,113 @@ | |||
| #include "ModulationProcessors.hpp" | |||
| #include "WEFilters/PerlinSource.hpp" | |||
| namespace Mi = ModelInterface; | |||
| namespace ModulationProcessors { | |||
| void prepareToPlay(Mi::ModulationSourcesState& state, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout) { | |||
| state.hostConfig.sampleRate = sampleRate; | |||
| state.hostConfig.blockSize = samplesPerBlock; | |||
| state.hostConfig.layout = layout; | |||
| for (std::shared_ptr<Mi::CloneableLFO>& lfo : state.lfos) { | |||
| lfo->setSampleRate(sampleRate); | |||
| } | |||
| for (std::shared_ptr<Mi::EnvelopeWrapper>& env : state.envelopes) { | |||
| env->envelope->setSampleRate(sampleRate); | |||
| } | |||
| for (std::shared_ptr<WECore::Perlin::PerlinSource>& random : state.randomSources) { | |||
| random->setSampleRate(sampleRate); | |||
| } | |||
| } | |||
| void reset(Mi::ModulationSourcesState& state) { | |||
| for (std::shared_ptr<Mi::CloneableLFO>& lfo : state.lfos) { | |||
| lfo->reset(); | |||
| } | |||
| for (std::shared_ptr<Mi::EnvelopeWrapper>& env : state.envelopes) { | |||
| env->envelope->reset(); | |||
| } | |||
| for (std::shared_ptr<WECore::Perlin::PerlinSource>& random : state.randomSources) { | |||
| random->reset(); | |||
| } | |||
| } | |||
| void processBlock(Mi::ModulationSourcesState& state, juce::AudioBuffer<float>& buffer, juce::AudioPlayHead::CurrentPositionInfo tempoInfo) { | |||
| const int totalNumInputChannels = buffer.getNumChannels(); | |||
| for (std::shared_ptr<Mi::CloneableLFO>& lfo : state.lfos) { | |||
| lfo->prepareForNextBuffer(tempoInfo.bpm, tempoInfo.timeInSeconds); | |||
| } | |||
| // TODO this could be faster | |||
| // We go through each sample then each source - sources can be dependent on each other, so | |||
| // we only advance each of them one sample at a time | |||
| for (int sampleIndex {0}; sampleIndex < buffer.getNumSamples(); sampleIndex++) { | |||
| // LFOs | |||
| for (std::shared_ptr<Mi::CloneableLFO>& lfo : state.lfos) { | |||
| lfo->getNextOutput(0); | |||
| } | |||
| // ENVs | |||
| for (std::shared_ptr<Mi::EnvelopeWrapper> env : state.envelopes) { | |||
| // Figure out which channels we need to be looking at | |||
| int startChannel {0}; | |||
| int endChannel {0}; | |||
| if (env->useSidechainInput) { | |||
| startChannel = state.hostConfig.layout.getMainInputChannels(); | |||
| endChannel = totalNumInputChannels; | |||
| } else { | |||
| startChannel = 0; | |||
| endChannel = state.hostConfig.layout.getMainInputChannels(); | |||
| } | |||
| // Average the samples across all channels | |||
| float averageSample {0}; | |||
| for (int channelIndex {startChannel}; channelIndex < endChannel; channelIndex++) { | |||
| averageSample += buffer.getReadPointer(channelIndex)[sampleIndex]; | |||
| } | |||
| averageSample /= (endChannel - startChannel); | |||
| env->envelope->getNextOutput(averageSample); | |||
| } | |||
| // Random | |||
| for (std::shared_ptr<WECore::Perlin::PerlinSource>& random : state.randomSources) { | |||
| random->getNextOutput(0); | |||
| } | |||
| } | |||
| } | |||
| double getLfoModulationValue(Mi::ModulationSourcesState& state, int lfoNumber) { | |||
| const int index {lfoNumber - 1}; | |||
| if (state.lfos.size() > index) { | |||
| return state.lfos[index]->getLastOutput(); | |||
| } | |||
| return 0; | |||
| } | |||
| double getEnvelopeModulationValue(Mi::ModulationSourcesState& state, int envelopeNumber) { | |||
| const int index {envelopeNumber - 1}; | |||
| if (state.envelopes.size() > index) { | |||
| return state.envelopes[index]->envelope->getLastOutput() * state.envelopes[index]->amount; | |||
| } | |||
| return 0; | |||
| } | |||
| double getRandomModulationValue(ModelInterface::ModulationSourcesState& state, int randomNumber) { | |||
| const int index {randomNumber - 1}; | |||
| if (state.randomSources.size() > index) { | |||
| return state.randomSources[index]->getLastOutput(); | |||
| } | |||
| return 0; | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "DataModelInterface.hpp" | |||
| namespace ModulationProcessors { | |||
| void prepareToPlay(ModelInterface::ModulationSourcesState& state, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout); | |||
| void releaseResources(ModelInterface::ModulationSourcesState& state); | |||
| void reset(ModelInterface::ModulationSourcesState& state); | |||
| void processBlock(ModelInterface::ModulationSourcesState& state, juce::AudioBuffer<float>& buffer, juce::AudioPlayHead::CurrentPositionInfo tempoInfo); | |||
| double getLfoModulationValue(ModelInterface::ModulationSourcesState& state, int lfoNumber); | |||
| double getEnvelopeModulationValue(ModelInterface::ModulationSourcesState& state, int envelopeNumber); | |||
| double getRandomModulationValue(ModelInterface::ModulationSourcesState& state, int randomNumber); | |||
| } | |||
| @@ -0,0 +1,79 @@ | |||
| #include "ProcessingInterface.hpp" | |||
| #include "SplitterProcessors.hpp" | |||
| #include "ModulationProcessors.hpp" | |||
| namespace ModelInterface { | |||
| void prepareToPlay(StateManager& manager, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout) { | |||
| WECore::AudioSpinLock lock(manager.sharedMutex); | |||
| SplitterState& splitter = manager.getSplitterStateUnsafe(); | |||
| ModulationSourcesState& sources = *manager.getSourcesStateUnsafe(); | |||
| ModulationProcessors::prepareToPlay(sources, sampleRate, samplesPerBlock, layout); | |||
| if (splitter.splitter != nullptr) { | |||
| SplitterProcessors::prepareToPlay(*splitter.splitter, sampleRate, samplesPerBlock, layout); | |||
| } | |||
| } | |||
| void releaseResources(StateManager& manager) { | |||
| WECore::AudioSpinLock lock(manager.sharedMutex); | |||
| SplitterState& splitter = manager.getSplitterStateUnsafe(); | |||
| if (splitter.splitter != nullptr) { | |||
| SplitterProcessors::releaseResources(*splitter.splitter.get()); | |||
| } | |||
| } | |||
| void reset(StateManager& manager) { | |||
| WECore::AudioSpinLock lock(manager.sharedMutex); | |||
| SplitterState& splitter = manager.getSplitterStateUnsafe(); | |||
| ModulationSourcesState& sources = *manager.getSourcesStateUnsafe(); | |||
| ModulationProcessors::reset(sources); | |||
| if (splitter.splitter != nullptr) { | |||
| SplitterProcessors::reset(*splitter.splitter.get()); | |||
| } | |||
| } | |||
| void processBlock(StateManager& manager, | |||
| juce::AudioBuffer<float>& buffer, | |||
| juce::MidiBuffer& midiMessages, | |||
| juce::AudioPlayHead* newPlayHead, | |||
| juce::AudioPlayHead::CurrentPositionInfo tempoInfo) { | |||
| // Use the try lock on the audio thread | |||
| WECore::AudioSpinTryLock lock(manager.sharedMutex); | |||
| if (lock.isLocked()) { | |||
| SplitterState& splitter = manager.getSplitterStateUnsafe(); | |||
| ModulationSourcesState& sources = *manager.getSourcesStateUnsafe(); | |||
| // Advance the modulation sources | |||
| // (the envelopes need to be done now before we overwrite the buffer) | |||
| ModulationProcessors::processBlock(sources, buffer, tempoInfo); | |||
| // TODO LFOs should still be advanced even if the lock is not acquired to stop them | |||
| // drifting out of sync | |||
| if (splitter.splitter != nullptr) { | |||
| SplitterProcessors::processBlock(*splitter.splitter, buffer, midiMessages, newPlayHead); | |||
| } | |||
| } | |||
| } | |||
| double getLfoModulationValue(StateManager& manager, int lfoNumber) { | |||
| // No locks here - they're called from ChainSlotProcessors::prepareToPlay so will already be locked | |||
| return ModulationProcessors::getLfoModulationValue(*manager.getSourcesStateUnsafe(), lfoNumber); | |||
| } | |||
| double getEnvelopeModulationValue(StateManager& manager, int envelopeNumber) { | |||
| // No locks here - they're called from ChainSlotProcessors::prepareToPlay so will already be locked | |||
| return ModulationProcessors::getEnvelopeModulationValue(*manager.getSourcesStateUnsafe(), envelopeNumber); | |||
| } | |||
| double getRandomModulationValue(StateManager& manager, int randomNumber) { | |||
| // No locks here - they're called from ChainSlotProcessors::prepareToPlay so will already be locked | |||
| return ModulationProcessors::getRandomModulationValue(*manager.getSourcesStateUnsafe(), randomNumber); | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| #pragma once | |||
| #include "DataModelInterface.hpp" | |||
| namespace ModelInterface { | |||
| void prepareToPlay(StateManager& manager, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout); | |||
| void releaseResources(StateManager& manager); | |||
| void reset(StateManager& manager); | |||
| void processBlock(StateManager& manager, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead, juce::AudioPlayHead::CurrentPositionInfo tempoInfo); | |||
| // Do not call from anything outside the model - they assume the locks are already held | |||
| double getLfoModulationValue(StateManager& manager, int lfoNumber); | |||
| double getEnvelopeModulationValue(StateManager& manager, int envelopeNumber); | |||
| double getRandomModulationValue(StateManager& manager, int randomNumber); | |||
| } | |||
| @@ -0,0 +1,235 @@ | |||
| #include "SplitterProcessors.hpp" | |||
| #include "ChainProcessors.hpp" | |||
| namespace { | |||
| void copyBuffer(juce::AudioBuffer<float>& source, juce::AudioBuffer<float>& destination) { | |||
| if (source.getNumSamples() == destination.getNumSamples()) { | |||
| const int channelsToCopy {std::min(source.getNumChannels(), destination.getNumChannels())}; | |||
| // For each channel, copy each sample | |||
| for (int channelIndex {0}; channelIndex < channelsToCopy; channelIndex++) { | |||
| const float* readPointer = source.getReadPointer(channelIndex); | |||
| float* writePointer = destination.getWritePointer(channelIndex); | |||
| juce::FloatVectorOperations::copy(writePointer, readPointer, source.getNumSamples()); | |||
| } | |||
| } | |||
| } | |||
| void addBuffers(juce::AudioBuffer<float>& source, juce::AudioBuffer<float>& destination) { | |||
| if (source.getNumSamples() == destination.getNumSamples()) { | |||
| const int channelsToCopy {std::min(source.getNumChannels(), destination.getNumChannels())}; | |||
| // For each channel, add each sample | |||
| for (int channelIndex {0}; channelIndex < channelsToCopy; channelIndex++) { | |||
| const float* readPointer = source.getReadPointer(channelIndex); | |||
| float* writePointer = destination.getWritePointer(channelIndex); | |||
| juce::FloatVectorOperations::add(writePointer, readPointer, source.getNumSamples()); | |||
| } | |||
| } | |||
| } | |||
| void processBlockSeries(PluginSplitterSeries& splitter, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead) { | |||
| ChainProcessors::processBlock(*(splitter.chains[0].chain.get()), buffer, midiMessages, newPlayHead); | |||
| } | |||
| void processBlockParallel(PluginSplitterParallel& splitter, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead) { | |||
| splitter.outputBuffer->clear(); | |||
| // Crop the internal buffers in case the DAW has provided a buffer smaller than the specified block size in prepareToPlay | |||
| juce::AudioBuffer<float> inputBufferCropped(splitter.inputBuffer->getArrayOfWritePointers(), splitter.inputBuffer->getNumChannels(), buffer.getNumSamples()); | |||
| juce::AudioBuffer<float> outputBufferCropped(splitter.outputBuffer->getArrayOfWritePointers(), splitter.outputBuffer->getNumChannels(), buffer.getNumSamples()); | |||
| for (PluginChainWrapper& chain : splitter.chains) { | |||
| // Only process if no bands are soloed or this one is soloed | |||
| if (splitter.numChainsSoloed == 0 || chain.isSoloed) { | |||
| // Make a copy of the input buffer for us to process, preserving the original for the other | |||
| // chains | |||
| // TODO: do the same for midi | |||
| copyBuffer(buffer, inputBufferCropped); | |||
| // Process the newly copied buffer | |||
| ChainProcessors::processBlock(*(chain.chain.get()), inputBufferCropped, midiMessages, newPlayHead); | |||
| // Add the output of this chain to the output buffer | |||
| addBuffers(inputBufferCropped, outputBufferCropped); | |||
| } | |||
| } | |||
| // Overwrite the original buffer with our own output | |||
| copyBuffer(outputBufferCropped, buffer); | |||
| } | |||
| void processBlockMultiband(PluginSplitterMultiband& splitter, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead) { | |||
| splitter.fftProvider.processBlock(buffer); | |||
| CrossoverProcessors::processBlock(*splitter.crossover.get(), buffer, midiMessages, newPlayHead); | |||
| } | |||
| void processBlockLeftRight(PluginSplitterLeftRight& splitter, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead) { | |||
| // TODO: maybe this should be done using mono buffers? (depends if plugins can handle it reliably) | |||
| // Make sure to clear the buffers each time, as on a previous call the plugins may have left | |||
| // data in the unused channel of each buffer, and since it's not used it won't get implicitly | |||
| // overwritten (but it'll still be copied to the output) | |||
| splitter.leftBuffer->clear(); | |||
| splitter.rightBuffer->clear(); | |||
| // We only need to check the first two chains | |||
| // (otherwise if the user is in parallel/multiband and soloes a chain above the first two | |||
| // and then switches to left/right, the output will be muted) | |||
| const bool isAnythingSoloed {splitter.chains[0].isSoloed || splitter.chains[1].isSoloed}; | |||
| const bool processLeftChain {!isAnythingSoloed || splitter.chains[0].isSoloed}; | |||
| const bool processRightChain {!isAnythingSoloed || splitter.chains[1].isSoloed}; | |||
| // Copy the left and right channels to separate buffers | |||
| if (processLeftChain) { | |||
| const float* leftRead {buffer.getReadPointer(0)}; | |||
| float* leftWrite {splitter.leftBuffer->getWritePointer(0)}; | |||
| juce::FloatVectorOperations::copy(leftWrite, leftRead, buffer.getNumSamples()); | |||
| } | |||
| if (processRightChain) { | |||
| const float* rightRead {buffer.getReadPointer(1)}; | |||
| float* rightWrite {splitter.rightBuffer->getWritePointer(1)}; | |||
| juce::FloatVectorOperations::copy(rightWrite, rightRead, buffer.getNumSamples()); | |||
| } | |||
| // Now the input has been copied we can clear the original | |||
| buffer.clear(); | |||
| // Process the left chain | |||
| if (processLeftChain) { | |||
| // Crop the internal buffer in case the DAW has provided a buffer smaller than the specified block size in prepareToPlay | |||
| juce::AudioBuffer<float> leftBufferCropped(splitter.leftBuffer->getArrayOfWritePointers(), splitter.leftBuffer->getNumChannels(), buffer.getNumSamples()); | |||
| ChainProcessors::processBlock(*(splitter.chains[0].chain.get()), leftBufferCropped, midiMessages, newPlayHead); | |||
| addBuffers(leftBufferCropped, buffer); | |||
| } | |||
| // Process the right chain | |||
| if (processRightChain) { | |||
| // Crop the internal buffer in case the DAW has provided a buffer smaller than the specified block size in prepareToPlay | |||
| juce::AudioBuffer<float> rightBufferCropped(splitter.rightBuffer->getArrayOfWritePointers(), splitter.rightBuffer->getNumChannels(), buffer.getNumSamples()); | |||
| ChainProcessors::processBlock(*(splitter.chains[1].chain.get()), rightBufferCropped, midiMessages, newPlayHead); | |||
| addBuffers(rightBufferCropped, buffer); | |||
| } | |||
| } | |||
| void processBlockMidSide(PluginSplitterMidSide& splitter, juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead) { | |||
| // TODO: check if this can be done using mono buffers that refer to the original buffer rather | |||
| // than copying to new ones (guest plugins don't seem to like mono buffers) | |||
| // Make sure to clear the buffers each time, as on a previous call the plugins may have left | |||
| // data in the unused channel of each buffer, and since it's not used it won't get implicitly | |||
| // overwritten (but it'll still be copied to the output) | |||
| splitter.midBuffer->clear(); | |||
| splitter.sideBuffer->clear(); | |||
| // Convert the left/right buffer to mid/side | |||
| const float* leftRead {buffer.getReadPointer(0)}; | |||
| const float* rightRead {buffer.getReadPointer(1)}; | |||
| float* midWrite {splitter.midBuffer->getWritePointer(0)}; | |||
| float* sideWrite {splitter.sideBuffer->getWritePointer(0)}; | |||
| const int numSamples {buffer.getNumSamples()}; | |||
| // Add the right channel to get the mid, subtract it to get the side | |||
| juce::FloatVectorOperations::add(midWrite, leftRead, rightRead, numSamples); | |||
| juce::FloatVectorOperations::subtract(sideWrite, leftRead, rightRead, numSamples); | |||
| // Multiply both by 0.5 | |||
| juce::FloatVectorOperations::multiply(midWrite, 0.5, numSamples); | |||
| juce::FloatVectorOperations::multiply(sideWrite, 0.5, numSamples); | |||
| // We only need to check the first two chains | |||
| // (otherwise if the user is in parallel/multiband and soloes a chain above the first two | |||
| // and then switches to mid/side, the output will be muted) | |||
| const bool isAnythingSoloed {splitter.chains[0].isSoloed || splitter.chains[1].isSoloed}; | |||
| // Process the buffers | |||
| if (!isAnythingSoloed || splitter.chains[0].isSoloed) { | |||
| // Crop the internal buffer in case the DAW has provided a buffer smaller than the specified block size in prepareToPlay | |||
| juce::AudioBuffer<float> midBufferCropped(splitter.midBuffer->getArrayOfWritePointers(), splitter.midBuffer->getNumChannels(), numSamples); | |||
| ChainProcessors::processBlock(*(splitter.chains[0].chain.get()), midBufferCropped, midiMessages, newPlayHead); | |||
| } else { | |||
| // Mute the mid channel if only the other one is soloed | |||
| juce::FloatVectorOperations::fill(midWrite, 0, numSamples); | |||
| } | |||
| if (!isAnythingSoloed || splitter.chains[1].isSoloed) { | |||
| // Crop the internal buffer in case the DAW has provided a buffer smaller than the specified block size in prepareToPlay | |||
| juce::AudioBuffer<float> sideBufferCropped(splitter.sideBuffer->getArrayOfWritePointers(), splitter.sideBuffer->getNumChannels(), numSamples); | |||
| ChainProcessors::processBlock(*(splitter.chains[1].chain.get()), sideBufferCropped, midiMessages, newPlayHead); | |||
| } else { | |||
| // Mute the side channel if only the other one is soloed | |||
| juce::FloatVectorOperations::fill(sideWrite, 0, numSamples); | |||
| } | |||
| // Convert from mid/side back to left/right, overwrite the original buffer with our own output | |||
| float* leftWrite {buffer.getWritePointer(0)}; | |||
| float* rightWrite {buffer.getWritePointer(1)}; | |||
| const float* midRead {splitter.midBuffer->getReadPointer(0)}; | |||
| const float* sideRead {splitter.sideBuffer->getReadPointer(0)}; | |||
| // Add mid and side to get the left buffer, subtract them to get the right buffer | |||
| juce::FloatVectorOperations::add(leftWrite, midRead, sideRead, numSamples); | |||
| juce::FloatVectorOperations::subtract(rightWrite, midRead, sideRead, numSamples); | |||
| } | |||
| } | |||
| namespace SplitterProcessors { | |||
| void prepareToPlay(PluginSplitter& splitter, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout) { | |||
| splitter.config.sampleRate = sampleRate; | |||
| splitter.config.blockSize = samplesPerBlock; | |||
| splitter.config.layout = layout; | |||
| if (auto parallelSplitter = dynamic_cast<PluginSplitterParallel*>(&splitter)) { | |||
| parallelSplitter->inputBuffer.reset(new juce::AudioBuffer<float>(getTotalNumInputChannels(layout), samplesPerBlock)); | |||
| parallelSplitter->outputBuffer.reset(new juce::AudioBuffer<float>(2, samplesPerBlock)); // stereo main | |||
| } else if (auto multibandSplitter = dynamic_cast<PluginSplitterMultiband*>(&splitter)) { | |||
| CrossoverProcessors::prepareToPlay(*multibandSplitter->crossover.get(), sampleRate, samplesPerBlock, layout); | |||
| CrossoverProcessors::reset(*multibandSplitter->crossover.get()); | |||
| multibandSplitter->fftProvider.reset(); | |||
| multibandSplitter->fftProvider.setSampleRate(sampleRate); | |||
| multibandSplitter->fftProvider.setIsStereo(canDoStereoSplitTypes(layout)); | |||
| } else if (auto leftRightSplitter = dynamic_cast<PluginSplitterLeftRight*>(&splitter)) { | |||
| leftRightSplitter->leftBuffer.reset(new juce::AudioBuffer<float>(getTotalNumInputChannels(layout), samplesPerBlock)); | |||
| leftRightSplitter->rightBuffer.reset(new juce::AudioBuffer<float>(getTotalNumInputChannels(layout), samplesPerBlock)); | |||
| } else if (auto midSideSplitter = dynamic_cast<PluginSplitterMidSide*>(&splitter)) { | |||
| midSideSplitter->midBuffer.reset(new juce::AudioBuffer<float>(getTotalNumInputChannels(layout), samplesPerBlock)); | |||
| midSideSplitter->sideBuffer.reset(new juce::AudioBuffer<float>(getTotalNumInputChannels(layout), samplesPerBlock)); | |||
| } | |||
| for (PluginChainWrapper& chainWrapper : splitter.chains) { | |||
| ChainProcessors::prepareToPlay(*chainWrapper.chain.get(), splitter.config); | |||
| } | |||
| } | |||
| void releaseResources(PluginSplitter& splitter) { | |||
| for (PluginChainWrapper& chainWrapper : splitter.chains) { | |||
| ChainProcessors::releaseResources(*chainWrapper.chain.get()); | |||
| } | |||
| } | |||
| void reset(PluginSplitter& splitter) { | |||
| for (PluginChainWrapper& chainWrapper : splitter.chains) { | |||
| ChainProcessors::reset(*chainWrapper.chain.get()); | |||
| } | |||
| } | |||
| void processBlock(PluginSplitter& splitter, | |||
| juce::AudioBuffer<float>& buffer, | |||
| juce::MidiBuffer& midiMessages, | |||
| juce::AudioPlayHead* newPlayHead) { | |||
| if (auto seriesSplitter = dynamic_cast<PluginSplitterSeries*>(&splitter)) { | |||
| processBlockSeries(*seriesSplitter, buffer, midiMessages, newPlayHead); | |||
| } else if (auto parallelSplitter = dynamic_cast<PluginSplitterParallel*>(&splitter)) { | |||
| processBlockParallel(*parallelSplitter, buffer, midiMessages, newPlayHead); | |||
| } else if (auto multibandSplitter = dynamic_cast<PluginSplitterMultiband*>(&splitter)) { | |||
| processBlockMultiband(*multibandSplitter, buffer, midiMessages, newPlayHead); | |||
| } else if (auto leftRightSplitter = dynamic_cast<PluginSplitterLeftRight*>(&splitter)) { | |||
| processBlockLeftRight(*leftRightSplitter, buffer, midiMessages, newPlayHead); | |||
| } else if (auto midSideSplitter = dynamic_cast<PluginSplitterMidSide*>(&splitter)) { | |||
| processBlockMidSide(*midSideSplitter, buffer, midiMessages, newPlayHead); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| #pragma once | |||
| #include <JuceHeader.h> | |||
| #include "PluginSplitter.hpp" | |||
| namespace SplitterProcessors { | |||
| void prepareToPlay(PluginSplitter& splitter, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout); | |||
| void releaseResources(PluginSplitter& splitter); | |||
| void reset(PluginSplitter& splitter); | |||
| void processBlock(PluginSplitter& splitter, | |||
| juce::AudioBuffer<float>& buffer, | |||
| juce::MidiBuffer& midiMessages, | |||
| juce::AudioPlayHead* newPlayHead); | |||
| } | |||
| @@ -0,0 +1,424 @@ | |||
| #include "catch.hpp" | |||
| #include "TestUtils.hpp" | |||
| #include "SplitterMutators.hpp" | |||
| #include "SplitterProcessors.hpp" | |||
| namespace { | |||
| constexpr int NUM_SAMPLES {10}; | |||
| constexpr int SAMPLE_RATE {2000}; | |||
| class ProcessorTestPluginInstance : public TestUtils::TestPluginInstance { | |||
| public: | |||
| std::optional<std::function<void(double, int)>> onPrepareToPlay; | |||
| std::optional<std::function<void()>> onReleaseResources; | |||
| std::optional<std::function<void()>> onReset; | |||
| std::optional<std::function<void(juce::AudioBuffer<float>&, juce::MidiBuffer&)>> onProcess; | |||
| ProcessorTestPluginInstance() = default; | |||
| void prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock) override { | |||
| if (onPrepareToPlay.has_value()) { | |||
| onPrepareToPlay.value()(sampleRate, maximumExpectedSamplesPerBlock); | |||
| } | |||
| } | |||
| void releaseResources() override { | |||
| if (onReleaseResources.has_value()) { | |||
| onReleaseResources.value()(); | |||
| } | |||
| } | |||
| void reset() override { | |||
| if (onReset.has_value()) { | |||
| onReset.value()(); | |||
| } | |||
| } | |||
| void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override { | |||
| if (onProcess.has_value()) { | |||
| onProcess.value()(buffer, midiMessages); | |||
| } | |||
| } | |||
| }; | |||
| } | |||
| SCENARIO("SplitterProcessors: Silence in = silence out") { | |||
| GIVEN("A splitter and a buffer of silence") { | |||
| HostConfiguration config; | |||
| config.sampleRate = SAMPLE_RATE; | |||
| config.blockSize = NUM_SAMPLES; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| auto latencyCallback = [](int) { | |||
| // Do nothing | |||
| }; | |||
| // Mono and stereo | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| config.layout = layout; | |||
| // Do this after calling GENERATE so it gets reset for each test | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIdx), 0, buffer.getNumSamples()); | |||
| } | |||
| const juce::String splitTypeString = GENERATE( | |||
| juce::String(XML_SPLIT_TYPE_SERIES_STR), | |||
| juce::String(XML_SPLIT_TYPE_PARALLEL_STR), | |||
| // juce::String(XML_SPLIT_TYPE_MULTIBAND_STR), // TODO multiband crashes on vDSP_destroy_fftsetup | |||
| juce::String(XML_SPLIT_TYPE_LEFTRIGHT_STR), | |||
| juce::String(XML_SPLIT_TYPE_MIDSIDE_STR) | |||
| ); | |||
| std::shared_ptr<PluginSplitter> splitter; | |||
| if (splitTypeString == XML_SPLIT_TYPE_SERIES_STR) { | |||
| auto splitterSeries = std::make_shared<PluginSplitterSeries>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterSeries); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { | |||
| auto splitterParallel = std::make_shared<PluginSplitterParallel>(config, modulationCallback, latencyCallback); | |||
| SplitterMutators::addChain(splitterParallel); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterParallel); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { | |||
| auto splitterMultiband = std::make_shared<PluginSplitterMultiband>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterMultiband); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR) { | |||
| auto splitterLeftRight = std::make_shared<PluginSplitterLeftRight>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterLeftRight); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { | |||
| auto splitterMidSide = std::make_shared<PluginSplitterMidSide>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterMidSide); | |||
| } | |||
| const bool isChainSoloed = GENERATE(false, true); | |||
| SplitterMutators::setChainSolo(splitter, 0, isChainSoloed); | |||
| // Add a gain stage to the first chain | |||
| SplitterMutators::insertGainStage(splitter, 0, 0); | |||
| SplitterMutators::setGainLinear(splitter, 0, 0, 0.5); | |||
| SplitterMutators::setPan(splitter, 0, 0, -0.5); | |||
| // Add a plugin to the second chain | |||
| auto plugin = std::make_shared<ProcessorTestPluginInstance>(); | |||
| plugin->onProcess = [](juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::multiply(buffer.getWritePointer(channelIdx), 0.3, buffer.getNumSamples()); | |||
| } | |||
| }; | |||
| SplitterMutators::insertPlugin(splitter, plugin, 1, 0); | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| if (buffer.getNumChannels() == 1 && | |||
| (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR || splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR)) { | |||
| // Skip these - can't use mono for these split types | |||
| } else { | |||
| SplitterProcessors::prepareToPlay(*(splitter.get()), SAMPLE_RATE, NUM_SAMPLES, layout); | |||
| SplitterProcessors::processBlock(*(splitter.get()), buffer, midiBuffer, nullptr); | |||
| } | |||
| THEN("The buffer contains silence") { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| const auto readPtr = buffer.getReadPointer(channelIdx); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == Approx(0.0f)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("SplitterProcessors: Gain stage and plugin processing is applied correctly") { | |||
| GIVEN("A splitter and a buffer of 1's") { | |||
| HostConfiguration config; | |||
| config.sampleRate = SAMPLE_RATE; | |||
| config.blockSize = NUM_SAMPLES; | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| return 0.0f; | |||
| }; | |||
| auto latencyCallback = [](int) { | |||
| // Do nothing | |||
| }; | |||
| // Mono and stereo | |||
| auto [layout, buffer] = GENERATE( | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), | |||
| juce::AudioBuffer<float>(1, NUM_SAMPLES) | |||
| ), | |||
| std::make_pair<juce::AudioProcessor::BusesLayout, juce::AudioBuffer<float>>( | |||
| TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), | |||
| juce::AudioBuffer<float>(2, NUM_SAMPLES) | |||
| ) | |||
| ); | |||
| config.layout = layout; | |||
| const juce::String splitTypeString = GENERATE( | |||
| juce::String(XML_SPLIT_TYPE_SERIES_STR), | |||
| juce::String(XML_SPLIT_TYPE_PARALLEL_STR), | |||
| // juce::String(XML_SPLIT_TYPE_MULTIBAND_STR), // TODO multiband crashes on vDSP_destroy_fftsetup | |||
| juce::String(XML_SPLIT_TYPE_LEFTRIGHT_STR), | |||
| juce::String(XML_SPLIT_TYPE_MIDSIDE_STR) | |||
| ); | |||
| std::shared_ptr<PluginSplitter> splitter; | |||
| if (splitTypeString == XML_SPLIT_TYPE_SERIES_STR) { | |||
| auto splitterSeries = std::make_shared<PluginSplitterSeries>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterSeries); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { | |||
| auto splitterParallel = std::make_shared<PluginSplitterParallel>(config, modulationCallback, latencyCallback); | |||
| SplitterMutators::addChain(splitterParallel); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterParallel); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { | |||
| auto splitterMultiband = std::make_shared<PluginSplitterMultiband>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterMultiband); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR) { | |||
| auto splitterLeftRight = std::make_shared<PluginSplitterLeftRight>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterLeftRight); | |||
| } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { | |||
| auto splitterMidSide = std::make_shared<PluginSplitterMidSide>(config, modulationCallback, latencyCallback); | |||
| splitter = std::dynamic_pointer_cast<PluginSplitter>(splitterMidSide); | |||
| } | |||
| const bool isChainSoloed = GENERATE(false, true); | |||
| SplitterMutators::setChainSolo(splitter, 0, isChainSoloed); | |||
| // Add a gain stage to the first chain | |||
| SplitterMutators::insertGainStage(splitter, 0, 0); | |||
| SplitterMutators::setGainLinear(splitter, 0, 0, 0.5); | |||
| SplitterMutators::setPan(splitter, 0, 0, -0.5); | |||
| // Add a plugin to the second chain | |||
| auto plugin = std::make_shared<ProcessorTestPluginInstance>(); | |||
| plugin->onProcess = [](juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) { | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::add(buffer.getWritePointer(channelIdx), 0.3, buffer.getNumSamples()); | |||
| } | |||
| }; | |||
| SplitterMutators::insertPlugin(splitter, plugin, 1, 0); | |||
| // Do this after calling GENERATE so it gets reset for each test | |||
| for (int channelIdx {0}; channelIdx < buffer.getNumChannels(); channelIdx++) { | |||
| juce::FloatVectorOperations::fill(buffer.getWritePointer(channelIdx), 1, buffer.getNumSamples()); | |||
| } | |||
| WHEN("The buffer is processed") { | |||
| juce::MidiBuffer midiBuffer; | |||
| if (buffer.getNumChannels() == 1 && | |||
| (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR || splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR)) { | |||
| // Skip these - can't use mono for these split types | |||
| } else { | |||
| SplitterProcessors::prepareToPlay(*(splitter.get()), SAMPLE_RATE, NUM_SAMPLES, layout); | |||
| SplitterProcessors::processBlock(*(splitter.get()), buffer, midiBuffer, nullptr); | |||
| } | |||
| THEN("The buffer contains silence") { | |||
| if (buffer.getNumChannels() == 1) { | |||
| // Mono | |||
| if (!(splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR || splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR)) { | |||
| const float expectedSample { | |||
| splitTypeString == XML_SPLIT_TYPE_SERIES_STR || isChainSoloed ? 0.5f : | |||
| splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR ? 1.8f : 0.0f | |||
| }; | |||
| const auto readPtr = buffer.getReadPointer(0); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readPtr[sampleIdx] == Approx(expectedSample)); | |||
| } | |||
| } | |||
| } else { | |||
| // Stereo | |||
| const float expectedLeftSample { | |||
| splitTypeString == XML_SPLIT_TYPE_SERIES_STR || isChainSoloed ? 0.5f : | |||
| splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR ? 1.8f : | |||
| splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR ? 0.8f : | |||
| splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR ? 0.8f : 0.0f | |||
| }; | |||
| const float expectedRightSample { | |||
| splitTypeString == XML_SPLIT_TYPE_SERIES_STR ? 0.25f : | |||
| splitTypeString == XML_SPLIT_TYPE_SERIES_STR ? 0.25f : | |||
| splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR && !isChainSoloed ? 1.55f : | |||
| splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR && isChainSoloed ? 0.25f : | |||
| splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR && !isChainSoloed ? 1.3f : | |||
| splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR && isChainSoloed ? 0.0f : | |||
| splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR && !isChainSoloed ? 0.2f : | |||
| splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR && isChainSoloed ? 0.5f : 0.0f | |||
| }; | |||
| const auto readLeftPtr = buffer.getReadPointer(0); | |||
| const auto readRightPtr = buffer.getReadPointer(1); | |||
| for (int sampleIdx {0}; sampleIdx < buffer.getNumSamples(); sampleIdx++) { | |||
| CHECK(readLeftPtr[sampleIdx] == Approx(expectedLeftSample)); | |||
| CHECK(readRightPtr[sampleIdx] == Approx(expectedRightSample)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| SCENARIO("SplitterProcessors: Chain methods are called correctly") { | |||
| GIVEN("A parallel splitter and an empty buffer") { | |||
| HostConfiguration config; | |||
| config.sampleRate = SAMPLE_RATE; | |||
| config.blockSize = NUM_SAMPLES; | |||
| config.layout = TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()); | |||
| auto modulationCallback = [](int, MODULATION_TYPE) { | |||
| // Return something unique we can test for later | |||
| return 1.234f; | |||
| }; | |||
| auto latencyCallback = [](int) { | |||
| // Do nothing | |||
| }; | |||
| auto splitterParallel = std::make_shared<PluginSplitterParallel>(config, modulationCallback, latencyCallback); | |||
| SplitterMutators::addChain(splitterParallel); | |||
| auto plugin1 = std::make_shared<ProcessorTestPluginInstance>(); | |||
| auto plugin2 = std::make_shared<ProcessorTestPluginInstance>(); | |||
| SplitterMutators::insertPlugin(splitterParallel, plugin1, 0, 0); | |||
| SplitterMutators::insertPlugin(splitterParallel, plugin2, 1, 0); | |||
| WHEN("prepareToPlay is called") { | |||
| bool calledPrepareToPlay1 {false}; | |||
| plugin1->onPrepareToPlay = [&calledPrepareToPlay1](double sampleRate, int samplesPerBlock) { | |||
| calledPrepareToPlay1 = true; | |||
| CHECK(sampleRate == SAMPLE_RATE); | |||
| CHECK(samplesPerBlock == NUM_SAMPLES); | |||
| }; | |||
| bool calledPrepareToPlay2 {false}; | |||
| plugin2->onPrepareToPlay = [&calledPrepareToPlay2](double sampleRate, int samplesPerBlock) { | |||
| calledPrepareToPlay2 = true; | |||
| CHECK(sampleRate == SAMPLE_RATE); | |||
| CHECK(samplesPerBlock == NUM_SAMPLES); | |||
| }; | |||
| // Set the buffers to something random so we can check if they get reset | |||
| splitterParallel->inputBuffer.reset(new juce::AudioBuffer<float>(1, 5)); | |||
| splitterParallel->outputBuffer.reset(new juce::AudioBuffer<float>(3, 84)); | |||
| SplitterProcessors::prepareToPlay(*(splitterParallel.get()), SAMPLE_RATE, NUM_SAMPLES, config.layout); | |||
| THEN("Each chain's prepareToPlay is called with the correct arguments") { | |||
| CHECK(calledPrepareToPlay1); | |||
| CHECK(calledPrepareToPlay2); | |||
| CHECK(splitterParallel->config.sampleRate == SAMPLE_RATE); | |||
| CHECK(splitterParallel->config.blockSize == NUM_SAMPLES); | |||
| CHECK(splitterParallel->inputBuffer->getNumChannels() == 2); | |||
| CHECK(splitterParallel->inputBuffer->getNumSamples() == NUM_SAMPLES); | |||
| CHECK(splitterParallel->outputBuffer->getNumChannels() == 2); | |||
| CHECK(splitterParallel->outputBuffer->getNumSamples() == NUM_SAMPLES); | |||
| } | |||
| WHEN("The splitter is converted to series and prepareToPlay is called") { | |||
| calledPrepareToPlay1 = false; | |||
| calledPrepareToPlay2 = false; | |||
| auto splitterSeries = std::make_shared<PluginSplitterSeries>(splitterParallel); | |||
| SplitterProcessors::prepareToPlay(*(splitterSeries.get()), SAMPLE_RATE, NUM_SAMPLES, config.layout); | |||
| THEN("Each chain's prepareToPlay is called with the correct arguments") { | |||
| CHECK(calledPrepareToPlay1); | |||
| CHECK(calledPrepareToPlay2); | |||
| CHECK(splitterSeries->config.sampleRate == SAMPLE_RATE); | |||
| CHECK(splitterSeries->config.blockSize == NUM_SAMPLES); | |||
| } | |||
| } | |||
| } | |||
| WHEN("releaseResources is called") { | |||
| bool calledReleaseResources1 {false}; | |||
| plugin1->onReleaseResources = [&calledReleaseResources1]() { | |||
| calledReleaseResources1 = true; | |||
| }; | |||
| bool calledReleaseResources2 {false}; | |||
| plugin2->onReleaseResources = [&calledReleaseResources2]() { | |||
| calledReleaseResources2 = true; | |||
| }; | |||
| SplitterProcessors::releaseResources(*(splitterParallel.get())); | |||
| THEN("Each chain's releaseResources is called") { | |||
| CHECK(calledReleaseResources1); | |||
| CHECK(calledReleaseResources2); | |||
| } | |||
| WHEN("The splitter is converted to series and releaseResources is called") { | |||
| calledReleaseResources1 = false; | |||
| calledReleaseResources2 = false; | |||
| auto splitterSeries = std::make_shared<PluginSplitterSeries>(splitterParallel); | |||
| SplitterProcessors::releaseResources(*(splitterSeries.get())); | |||
| THEN("Each chain's releaseResources is called") { | |||
| CHECK(calledReleaseResources1); | |||
| CHECK(calledReleaseResources2); | |||
| } | |||
| } | |||
| } | |||
| WHEN("reset is called") { | |||
| bool calledReset1 {false}; | |||
| plugin1->onReset = [&calledReset1]() { | |||
| calledReset1 = true; | |||
| }; | |||
| bool calledReset2 {false}; | |||
| plugin2->onReset = [&calledReset2]() { | |||
| calledReset2 = true; | |||
| }; | |||
| SplitterProcessors::reset(*(splitterParallel.get())); | |||
| THEN("Each chain's reset is called") { | |||
| CHECK(calledReset1); | |||
| CHECK(calledReset2); | |||
| } | |||
| WHEN("The splitter is converted to series and reset is called") { | |||
| calledReset1 = false; | |||
| calledReset2 = false; | |||
| auto splitterSeries = std::make_shared<PluginSplitterSeries>(splitterParallel); | |||
| SplitterProcessors::reset(*(splitterSeries.get())); | |||
| THEN("Each chain's releaseResources is called") { | |||
| CHECK(calledReset1); | |||
| CHECK(calledReset2); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1 @@ | |||
| Contains code common to both Syndicate plugins | |||
| @@ -0,0 +1,12 @@ | |||
| /* | |||
| ============================================================================== | |||
| This file contains the basic startup code for a JUCE application. | |||
| ============================================================================== | |||
| */ | |||
| #include <JuceHeader.h> | |||
| #include "ServerApplication.h" | |||
| START_JUCE_APPLICATION(ServerApplication) | |||