Signed-off-by: falkTX <falktx@falktx.com>master
| @@ -165,11 +165,7 @@ | |||||
| @see VSTPluginFormat, VST3PluginFormat, AudioPluginFormat, AudioPluginFormatManager, JUCE_PLUGINHOST_VST, JUCE_PLUGINHOST_AU | @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 | /** Config: JUCE_PLUGINHOST_AU | ||||
| Enables the AudioUnit plugin hosting classes. This is Mac-only, of course. | Enables the AudioUnit plugin hosting classes. This is Mac-only, of course. | ||||
| @@ -188,6 +184,13 @@ | |||||
| #define JUCE_PLUGINHOST_LADSPA 0 | #define JUCE_PLUGINHOST_LADSPA 0 | ||||
| #endif | #endif | ||||
| /** Config: JUCE_PLUGINHOST_LV2 | |||||
| * Enables the LV2 plugin hosting classes. | |||||
| */ | |||||
| #ifndef JUCE_PLUGINHOST_LV2 | |||||
| #define JUCE_PLUGINHOST_LV2 0 | |||||
| #endif | |||||
| //============================================================================= | //============================================================================= | ||||
| // juce_audio_utils | // juce_audio_utils | ||||
| @@ -5,6 +5,7 @@ if linux_headless | |||||
| 'source/modules/juce_audio_basics/juce_audio_basics.cpp', | 'source/modules/juce_audio_basics/juce_audio_basics.cpp', | ||||
| 'source/modules/juce_audio_formats/juce_audio_formats.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.cpp', | ||||
| # 'source/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp', | |||||
| 'source/modules/juce_audio_utils/juce_audio_utils.cpp', | 'source/modules/juce_audio_utils/juce_audio_utils.cpp', | ||||
| 'source/modules/juce_core/juce_core.cpp', | 'source/modules/juce_core/juce_core.cpp', | ||||
| 'source/modules/juce_cryptography/juce_cryptography.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_basics/juce_audio_basics.cpp', | ||||
| 'source/modules/juce_audio_formats/juce_audio_formats.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.cpp', | ||||
| # 'source/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp', | |||||
| 'source/modules/juce_audio_utils/juce_audio_utils.cpp', | 'source/modules/juce_audio_utils/juce_audio_utils.cpp', | ||||
| 'source/modules/juce_core/juce_core.cpp', | 'source/modules/juce_core/juce_core.cpp', | ||||
| 'source/modules/juce_cryptography/juce_cryptography.cpp', | 'source/modules/juce_cryptography/juce_cryptography.cpp', | ||||
| @@ -35,6 +37,7 @@ juce7_devices_srcs = [ | |||||
| ] | ] | ||||
| juce7_extra_cpp_args = [ | juce7_extra_cpp_args = [ | ||||
| # '-DJUCE_PLUGINHOST_LV2=1', | |||||
| '-std=gnu++17', | '-std=gnu++17', | ||||
| '-Wno-non-virtual-dtor', | '-Wno-non-virtual-dtor', | ||||
| ] | ] | ||||
| @@ -61,6 +64,14 @@ lib_juce7 = static_library('juce7', | |||||
| include_directories('.'), | include_directories('.'), | ||||
| include_directories('source'), | include_directories('source'), | ||||
| include_directories('source/modules'), | 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'), | include_directories('../juced/source/dependancies/ladspa_sdk/src'), | ||||
| juce7_extra_include_dirs | juce7_extra_include_dirs | ||||
| ], | ], | ||||
| @@ -366,6 +366,10 @@ endif | |||||
| ############################################################################### | ############################################################################### | ||||
| # extra files to install | # 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') | if 'tal-noisemaker' in get_option('plugins') | ||||
| extra_lv2_preset_files = [ | extra_lv2_preset_files = [ | ||||
| 'TAL-NoiseMaker-Noise4U.lv2/manifest.ttl', | 'TAL-NoiseMaker-Noise4U.lv2/manifest.ttl', | ||||
| @@ -119,5 +119,6 @@ option('plugins', | |||||
| 'roth-air', | 'roth-air', | ||||
| 'swankyamp', | 'swankyamp', | ||||
| # juce7 | # 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 | else | ||||
| plugins = [ | plugins = [ | ||||
| 'syndicate' | |||||
| ] | ] | ||||
| endif | 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) | |||||