Browse Source

Add syndicate plugin, enable juce7 vst3 hosting

Signed-off-by: falkTX <falktx@falktx.com>
master
falkTX 4 months ago
parent
commit
d3b62da2e8
Signed by: falkTX <falktx@falktx.com> GPG Key ID: CDBAA37ABC74FBA0
100 changed files with 17041 additions and 5 deletions
  1. +8
    -5
      libs/juce7/AppConfig.h
  2. +11
    -0
      libs/juce7/meson.build
  3. +4
    -0
      meson.build
  4. +1
    -0
      meson_options.txt
  5. +185
    -0
      ports-juce7/changes.patch
  6. +1
    -0
      ports-juce7/meson.build
  7. +85
    -0
      ports-juce7/syndicate/AllCommon/AllUtils.h
  8. +62
    -0
      ports-juce7/syndicate/AllCommon/MainLogger.cpp
  9. +16
    -0
      ports-juce7/syndicate/AllCommon/MainLogger.h
  10. +12
    -0
      ports-juce7/syndicate/AllCommon/NullLogger.hpp
  11. +1
    -0
      ports-juce7/syndicate/AllCommon/README.md
  12. +21
    -0
      ports-juce7/syndicate/JuceHeader.h
  13. +28
    -0
      ports-juce7/syndicate/JucePluginCharacteristics.h
  14. +674
    -0
      ports-juce7/syndicate/LICENSE
  15. +77
    -0
      ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.cpp
  16. +21
    -0
      ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.h
  17. +246
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.cpp
  18. +70
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.hpp
  19. +140
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/CustomScanner.hpp
  20. +303
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.cpp
  21. +96
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.h
  22. +210
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.cpp
  23. +55
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.h
  24. +27
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusMessage.h
  25. +194
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.cpp
  26. +45
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.h
  27. +210
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.cpp
  28. +87
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.h
  29. +26
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorListParameters.h
  30. +78
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.cpp
  31. +35
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.h
  32. +59
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.cpp
  33. +26
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.h
  34. +107
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.cpp
  35. +29
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.hpp
  36. +34
    -0
      ports-juce7/syndicate/PluginCommon/PluginScanning/SelectorComponentStyle.h
  37. +59
    -0
      ports-juce7/syndicate/PluginCommon/PluginUtils.h
  38. +186
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots.hpp
  39. +104
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots_test.cpp
  40. +132
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.cpp
  41. +257
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.hpp
  42. +47
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine_test.cpp
  43. +118
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.cpp
  44. +125
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.hpp
  45. +48
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter_test.cpp
  46. +83
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources.hpp
  47. +107
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources_test.cpp
  48. +93
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState.hpp
  49. +108
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState_test.cpp
  50. +144
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/DataModelInterface.hpp
  51. +83
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.cpp
  52. +42
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.hpp
  53. +56
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.cpp
  54. +47
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.hpp
  55. +128
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/ModulationSourceDefinition.hpp
  56. +81
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain.hpp
  57. +86
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain_test.cpp
  58. +66
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.cpp
  59. +56
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.hpp
  60. +153
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator_test.cpp
  61. +379
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter.hpp
  62. +417
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter_test.cpp
  63. +9
    -0
      ports-juce7/syndicate/PluginCommon/Processor/DataModel/SplitTypes.hpp
  64. +5
    -0
      ports-juce7/syndicate/PluginCommon/Processor/ModelInterface.hpp
  65. +216
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.cpp
  66. +113
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.hpp
  67. +529
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators_test.cpp
  68. +180
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.cpp
  69. +16
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.hpp
  70. +733
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.cpp
  71. +83
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.hpp
  72. +1767
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.cpp
  73. +181
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.hpp
  74. +474
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.cpp
  75. +64
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.hpp
  76. +1300
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators_test.cpp
  77. +159
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlConsts.hpp
  78. +752
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.cpp
  79. +54
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.hpp
  80. +926
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader_test.cpp
  81. +245
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.cpp
  82. +24
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.hpp
  83. +643
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter_test.cpp
  84. +76
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.cpp
  85. +14
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.hpp
  86. +376
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors_test.cpp
  87. +134
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.cpp
  88. +19
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.hpp
  89. +437
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors_test.cpp
  90. +106
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.cpp
  91. +9
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.hpp
  92. +113
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.cpp
  93. +15
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.hpp
  94. +79
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.cpp
  95. +15
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.hpp
  96. +235
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.cpp
  97. +14
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.hpp
  98. +424
    -0
      ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors_test.cpp
  99. +1
    -0
      ports-juce7/syndicate/PluginCommon/README.md
  100. +12
    -0
      ports-juce7/syndicate/PluginScanServer/Main.cpp

+ 8
- 5
libs/juce7/AppConfig.h View File

@@ -165,11 +165,7 @@
@see VSTPluginFormat, VST3PluginFormat, AudioPluginFormat, AudioPluginFormatManager, JUCE_PLUGINHOST_VST, JUCE_PLUGINHOST_AU
*/
#if 0 //MAC || WINDOWS
#define JUCE_PLUGINHOST_VST3 1
#else
#define JUCE_PLUGINHOST_VST3 0
#endif
#define JUCE_PLUGINHOST_VST3 1
/** Config: JUCE_PLUGINHOST_AU
Enables the AudioUnit plugin hosting classes. This is Mac-only, of course.
@@ -188,6 +184,13 @@
#define JUCE_PLUGINHOST_LADSPA 0
#endif
/** Config: JUCE_PLUGINHOST_LV2
* Enables the LV2 plugin hosting classes.
*/
#ifndef JUCE_PLUGINHOST_LV2
#define JUCE_PLUGINHOST_LV2 0
#endif
//=============================================================================
// juce_audio_utils


+ 11
- 0
libs/juce7/meson.build View File

@@ -5,6 +5,7 @@ if linux_headless
'source/modules/juce_audio_basics/juce_audio_basics.cpp',
'source/modules/juce_audio_formats/juce_audio_formats.cpp',
'source/modules/juce_audio_processors/juce_audio_processors.cpp',
# 'source/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp',
'source/modules/juce_audio_utils/juce_audio_utils.cpp',
'source/modules/juce_core/juce_core.cpp',
'source/modules/juce_cryptography/juce_cryptography.cpp',
@@ -17,6 +18,7 @@ else
'source/modules/juce_audio_basics/juce_audio_basics.cpp',
'source/modules/juce_audio_formats/juce_audio_formats.cpp',
'source/modules/juce_audio_processors/juce_audio_processors.cpp',
# 'source/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp',
'source/modules/juce_audio_utils/juce_audio_utils.cpp',
'source/modules/juce_core/juce_core.cpp',
'source/modules/juce_cryptography/juce_cryptography.cpp',
@@ -35,6 +37,7 @@ juce7_devices_srcs = [
]

juce7_extra_cpp_args = [
# '-DJUCE_PLUGINHOST_LV2=1',
'-std=gnu++17',
'-Wno-non-virtual-dtor',
]
@@ -61,6 +64,14 @@ lib_juce7 = static_library('juce7',
include_directories('.'),
include_directories('source'),
include_directories('source/modules'),
# include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK'),
# include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/serd'),
# include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/sord'),
# include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/sord/src'),
# include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/sratom'),
# include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/lilv'),
# include_directories('source/modules/juce_audio_processors/format_types/LV2_SDK/lilv/src'),
include_directories('source/modules/juce_audio_processors/format_types/VST3_SDK'),
include_directories('../juced/source/dependancies/ladspa_sdk/src'),
juce7_extra_include_dirs
],


+ 4
- 0
meson.build View File

@@ -366,6 +366,10 @@ endif
###############################################################################
# extra files to install

if 'syndicate' in get_option('plugins')
meson.add_install_script('scripts/install-syndicate-scanner.sh')
endif

if 'tal-noisemaker' in get_option('plugins')
extra_lv2_preset_files = [
'TAL-NoiseMaker-Noise4U.lv2/manifest.ttl',


+ 1
- 0
meson_options.txt View File

@@ -119,5 +119,6 @@ option('plugins',
'roth-air',
'swankyamp',
# juce7
'syndicate',
],
)

+ 185
- 0
ports-juce7/changes.patch View File

@@ -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;
}
}

+ 1
- 0
ports-juce7/meson.build View File

@@ -5,6 +5,7 @@ if linux_headless
]
else
plugins = [
'syndicate'
]
endif



+ 85
- 0
ports-juce7/syndicate/AllCommon/AllUtils.h View File

@@ -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;
}
}

+ 62
- 0
ports-juce7/syndicate/AllCommon/MainLogger.cpp View File

@@ -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");
}
}

+ 16
- 0
ports-juce7/syndicate/AllCommon/MainLogger.h View File

@@ -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);
};

+ 12
- 0
ports-juce7/syndicate/AllCommon/NullLogger.hpp View File

@@ -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 { }
};

+ 1
- 0
ports-juce7/syndicate/AllCommon/README.md View File

@@ -0,0 +1 @@
Contains code common to both plugins and the plugin scanning server.

+ 21
- 0
ports-juce7/syndicate/JuceHeader.h View File

@@ -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

+ 28
- 0
ports-juce7/syndicate/JucePluginCharacteristics.h View File

@@ -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"

+ 674
- 0
ports-juce7/syndicate/LICENSE View File

@@ -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>.

+ 77
- 0
ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.cpp View File

@@ -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();
}

+ 21
- 0
ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.h View File

@@ -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;
};

+ 246
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.cpp View File

@@ -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);
}
}

+ 70
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.hpp View File

@@ -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;
};

+ 140
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/CustomScanner.hpp View File

@@ -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)
};

+ 303
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.cpp View File

@@ -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));
}

+ 96
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.h View File

@@ -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);
};

+ 210
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.cpp View File

@@ -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());
}

+ 55
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.h View File

@@ -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();
};

+ 27
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusMessage.h View File

@@ -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) {}
};

+ 194
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.cpp View File

@@ -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);
}

+ 45
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.h View File

@@ -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)
};

+ 210
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.cpp View File

@@ -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");
}
}

+ 87
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.h View File

@@ -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;
};

+ 26
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorListParameters.h View File

@@ -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;
};

+ 78
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.cpp View File

@@ -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);
}

+ 35
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.h View File

@@ -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;
};

+ 59
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.cpp View File

@@ -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();
}
}

+ 26
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.h View File

@@ -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;
};

+ 107
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.cpp View File

@@ -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);
}

+ 29
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.hpp View File

@@ -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;
};

+ 34
- 0
ports-juce7/syndicate/PluginCommon/PluginScanning/SelectorComponentStyle.h View File

@@ -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)) { }
};

+ 59
- 0
ports-juce7/syndicate/PluginCommon/PluginUtils.h View File

@@ -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());
}
}
}
}

+ 186
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots.hpp View File

@@ -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)) {}
};

+ 104
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots_test.cpp View File

@@ -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;
}
}
}

+ 132
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.cpp View File

@@ -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>;

+ 257
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.hpp View File

@@ -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);
}
}
};

+ 47
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine_test.cpp View File

@@ -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;
}
}
}

+ 118
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.cpp View File

@@ -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>;

+ 125
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.hpp View File

@@ -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();
};

+ 48
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter_test.cpp View File

@@ -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;
}
}
}

+ 83
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources.hpp View File

@@ -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;
}
};
}

+ 107
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources_test.cpp View File

@@ -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;
}
}
}

+ 93
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState.hpp View File

@@ -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;
}

+ 108
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState_test.cpp View File

@@ -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;
}
}
}

+ 144
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/DataModelInterface.hpp View File

@@ -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; }
};
}

+ 83
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.cpp View File

@@ -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]);
}
}
}
}

+ 42
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.hpp View File

@@ -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;
};

+ 56
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.cpp View File

@@ -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;
}

+ 47
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.hpp View File

@@ -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;
};

+ 128
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/ModulationSourceDefinition.hpp View File

@@ -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 {}
};

+ 81
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain.hpp View File

@@ -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();
}
};

+ 86
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain_test.cpp View File

@@ -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();
}

+ 66
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.cpp View File

@@ -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;
}

+ 56
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.hpp View File

@@ -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;
};

+ 153
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator_test.cpp View File

@@ -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);
}
}
}
}
}

+ 379
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter.hpp View File

@@ -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));
}
};

+ 417
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter_test.cpp View File

@@ -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();
}

+ 9
- 0
ports-juce7/syndicate/PluginCommon/Processor/DataModel/SplitTypes.hpp View File

@@ -0,0 +1,9 @@
#pragma once

enum class SPLIT_TYPE {
SERIES,
PARALLEL,
MULTIBAND,
LEFTRIGHT,
MIDSIDE
};

+ 5
- 0
ports-juce7/syndicate/PluginCommon/Processor/ModelInterface.hpp View File

@@ -0,0 +1,5 @@
#pragma once

#include "DataModelInterface.hpp"
#include "MutatorsInterface.hpp"
#include "ProcessingInterface.hpp"

+ 216
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.cpp View File

@@ -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;
}
}

+ 113
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.hpp View File

@@ -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);
}

+ 529
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators_test.cpp View File

@@ -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);
}
}
}
}

+ 180
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.cpp View File

@@ -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;
}
}

+ 16
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.hpp View File

@@ -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);
}

+ 733
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.cpp View File

@@ -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;
}
}

+ 83
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.hpp View File

@@ -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);
}

+ 1767
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.cpp
File diff suppressed because it is too large
View File


+ 181
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.hpp View File

@@ -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);
}

+ 474
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.cpp View File

@@ -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;
}
}

+ 64
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.hpp View File

@@ -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);
}

+ 1300
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators_test.cpp
File diff suppressed because it is too large
View File


+ 159
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlConsts.hpp View File

@@ -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;
}

+ 752
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.cpp View File

@@ -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);
}
}
}
}

+ 54
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.hpp View File

@@ -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);
}

+ 926
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader_test.cpp View File

@@ -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);
}
}
}
}
}

+ 245
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.cpp View File

@@ -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);
}
}
}
}
}

+ 24
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.hpp View File

@@ -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);
}

+ 643
- 0
ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter_test.cpp View File

@@ -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));
}
}
}
}
}

+ 76
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.cpp View File

@@ -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);
}
}
}
}
}

+ 14
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.hpp View File

@@ -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);
}

+ 376
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors_test.cpp View File

@@ -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);
}
}
}
}

+ 134
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.cpp View File

@@ -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);
}
}
}
}

+ 19
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.hpp View File

@@ -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);
}

+ 437
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors_test.cpp View File

@@ -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);
}
}
}
}
}
}

+ 106
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.cpp View File

@@ -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());
}
}
}
}
}

+ 9
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.hpp View File

@@ -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);
}

+ 113
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.cpp View File

@@ -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;
}
}

+ 15
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.hpp View File

@@ -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);
}

+ 79
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.cpp View File

@@ -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);
}
}

+ 15
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.hpp View File

@@ -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);
}

+ 235
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.cpp View File

@@ -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);
}
}
}

+ 14
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.hpp View File

@@ -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);
}

+ 424
- 0
ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors_test.cpp View File

@@ -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);
}
}
}
}
}

+ 1
- 0
ports-juce7/syndicate/PluginCommon/README.md View File

@@ -0,0 +1 @@
Contains code common to both Syndicate plugins

+ 12
- 0
ports-juce7/syndicate/PluginScanServer/Main.cpp View File

@@ -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)

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save